iOS - TECH PLAY - TECH PLAY

TECH PLAY

iOS

イベント

該当するコンテンツが見つかりませんでした

マガジン

技術ブログ

はじめに こんにちは、プラットフォームエンジニアリング部ネイティブブロックの とし です。普段は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
目次 はじめに 前提 結論 理由1: プログラムの呼び出しを1つのシェルコマンドに合成できる 理由2: 修正が次のツール呼び出しから反映される MCPを選ぶ場面 まとめ 参考文献 はじめに こんにちは、ヘルシカでiOSエンジニアをしている谷口恭一です。 Claude Codeに開発の作業を任せるようになって私が必要だと感じたのは、人が確かめなくてもエージェントが作業を最後まで進められる状態でした。結果を毎回自分で確かめて、指示し直すのをやめたかったからです。そのためにClaude Codeに足そうと考えたのは、2つの仕組みでした。1つは、エージェントがルールから外れないようにするガードレール。もう1つは、エージェントが作業し、ビルドやテストで検証し、通らなければ修正する、というフィードバックループです。このループが人を介さずに回れば、エージェントは自律的に作業を最後まで進められます。こうしたモデルの周りの仕組みを設計することは、ハーネスエンジニアリングと呼ばれています( OpenAI 、2026-02-11)。 Claude Code自体もモデルの周りのハーネスですが、本記事では、Claude Codeに自分たちで足すプログラム・スキル・フック・権限ルールをハーネスと呼びます。 本記事では、ハーネスのプログラムの例として、エージェント用のタスク管理のプログラムを使います。バックログの項目を決まった形式で読み書きするプログラムで、 backlog というコマンドで動くとします。Claudeはこれを使って、次のように作業を進めます。 backlog show-next-items で、次にやる項目を取り出す backlog take-item で項目を自分の担当にして、実装する テストが通ったら、コミットのハッシュとテスト結果を記録に付けて、 backlog move-item --to=done で完了にする このプログラムには、ガードレールとフィードバックループの両方が入っています。人の判断を待っている項目は、 take-item が担当にさせません。完了にするときに完了条件に達していなければ、何が足りないかを添えて差し戻し、Claudeはそれを読んで直します。 こうしたプログラムをClaude Codeから使えるようにする方法は2つあります。 インターフェース Claudeからの見え方 MCPサーバー MCPツールとして加わる。名前は mcp__<server>__<tool> CLI 組み込みのBashツールから実行するコマンド スキル(使い方などの説明)とフック(決まったタイミングでClaude Codeが実行する処理)は、どちらのインターフェースとも組み合わせられるので、違いが出るのはプログラムのインターフェースです。では、このプログラムはMCPとCLIのどちらで作るのがよいのでしょうか。 前提 本記事が扱うのは、開発者がローカル環境で自分の開発のために使う、ガードレールとフィードバックループのためのプログラムです。SlackやGitHub、社内のAPIなどにつなぐためのMCPサーバーは、外部サービスと連携するためのもので、この話の範囲の外にあります。 また、本記事でCLIと呼ぶのは呼ばれるたびに起動し、最新のコードを実行するコマンドです。CLIでも、常駐プロセス(デーモン)に処理を任せる作りでは、古いコードが動き続けることがあります。状態をローカル環境でファイルなどに都度書き出す仕組みにすることなどによって、常駐プロセスを持たない作りにしています。 調査したドキュメントと仕様の記述は2026年10月13日時点のものです。 結論 エージェントが自律的にフィードバックループを回し、失敗したらハーネスを自己改善する。この仕組みを作るなら、プログラムのインターフェースはCLIのほうが良いと私は考えています。 理由は2つです。1つ目に、Claude CodeがCLIをパイプや条件分岐でつなぎ、中間結果を絞り込んでから読むように動作するようになってきたからです。MCPツールでは、中間結果が毎回すべてコンテキストに戻ることが弱点だと考えています。2つ目に、CLIの場合はエージェントが修正した自分自身のコードが、次のツール呼び出しから確実に反映されることにあります。stdioのMCPサーバーは、仕組みを足さない限り再接続するまで古いコードで動き続けてしまうので、自己改善のループにおける弱点だと考えています。 理由1: プログラムの呼び出しを1つのシェルコマンドに合成できる 私の環境では、Claudeが && や | でいくつものコマンドをつないだ1つのシェルコマンドを組み、Claude Codeがそれを1回のBashツールの呼び出しで実行することが増えました。Claudeのモデル性能の向上により、暗号のような長いコマンドを一気に正確に実行する様子を見たことがある人は多いかも知れません。CLIはこのシェルコマンドの中に組み込めます。次の例は、テストが通ったときだけ、今のコミットのハッシュを付けて項目を完了にするシェルコマンドです。 npm test && backlog move-item B012 --to=done --evidence="$(git rev-parse --short HEAD) とテスト" --json | jq '{status, unblocked}' テストが失敗すれば && の後ろは実行されず、項目は完了になりません。テストが通ったときだけ backlog を呼び、その結果から、新しいステータスと、この項目を待っていて着手できるようになった項目を取り出して読みます。 Claudeは現状これらのコマンドの組み合わせをその場で自律的に考える能力があり、モデル側に柔軟に効率的なコマンド実行を任せてしまうことは大きなメリットがあると考えています。 ClaudeはMCPツールも、互いに関係しない呼び出しならモデルの1回の応答で並列に呼び出せます。しかし、前の結果による条件分岐や中間結果の絞り込みなどは結局できないことになります。よってMCPツールの結果はいったんすべてClaudeのコンテキストに戻り、Claudeが読んでから次の呼び出しを組む流れが強制されてしまいます。 ツールを1つずつ呼ぶと、呼び出しのたびに推論が1回必要になり、中間結果がコンテキストにたまります( Introducing advanced tool use on the Claude Developer Platform 、 Code execution with MCP )。Anthropicの対策は、エージェントが書いたコードの中からツールを呼ばせる、Claude APIのProgrammatic Tool Callingです。Claude Codeでこれに相当するのは、Bashツールから実行するCLIです。 フィードバックループでは、この差が回数に比例して積み重なります。「テストが通ったら完了にし、必要な部分を取り出して読む」を項目ごとに繰り返すからです。CLIなら1つのシェルコマンドで済み、Claudeが読むのは絞り込んだ結果だけになります。これらすべてを1つのスクリプトにして再利用可能にする設計も考えられますが、シンプルなツールをモデル側がうまく組み合わせるほうが柔軟性の観点から良いのではないかと考えています。 シェルコマンドの権限ルールは、 && や | で区切った部分ごとに判定されます。自作のCLIのほかに、 jq のように一緒に使うコマンドも権限ルールに入れておきます。長いシェルコマンドは、権限プロンプトで人が読んで判断しにくくなる点には注意が必要です。 理由2: 修正が次のツール呼び出しから反映される ハーネス自身の自己改善までエージェントに任せたい場合のことを考えた時に、修正がすぐに適応されるかという観点が重要になると考えています。 エージェントが作業の中で失敗したときに、原因になったハーネス(プログラム・スキル・フック・権限ルール)をエージェント自身が修正し、修正したハーネスで作業を続ける流れを作るイメージです。作業を回すループの外側で、エージェントがハーネスを直すループが回ります。 例のタスク管理のプログラムで考えます。エージェントが、人の判断を待っている項目を完了にしてしまったとします。原因は、 move-item が人の判断待ちかどうかを確かめていなかったことです。エージェントはこの確認を backlog のコードに足し、テストを通します。CLIなら、次に backlog move-item を呼んだときから新しいルールで判定されます。stdioのMCPサーバーなら、再接続するまで古いルールのまま動くので、同じ失敗が起き得ます。 このループを自律的に回すには、エージェントが修正したものが、実行中のセッションにその場で反映される必要があります。修正のたびに人がMCPサーバーの再接続やClaude Codeの再起動をするなら、ループはそこで人を待ちます。 stdioのMCPサーバーは、Claude Codeが自動では再接続しません( MCP )。 list_changed 通知でツールの一覧は再取得されますが、実行中のプロセスは再起動されず、コードの修正を反映するには /mcp からの再接続が要ります。定期実行のように人が見ていないセッションでは再接続する人がいないので、自己改善のループはそこで止まります。 MCPでも、仕組みを足せば再接続なしに新しいコードで動かせます。プロキシやファイル監視でサーバーを入れ替える方法や、CLIをラップするMCPサーバー(ラッパー)を作り、 tools/call を受けるたびにラップしたCLIを起動する方法です。ただし、どちらも管理するプロセスが1つ増えます。また、ツールを追加したり名前を変えたりしたときは、Claude Codeに確実には反映されません。ラッパーの場合は中身が結局CLIになるので、理由1の合成はできないまま、CLIとMCPサーバーの2つを持つことになります。CLIなら、何も足さずに次の呼び出しから反映されます。 Claude CodeのBashツールは、シェルコマンドごとに別のプロセスで実行します( ツールリファレンス )。常駐プロセスを持たないCLIなら、修正すれば次のツール呼び出しから新しいコードで動きます。ビルドが要るCLIでも、エージェントがビルドとインストールまで済ませれば同じです。フックから呼ぶCLIも同じです。プロジェクトの .claude/ に置いたスキルと設定(フックや権限ルール)も、再起動なしで実行中のセッションに反映されます。プログラム・スキル・フックのどこをエージェントが修正しても、次のツール呼び出しから新しいハーネスで動きます。 MCPを選ぶ場面 ユーザー単位の認可が要るときは、MCPのほうが向いていると考えます。MCPの仕様では、HTTPでつなぐサーバーはOAuth 2.1で認可し、ユーザーごとにアクセストークンを発行して、スコープで権限を絞れます。複数のユーザーが使うサービスなら、これは大きな利点です。また、そもそも暗号のようなBash実行をAuto Modeで許していることが前提であり、適切な権限管理をしたい場合はやはりMCPを使うべきだと思います。手元の環境をCLIで用意し、どうしても必要な時にMCP化を検討するという原則が最も適切なのではないかと私は考えています。「AIから呼ぶからMCPが良い」と私は最初は思っていましたが、そうではないと後から思いました。 まとめ エージェントが自律的にフィードバックループを回し、失敗したらハーネスも自分で直していくような仕組みを目指すなら、プログラムのインターフェースはCLIのほうが良いと私は考えています。CLIなら、Claudeがその場でシェルコマンドを組み、中間結果を絞り込んでから読めるからです。また、エージェントがハーネスを修正すると、次のツール呼び出しから反映されるからです。このような軽量なハーネス用CLIを設計するときは、結果をJSONで返し、常駐プロセスを持たない作りにしておくと、この2つの利点を活かせます。 一方で、ユーザー単位の認可が要るときはMCPが向いています。どちらを選ぶかは、目的によるという結論になると思います。Claude Codeを使う時に同じような迷いがあった場合、1つの参考にしていただけると嬉しいです。 参考文献 OpenAI, Harness engineering: leveraging Codex in an agent-first world , 2026-02-11 Anthropic, Introducing advanced tool use on the Claude Developer Platform , 2025-11-24 Anthropic, Code execution with MCP: Building more efficient agents , 2025-11-04 Claude Codeドキュメント: MCP / ツールリファレンス
環境の変化に伴い、過去に下した合理的な判断がもはや有効でなくなることはよくあります。この記事では、LINEアプリの成長に伴い、動的設定配信システムである「サービス設定」のiOSクライアントがどのような...

動画

書籍