Xcode16にアップデートする際の対応について

# はじめに
App Store Connectへのアップロードが4/24からXcode16が必須になります。
https://developer.apple.com/jp/news/upcoming-requirements/?id=02212025a

弊社が提供しているwithアプリでは、Xcode16への移行が完了し、一部トラブルはあったものの現在は落ち着いた状況となりましたので、
これからXcode16への移行をされる方や、Xcode16移行は完了したが対応内容を見直したい方向けに社内で対応した内容のまとめを共有させていただきます。

# 対応方針
大まかな対応方針としては、Xcode16アップデートで新たに発生したwarningについては後回しにしないように定めました。
基本的にXcode16でビルドが通るようにしておけば開発面では困ることはないものの、やはり期限を決めずに置いてしまうと消化するまで時間がかかってしまったり、CIなどで検知している場合はノイズになりやすいためです。
withではすでに対応が終えたからこそ言えるのですが、今回のXcodeアップデートに伴って発生するwarningはそれほど大変でもないと思いますので、もしこれからXcode16へ移行される方は同じようにwarning対応も一緒にやることをお勧めします。

## 具体的な方針
対応方針のところで書いたように、warningはなるべく一緒に対応することを念頭にいれましたが、
Xcode16にアップデートした直後は既存の型に後からプロトコル準拠を追加する箇所で多く発生するでしょう。

その際に、警告にフォーカスをあてると、`@retroactive`を付与するサジェストが出るかと思います。
`@retroactive`というのは、
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0364-retroactive-conformance-warning.md 
で提案された機能で、既存の型に後からプロトコル準拠を追加することを宣言するための識別子です。
`@retroactive`を付与すると、warningは抑制されるようになります。
しかし、`@retroactive`はただの識別子にすぎず、モジュール間で思わぬ衝突が発生するリスクは伴ったままです。
そのため根本的対応の先送りにしかならないため、極力`@retroactive`を利用しない方針としました。

弊社が対応を始めたのは、Xcode16必須化のアナウンスがされる前で、スケジュールに余裕があったためこの方針を取りました。
しかしこれから対応される方は4/24までに対応を終えないとアップデートができないため、`@retroactive`の利用も検討すると良いかもしれません。

以下、withで発生したwarning箇所とその対応になります。

# 対応内容
## 引数に`@MainActor`が追加された箇所の対応
`WKNavigationDelegate.webView(_:decidePolicyFor:decisionHandler:)`や
`WKUIDelegate.webView(_:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler:)`など、
クロージャー引数に`@MainActor`ラベルが付与されました。
Xcode上のwarningメッセージからだと原因はわかりにくいですが、Appleのドキュメントの定義が変わっているため、
きちんとメソッドを揃えるよう対応しました。
https://developer.apple.com/documentation/webkit/wknavigationdelegate/webview(_:decidepolicyfor:decisionhandler:)-2ni62

```swift:before
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {}
```

```swift:after
// completionHandlerに@MainActorが追加する
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping @MainActor () -> Void) {}
```

## JSONDecoderを継承したクラスに`@unchecked Sendable`を適合する
withではAPI通信にJSONを使っているのですが、レスポンスのパース処理に失敗した場合のデバッグをしやすくするよう、JSONDecoderを継承した独自のクラスを使っています。
しかしJSONDecoder側は`@unchecked Sendable`のprotocolに準拠していたものの、JSONDecoderを継承した独自クラスのほうでは`@unchecked Sendable`の準拠をしていませんでした。
てっきりJSONDecoderを継承したクラスであれば、JSONDecoderの準拠している`Sendable`にも自動で継承されるかと思っていたのですが、どうやらそうでないらしいです。

## モジュール外の定義の型に対してExtensionで別のプロトコルへの準拠をさせないようにする
Extensionを使って型を別のprotocolへ適合する処理を入れたい場合があるかと思います。
そのような場合にXcode16からは、型定義とextensionする箇所でモジュールが異なる場合、
warningが発生するようになりました。

