
Swift
イベント

マガジン
技術ブログ
はじめに こんにちは、プラットフォームエンジニアリング部ネイティブブロックの とし です。普段はZOZOTOWN iOSアプリの基盤開発を担当しています。今回は現在取り組んでいるZOZOTOWN iOSアプリのSwift 6移行の進め方と方針について紹介します。 Swift 6移行について最初に自分が着手した時「難しそう」「よくわからない」「本当にここまで大きく変えるのか」と思い、正直かなり腰が重かったです。ただ、実際にはSwift 6で何を達成したいのかという背景を深掘りしながら進めていくことで、ぼんやりと輪郭が見えてきた気がします。Swift 6移行は、完璧を求めて対応しようとするとかなり大変です。しかし、頑張りすぎず、まずはできることから始めれば、一歩ずつ進めていけると感じました。 なお、2026年10月時点でZOZOTOWNのSwift 6移行はまだ完了していません。 目次 はじめに 目次 背景 なぜSwift 6移行が難しいのか 頑張りすぎないための選択1:最新の仕組みに乗っかる Xcodeを最新にする ビルド設定を2つ変える 頑張りすぎないための選択2:基本はMainActorで動かす 頑張りすぎないための選択3:モジュールごとに小さく試す 頑張りすぎないための選択4:全部は対応しない まとめ 背景 まず、Swift 6移行を始める前のZOZOTOWN iOSアプリの状況を紹介します。 2026年4月時点で、ZOZOTOWN iOSアプリはSwift 6.2コンパイラ(Xcode 26.2)を使いつつ、Swift 5言語モードで動いていました。Swift Concurrencyへの対応はまだ行われていない状態でした。 言語モードを上げる前に、移行の規模感を数字で把握することにしました。Swift 5言語モードのままStrict Concurrency CheckingをCompleteにすると、警告が2,500件以上出ていました。 正直、この2,500件の警告をすべて対応してからSwift 6言語モードへと上げるのは、あまり現実的ではないと感じていました。また、Swift 6への理解が深いメンバーもあまりおらず、チームとしてキャッチアップする必要がありました。 なぜSwift 6移行が難しいのか ではいったいなぜ、Swift 6移行が難しいのでしょうか?その理由は下記の難しさが同時にやってくるからです。 言語仕様の理解が難しい async/await、Actor isolation、Structured concurrencyといった難しい概念をきちんと理解する必要がある 何が正解なのかがわからない アーキテクチャはAppleが定義していないので、どこまでをUIレイヤーとしてMainActorで実行し、何にnonisolatedを付け、何をactorで定義するかを自分たちで決める必要がある Swift 6.0と6.2で設計思想が変わっており、過去のWWDCの説明が今の前提と合わないこともある チームの理解度が追いついていない 一部の人が理解するだけでは足りず、メンバー全員が同じ前提で書ける状態にそろえる必要がある 実は、最初は正攻法で進めようとしていました。Strict Concurrency CheckingをTargetedにして、出てきた警告を頭から順に直していく進め方です。しかし、自分を含めてチームの対応はまったく進みませんでした。チームとしてどう進めるかの方針が決まっておらず、警告をどう直すべきかもよく分からなかったためです。 振り返ると、この進め方は3つの難しさを最初からすべて背負うものでした。 そこで私たちは、難しさを一度に引き受けるのをやめ、分けて受け取ることにしました。この方針で取った頑張りすぎないための選択は、下記の4つです。 選択1:最新の仕組みに乗っかる(対応済み) 選択2:基本はMainActorで動かす(対応済み) 選択3:モジュールごとに小さく試す(進行中) 選択4:全部は対応しない(進行中) それぞれの詳細は、次章から順に紹介します。 頑張りすぎないための選択1:最新の仕組みに乗っかる 選択の1つ目は、自分たちで頑張って対応する前に、Swiftの最新の仕組みに乗っかることです。 Swift 6.0の時点では、データ競合を防ぐための対応が開発者にとって負担の大きいものになっていました。Swiftチーム自身もこの点を認めていて、Swift 6.2ではデータ競合の安全性を取り入れやすくする仕組みが入っています。開発者が楽をするために用意された仕組みなので、まずはそこへ乗っかることにしました。 github.com Xcodeを最新にする 設定を変える前に、まずXcodeを当時の最新(Xcode 26.5)にしました。 XcodeとSwiftコンパイラは強く結びついています。同じXcodeのメジャーバージョンでも、中のSwiftコンパイラのマイナーバージョンは異なることがあります。マイナーバージョンが違えば、診断の出方が違ったり、不具合が直っていたりします。Concurrency関連は、まだコンパイラ側のバグや挙動の違いが多く見られる領域です。そのため、コードに手を入れる前に、できるだけ最新バージョンのコンパイラを使うのがおすすめです。 ZOZOTOWNでも実際に、Approachable Concurrencyを有効にするとエラーが出るかどうかがXcodeのバージョンで変わりました。Swift 6移行の一歩目は、Xcodeのアップデートです。 ビルド設定を2つ変える Xcodeを更新したら、ビルド設定を2つ変えました。Approachable Concurrencyを有効にし、メインのアプリターゲットだけDefault Actor IsolationをMainActorにしました。APIClientやログ基盤をはじめとした他のモジュールは、デフォルト値のnonisolatedのままです。言語モードはどちらもSwift 5のままです。 Approachable Concurrencyとは、並行処理に関する複数のupcoming featureをまとめて有効にするXcodeのビルド設定です。含まれる機能の1つがNonisolatedNonsendingByDefault( SE-0461 )で、nonisolatedなasync関数の実行先を呼び出し元にそろえます。 これまでは、nonisolatedなasync関数を呼ぶと、処理が呼び出し元のactorから離れていました。そのため、MainActorから引数や戻り値を受け渡すたびに、その型をSendableにする必要がありました。SE-0461を有効にすると、処理は呼び出し元のactorのまま実行され、actorの境界をまたぎません。その結果、Sendableを付けなくても済む場面が増えます。 Default Actor Isolationとは、隔離先を明示しない宣言をどこに隔離するかを決めるXcodeのビルド設定です。MainActorにすると、何も書いていない宣言はデフォルトでMainActorに隔離されます( SE-0466 )。これらはSwift 6.2(Xcode 26)から入った機能になります。 Xcodeのプロジェクトでは、ターゲットのBuild Settingsにある「Swift Compiler - Concurrency」から、この2つの設定を変更できます。 Swift Packageで管理しているモジュールでは、Package.swiftのswiftSettingsで指定します。Package.swiftにはApproachable Concurrencyをまとめて有効にする設定がありません。そのため、含まれるupcoming featureを1つずつ有効にしています。ZOZOTOWNではモジュールのデフォルトの隔離先をnonisolatedのままにしているので、defaultIsolationにはnilを指定しています。 // swift-tools-version: 6.2 import PackageDescription let package = Package( name : "SampleModule" , targets : [ .target ( name: "SampleModule" , swiftSettings: [ // Default Actor Isolationに関する設定 // MainActor.self(MainActor)かnil(nonisolated)が入る .defaultIsolation( nil ), // XcodeのApproachable Concurrencyに含まれる機能を1つずつ有効にしている .enableUpcomingFeature( "NonisolatedNonsendingByDefault" ), .enableUpcomingFeature( "InferIsolatedConformances" ), .enableUpcomingFeature( "InferSendableFromCaptures" ), .enableUpcomingFeature( "DisableOutwardActorInference" ), .enableUpcomingFeature( "GlobalActorIsolatedTypesUsability" ) ] ) ], // Swift 5言語モードでビルドする swiftLanguageModes : [ .v5 ] ) この2つの設定は、移行の最初に入れました。どちらもコードの書き方の前提を決める設定です。途中で前提が変わると「このコードは今どのactorで動いているんだっけ」とメンバーが迷い、キャッチアップのコストが高くなります。 最初から入れられたのは、ZOZOTOWNのConcurrency対応がほとんど進んでいなかったからです。既存コードに書き方の前提がまだなかったので、設定で前提を決めてしまえました。対応が進んでいなかったプロダクトだからこそ取れた選択肢です。 また、Approachable Concurrencyに含まれる機能は、今後デフォルトで有効になっていくと予想しています。Xcode 26で新しく作成したプロジェクトでも、この2つの設定はすでに有効です。先に合わせておけば、将来もう一度前提を変えずに済みます。 頑張りすぎないための選択2:基本はMainActorで動かす 選択の2つ目は、処理をどこで実行するかを厳密に設計しすぎず、基本はMainActorで動かすことです。 選択1でDefault Actor IsolationをMainActorにしたアプリターゲットでは、何も書いていないコードはMainActorで動きます。データ競合の心配は減る一方で、本来メインスレッドで動かす必要のない処理までMainActorに集まります。そこで、どこまでをMainActorで実行するかを決める必要がありました。 検討の中では、UI関連以外のすべてにnonisolatedを付けて厳密に書き分ける案もありました。Swift 6の設計思想には一番忠実ですが、付ける対象が膨大になり、Sendable対応も連鎖します。何より、メンバー全員がisolationの使い分けを理解していることが前提になります。 最終的には、パフォーマンスが気になる部分以外はMainActorのままとする方針へ寄せました。この方針を決めるにあたり、実機でInstruments(HangsとTime Profiler)を使ってパフォーマンスを計測しました。その結果、ほとんどの処理をMainActorで実行しても問題ないことを確認しています。重いJSONのデコードなど、計測でメインスレッドの負荷が問題になった箇所だけ、 @concurrent を付けてMainActorから処理を逃がします。それ以外ではnonisolatedを付けず、アプリターゲットのコードはすべてMainActorで実行しています。性能面では厳密に付ける案の方が有利ですが、メンバーが今までの書き方から特別に意識を変えることなく、Swift 6へ移行できることを目標にしました。 最初から完璧な書き分けを求めず、チームメンバーに無理をさせないことも、本記事で言う「頑張りすぎない」の1つです。 頑張りすぎないための選択3:モジュールごとに小さく試す ZOZOTOWNには、メインのターゲットの他に、APIClientやログ基盤をはじめとしたいくつかのモジュールがあります。 選択の3つ目は、Swift 6言語モードへの切り替えをモジュール単位で順番に進めることです。この作業は、現在も進行中です。 アプリ全体を一度にSwift 6言語モードへ上げる道は取りませんでした。モジュール単位で進めれば、1回の切り替えで向き合うエラーはそのモジュールの分だけに収まります。 モジュールの大きさもさまざまです。小さいモジュールもあれば、大きいモジュールもあります。そこで、とにかくシンプルで小さいモジュールからSwift 6言語モードへ切り替えていきました。 小さいモジュールから始めたことで、次のメリットがありました。 対応する箇所が少ないので、対応しやすい 小さいモジュールで得た知見をためて、次のモジュールの対応に生かせる 一度に大きな変更をリリースせず、モジュールごとにこまめにリリースできるので、プロダクトへの影響を抑えられる モジュールを小さく区切ったうえで、もう1つ頑張りすぎないために決めたのが、選択4です。 頑張りすぎないための選択4:全部は対応しない 選択の4つ目は、すべての警告を解消してからSwift 6言語モードに上げるやり方を取らないことです。特に一番大きい本体ターゲットでは、この方針を積極的に活用していく想定です。 Swift 6言語モードの目的は、データ競合をコンパイルの段階でコンパイラに検知してもらうことです。もちろん、すべて対応できるのであれば、対応した方が良いです。ただ、既存コードをすべて対応させてから上げるのは、特に大きいモジュールではあまりに大変です。 そこで、既存コードと新しく書くコードを分けて扱います。 既存コードで対応しきれない箇所には、 @preconcurrency や @unchecked Sendable を付けたうえでSwift 6言語モードに上げる 新規追加・変更するコードは、Swift 6のコンパイラにデータ競合を検知してもらう @preconcurrency や @unchecked Sendable を付けた箇所は、コンパイラの検査から外れます。安全性を手放しているように見えますが、Swift 5言語モードのままの状態と比べても、コンパイル時の検査に差分はありません。 Swift 5言語モードでも、データ競合の恐れがある箇所は存在しています。コンパイラがそれを特定していないだけです。つまり、Swift 5言語モードのままにしておくことは、Swift 6言語モードで @preconcurrency などを付けることと同じ状態です。 むしろ、Swift 6言語モードに上げてこれらを付けておけば、データ競合の恐れがある箇所に目印が付きます。あとから対応するときも、目印を手がかりに探せます。 全部対応し終えるまでSwift 5言語モードで待つより、先に上げてしまった方が、コンパイラに守ってもらえる範囲は早く広がります。 ただし、 @unchecked Sendable はスレッド安全性を私たち開発者が保証する宣言です。使う場所は既存コードに絞り、スレッド安全性を私たちで保証できる型にだけ付けます。 実際にやってみると、不都合が出てくるかもしれません。その時は、その時に対応すれば良いと考えています。問題が起きてから考えると割り切ることも、頑張りすぎないための1つです。 まとめ 本記事では、ZOZOTOWN iOSアプリのSwift 6移行で私たちが取った、頑張りすぎないための4つの選択を紹介しました。 Swift 6移行が難しいことは、取り組んでみて改めて感じました。それでも、完璧を求めず頑張りすぎないことを意識したことで、最初の一歩を踏み出し、歩き始めることができました。 ここまで書いた内容はあくまで現在の方針で、今後変えることも十分にあると思います。だからこそ、Swift 6移行に腰が重くなっている方がいれば、ぜひ頑張りすぎず、できそうなところから始めてみてください。 ZOZOでは、一緒に大規模なサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
環境の変化に伴い、過去に下した合理的な判断がもはや有効でなくなることはよくあります。この記事では、LINEアプリの成長に伴い、動的設定配信システムである「サービス設定」のiOSクライアントがどのような...
SwiftのDateFormatterを使用する際、iPhoneの「24時間表示」設定によって予期せぬ文字列が出力される問題と、その回避策としてロケールをPOSIX形式に固定する方法を解説します。
動画
該当するコンテンツが見つかりませんでした