なぜwarningがでるようになったかについては
https://maiyama4.hatenablog.com/entry/2024/07/01/154134
の記事が大変参考になりました。

withではエラーを複数待ち受ける処理があり、その際はエラーオブジェクトを配列にいれ、配列の中身がエラー型であれば配列自体をエラーに適合する、という処理が書かれていました。

```swift
struct MyError: Error {
    ...
}
extension Array: Error where Iterator.Element == MyError {}
```

かなり乱暴なやり方ではありますが、一応ちゃんと動作はします。
しかし、こうしたArrayをErrorに独自で適合することが今回warning発生する原因となりました。

この例に関しては、Arrayを拡張するのではなく、MyError配列を内包するエラー適合したstructを作る形で修正しました。

```swift
struct MyErrors: Error {
  let errors: [MyError]
}

struct MyError: Error {
    ...
}
```

## そのほか細かい変更箇所
- Sign In with Appleのエラー型の追加
    - `.matchedExcludedCredential`, `.credentialImport`, `.credentialExport`の3つが新たにエラー型に追加されました
- `DispatchQueue.main.async`で`self`を参照している箇所への対応
  - Sendable周りの影響により`DispatchQueue.main.async`内で`self`を使用する箇所にwarningがでるようになりました



# 重大な問題
ここまでの対応で、Xcodeアップデートに伴って発生したwarningはなくなりましたが、重大な問題が2点ありました。

## UITableView、UICollectionViewでクラッシュする
UITableView、UICollectionViewでのセルの生成処理でクラッシュが発生するようになりました。
クラッシュの発生条件は`cellForRowAt`、`cellForItemAt`内で2回以上セルを`dequeue`した際に発生したようです。
実際に使われないセルまで`dequeue`するのはパフォーマンス面でも無駄ですし、普通に考えれば避けるべき実装ではありますが、
withではこうしたよくないコードがありました。


```swift:クラッシュする例
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    // 2回目のdequeueReusableCellが呼ばれた際にクラッシュする
    let cell2 = tableView.dequeueReusableCell(withIdentifier: "cell2", for: indexPath)
    return cell
}
```

このクラッシュに関しては、`UITableView`、`UICollectionView`といった利用頻度が高いViewではありながら、
セルの出しわけ条件が厳しければ厳しいほど顕在化しにくい特性があります。

そのため、現在のソースコードの状態だけでなく、今後追加されるソースコードにも意識が向くよう気をつけていきたい問題です。


## UIApplication.shared.open(url)が機能しなくなる

URLを開く処理には、[UIApplication.openURL(_:)](https://developer.apple.com/documentation/uikit/uiapplication/openurl(_:))と[UIApplication.open(_:options:completionHandler:)](https://developer.apple.com/documentation/uikit/uiapplication/open(_:options:completionhandler:))
の2種類があり、前者は現在deprecatedになっています。

しかし、withでは前者の`UIApplication.openURL(_:)`を使用している箇所が残っていました。
Xcode16以降では、iOS18以上で`UIApplication.openURL(_:)`を呼び出しても反応しない動作になりました。
https://developer.apple.com/documentation/uikit/uiapplication/openurl(_:) にも
> Calling this method has no effect. Use the open(_:options:completionHandler:) method instead.

とかかれている通り、メソッドを呼び出しても反応しなくなるのですが、iOS18未満は問題なく動作します。
そのため、動作確認端末の環境によっては問題なくURLを開けてしまうため注意が必要です。


# 最後に
今回、弊社のwithアプリでXcode16にアップデートした際の対応内容を共有させていただきました。
これからXcode16へ移行をする方はもちろん、Xcode16移行は終わったがトラブルが発生した場合にも参考にしていただければと思います。

# PR
withではエンジニアの採用を行っています。
興味がありましたら、ぜひお問い合わせください！
https://enito.co.jp/careers/