🛩️

【iOS18対応】事業の急成長・品質・最新技術追従のトレードオフ関係を断ち切るための段階的Swift6対応

2024/10/09に公開

こんにちは!令和トラベルにて旅行アプリ「NEWT (ニュート)」の開発をしているRickです。

2024年9月17日、日本でもiOS18が正式リリースされましたね!今回は、iOS18対応のリリースをどのように進めていったかを、関連するSwift6対応を中心にご紹介します。

また、本記事の内容は、先日行われた【GO/note/令和トラベル】After iOSDC & DroidKaigi 2024 で話した内容の詳細版となります。併せてこちらもご覧ください。

https://speakerdeck.com/ryu1sazae/newtniokeruios18dui-ying-nojin-mefang

事業の急成長・品質・最新技術追従のバランスを重視

iOS18の対応を実施するにあたって、NEWTでは「事業成長・アプリの品質と最新技術追従のバランス」を重要視していました。

事業の急成長に応える

こちらの記事でも語られている通り、NEWTは次なるステージへと段階を進め、更なる価値をカスタマーに届けようとしています。

https://www.reiwatravel.co.jp/news/240909-02

それ故、多くの新機能開発・既存機能改修を行っており、iOS18対応直近の8・9月だけでも多くの機能をリリースしました。こちらに載せきれなかったものも多くあります。

NEWTのiOSアプリは、私を含めてiOSエンジニア2名で開発を行っています(2024年9月時点)。そのため、直前にiOS18対応の準備を進めていては開発が遅れてしまう機能が出てきてしまうことが分かっていました。

安心・安全な旅行体験をお届けするために

旅行は多いと年に数回行く方もいますが、実際のところ数年に一回、という方がほとんどです。そんな貴重な体験を最大限素敵なものにしてもらうために、最高の品質をカスタマーに届ける必要があり、それを保証する必要があります。

そのため、安定性が保証できていない状態でのリリースは、どんなに魅力的な選択を犠牲にしてでも避けなければいけません。

最新技術を追従するメリットとは?

エンジニアとしては、最新技術を利用し、実装容易性・プロダクトとしての表現幅を合わせて獲得できる可能性を高めたいという欲求も捨てきれません。どの施策を優先的に行うかのデザインはするものの、それらをできるだけ短期間でカスタマーに届けることも重要です。

”スピード”も、誰かにとっての”価値” ですよね。より新しい技術を使うと、世の中で進化するそれとの相対的なギャップを抑えることができ、その結果見えてくる ”スピード” は無視できません。他にもいくつかのメリットが挙げられるでしょう。

これらのバランスをどう実現するか?

これらを全て叶えるためにどのような手順・スケジュールで対応を進められるかを、WWDC2024が終わった頃から考え始めていました。iOS18をサポートするXcode16にて、Swift6のコンパイラが同梱されたわけですが、幸いSwift6への対応は ”段階的” に実施することが可能です。

「それが何を意味するか」・「段階的な移行を活用し、上述したバランスをどのように実現したのか」を、Swift6対応の話も交えながら解説していきます。

段階的なSwift6対応

Swift6対応を先取りするための2種類のFlag

Swift6へのメジャーバージョンアップデートでは、 StrictConcurrency に代表されるいくつかの破壊的変更が有効化されました。

ここでAppleが、「Xcode16より前のバージョンを使いつつも、Swift6の一部機能は先に享受したい・準備したい」「Swift6の機能を徐々に有効化して、Swift6の言語モードを有効化できる状態に近づけておきたい」という需要に応えてくれています。

これからご紹介するFlagを使用すると良いでしょう。

これらのFlagを用いることで、Swift6より前のversionを使いつつも、Swift6以降で有効になる機能を先行して使用することができます。

各Swift versionにおいて、どちらのFlagでどの機能を有効化できるかは、Swiftのリポジトリから確認できます。e.g. Swift5.10で利用可能なFlag一覧

例えば、Swift5.10においてStrictConcurrencyは、enableExperimentalFeatureで有効化できます。

// swift-tools-version:5.10

import PackageDescription

let package = Package(
  name: "NEWT",
  ・・・
  targets: [
    .target(
      name: "ApiClient",
      dependencies: [
        ・・・
      ],
      path: "Sources/Dependencies/ApiClient",
      swiftSettings: [
        .enableExperimentalFeature("StrictConcurrency"), // SE-0337
      ]
    ),
    ・・・
  ]
)

NEWTアプリでは、Xcode16の登場時になるべく多くのTargetでSwift6モードを使用すべく、Xcode15.4の利用時から、個々のTargetに対して可能な限りSwift6以降の機能を有効化させていました。

// swift-tools-version:5.10

import PackageDescription

extension SwiftSetting {
  static let upcomingFeatures6: [Self] = [
    .enableUpcomingFeature("ConciseMagicFile"), // SE-0274
    .enableUpcomingFeature("ForwardTrailingClosures"), // SE-0286
    .enableUpcomingFeature("BareSlashRegexLiterals"), // SE-0354
    .enableUpcomingFeature("DeprecateApplicationMain"), // SE-0383
    .enableUpcomingFeature("DisableOutwardActorInference"), // SE-0401
    .enableUpcomingFeature("IsolatedDefaultValues"), // SE-0411
    .enableUpcomingFeature("GlobalConcurrency"), // SE-0412
  ]

  static let upcomingFeatures7: [Self] = [
    .enableUpcomingFeature("ExistentialAny"), // SE-0335
  ]

  static let strictConcurrency = .enableExperimentalFeature("StrictConcurrency")
}

let package = Package(
  name: "NEWT",
  ・・・
  targets: [
    .target(
      name: "AppFeature",
      dependencies: [
        ・・・
      ],
      path: "Sources/Features/AppFeature",
      swiftSettings: upcomingFeatures6
        + upcomingFeatures7
        + [strictConcurrency]
    ),
    .target(
      name: "ApiClient",
      dependencies: [
        ・・・
      ],
      path: "Sources/Dependencies/ApiClient",
      swiftSettings: upcomingFeatures6
        + upcomingFeatures7
        + [strictConcurrency]
    ),
    ・・・
  ]
)

部分的にSwift6の利用開始を遅らせるためのMode指定

以前までSwiftのversionは、Package単位でしか指定できませんでした。つまり、従来の仕組みであれば、swift-tools-version: 6.0を指定した場合、Package.swiftファイルはそこで定義している全てのTargetに対してSwift 6言語モードが有効されます。

これは同時に、全てのTargetにおいて、Swift6からデフォルトで有効になる機能(e.g. StrictConcurrency)を有効化することになります。破壊的変更を要する場合、一度に対応するのは根気強さ・時間・品質担保のコストが必要です・・・。

ここで便利なのが swiftLanguageMode です。どのSwift versionを使うかを、Target毎に指定できるようになり、すぐに対応が難しいTargetに関しては、Swift6への対応を遅らせることができます。詳しくは SE-0435:Swift Language Version Per Target をご覧ください。

// swift-tools-version: 6.0

let package = Package(
  name: "NEWT",
  products: [
    // ...
  ],
  targets: [
    // Uses Swift6
    .target(
      name: "ApiClient",
    ),
    // Still uses Swift5
    .target(
      name: "AppFeature",
      dependencies: [
        "ApiClient"
      ],
      swiftSettings: [
        .swiftLanguageMode(.v5)
      ]
    ),
    // ...
  ]
)

iOS18対応のリリースタイミングにおける、以下の要求・願望を同時に叶える方法を模索しました。しかし、例年の傾向から、Xcode16 RCリリース 〜 iOS18の正式リリースの期間は短いと予想されていたため、良い塩梅で折り合いをつける必要がありました。

  • 機能開発は通常通りのスピードをなるべく維持したいという事業目線での要求
  • 新OS対応時は、QAチームによるテストを手厚めに実施するため、このタイミングで可能な限りSwift6の対応を進めておきたいというエンジニアの願望

ここで、TCAを採用しているNEWTのPackage構成を部分的ですが紹介します。

サービス内のコンテキスト毎に分けられたFeature単位でTargetを区別し、各画面を構成するView・TCAのReducerが定義されています。このFeatureが、swift-dependencies に依存する各Dependency・共通コンポーネントやリソース・便利関数を格納するその他のTargetに依存します。

UIを含むFeature targetに関して、以下の懸念が明確に存在していたため、Swift6有効化を意図的に遅らせる決断をしました。

  • 新OS登場直後に不具合発生が予想されやすく・対応に時間もかかり得る

    Beta版を用いた開発だと、Swift・Xcode自体に不具合が起因するケースも少なくなく、iOSエンジニア2名で開発をするにあたっては、そのコストを無視できないと判断。

  • TCAへの依存から生じるコストの最小化

    2024/09前半時点で、Concurrency周りのwarningやerror対応、今後の方針に対するdiscussionが活発に行われている状態でした。この状況下でStrictConcurrencyの対応を完了させるよりも、それらが落ち着いたタイミング、または予定されているTCA v2.0の方針がもう少し固まってからの方が、ライブラリに依存するコストを最小化できると判断。

改めて、先ほど紹介した2つのフラグも使い、Xcode16では以下のようなPackage.swiftを記述していました。

// swift-tools-version:6.0

import PackageDescription

extension SwiftSetting {
  static let upcomingFeatures6: [Self] = [
    .enableUpcomingFeature("NonfrozenEnumExhaustivity"), // SE-0192
    .enableUpcomingFeature("ConciseMagicFile"), // SE-0274
    .enableUpcomingFeature("ForwardTrailingClosures"), // SE-0286
    .enableUpcomingFeature("StrictConcurrency"), // SE-0337
    .enableUpcomingFeature("ImplicitOpenExistentials"), // SE-0352
    .enableUpcomingFeature("BareSlashRegexLiterals"), // SE-0354
    .enableUpcomingFeature("DeprecateApplicationMain"), // SE-0383
    .enableUpcomingFeature("DisableOutwardActorInference"), // SE-0401
    .enableUpcomingFeature("IsolatedDefaultValues"), // SE-0411
    .enableUpcomingFeature("GlobalConcurrency"), // SE-0412
    .enableUpcomingFeature("RegionBasedIsolation"), // SE-0414
    .enableUpcomingFeature("InferSendableFromCaptures"), // SE-0418
    .enableUpcomingFeature("DynamicActorIsolation"), // SE-0423
    .enableUpcomingFeature("GlobalActorIsolatedTypesUsability"), // SE-0434
  ]

  static let upcomingFeatures7: [Self] = [
    .enableUpcomingFeature("ExistentialAny"), // SE-0335
  ]
}

let package = Package(
  name: "NEWT",
  ・・・
  targets: [
    .target(
      name: "AppFeature",
      dependencies: [
        ・・・
      ],
      path: "Sources/Features/AppFeature",
      swiftSettings: upcomingFeatures6
        + upcomingFeatures7
        + [.swiftLanguageMode(.v5),]
    ),
    .target(
      name: "ApiClient",
      dependencies: [
        ・・・
      ],
      path: "Sources/Dependencies/ApiClient",
      swiftSettings: upcomingFeatures7
    ),
    ・・・
  ]
)

iOS18対応のスケジュール

2024/05〜2024/06 - Swift6機能の有効化

基本的には6月までに、Xcode16でSwift6を部分適応する準備を終わらせていました。

  • 環境 : Xcode15.4, Swift5.10
  • enableExperimentalFeatureフラグを用いて、Feature target以外へのStrictConcurrency対応はほとんど完了
  • enableUpcomingFeatureフラグを用いて、ほとんど全てのTargetに対して、StrictConcurrency以外のSwift6の機能の対応完了

2024/07 - 通常通りの開発

  • 環境 : Xcode15.4, Swift5.10
  • Xcode16.0 betaで不具合が明らかに多いのを観測していたため、基本的には静観
  • 通常の機能開発や他の技術的改善に着手

2024/08 - QA可能な状態へ

  • 環境 :
    • リリース : Xcode15.4, Swift5.10
    • iOS18対応 : Xcode16.1 beta・16.6 beta, Swift6

iOS18の正式リリースは、9月10日のApple Event直後だろうと予想できていました。そこから逆算したQAの具体的なスケジュールを、QAチームと08/15週に擦り合わせました。Xcode16.0 beta5までは不具合も多かったのですが、その後のリリースでSwiftの不具合が落ち着きそうだという見立ても、このスケジュールに反映されています。

8月中
開発完了 → エンジニアメンバーでデバッグして不具合があれば修正

9月1週目
beta版のアプリで、簡易的なQAを実施

9月2週目
Appleの動向次第。Xcode RCがReleaseされ次第、それで配布したアプリで手厚めのQA実施。


※ XcodeのRelease dateは日本時間ではないがRelease noteの日付をそのまま記載

元々、enableExperimentalFeature・enableUpcomingFeatureフラグを利用してSwift6の機能を有効化してアプリリリースを行っていたため、Swift6モードの有効化で強いられた変更は”ほとんど”ありませんでした。

また、それによる不具合も見つかることなく、StrictConcurrency未対応のTargetをいくつか対応し、QA用のアプリを配布しました。

2024/09 - QA実施 & リリース

  • 環境 :
    • iOS18未対応リリース : Xcode15.4, Swift5.10
    • iOS18対応済み初回リリース : Xcode16.0 RC, Swift6.0
    • Xcode16.0登場以降のリリース : Xcode16.0, Swift6.0

予定していたスケジュール通り、1週目はQAチームに簡易的なテストを実施してもらいました。QAチームの工数を過剰に消費することなく、Swift6・新OS対応起因の不具合・OS依存のUI崩れを見つけるには、通常リリース時に実施しているリグレッションテストを「iOS18・iOS16 or 17」に対して、いくつかの端末種類ごとに実施すると十分だと判断しました。

QAを2回に分けて実施してもらった理由は以下の通りです。

  • 正式リリース直前に、短い期間の1回のQA実施にかけて、修正が難し目の不具合を見つけた場合かなりリスクとなる。
  • なるべく早く不具合を見つけて、余裕を持ってリリースしたい。

リリース用QAでは、iOS16・17・18 x 複数端末種類の網羅的な組み合わせでQAを実施してもらいました。

進め方を振り返って

機能開発と並行して実施できるような余裕を持ったスケジュールで進めることができ、リリース直前もサプライズが発生することがなかったため、事前に建てていた計画は良いものだったと感じています。こうした結果に繋がった要因は以下のように考えています。

  • 段階的なSwift6移行に対する手段の活用
    • Xcode15.4を使いながらも、Flagを活用してSwift6の機能を有効化した状態でリリースできていたため、iOS18対応における不具合がゼロ 🎉(ゼロだったのは運要素もあるが、事前準備による恩恵は間違いなく受けている)
    • iOS18対応リリース後約1週間、Feature targetもSwift6の有効化完了
  • 予想されるiOS18リリース時期からの、逆算したスケジューリング
    • Appleの動向に見立てを立てつつ、QAチームと相談しながら余裕を持った計画を行うことで、サプライズ無くリリースまで完了
  • Xcodeの各beta versionが、どれくらいRCに近いものなのかを見極める審美眼
    • SwiftのリポジトリやForum・X等で情報を集め、どれくらいの時期に本格着手すべきか、直面している不具合に対する温度感を見極める
    • 逆に、Xcode16 betaの1〜5・16.1 betaあたりまでは、本来エラーではない場所が偽陽性として検出されてしまったり、その逆の事象が発生しているケースが多発していた印象でした。しかし、これらの事象が運よく解消されるのを待った結果、解消されないままRCのリリースがされてしまっては、カスタマーに不便を負わせることとなります。そのため、発生している事象がいつ解消される見込みなのか・それを待たずとも自力で解決できる方法がないのかを、SwiftのRepositoryForum をWatchすることで能動的にキャッチアップ・解決することが必要だと考えています。

ここまで読んだ限りでは、Swift6対応自体も順調だったように見えるかもしれませんが、完全にそうだったわけではありません。既に多くの情報が出ているので多くは書きませんが、対応方針で検討したことを次のセクションでいくつか紹介します。

StrictConcurrency対応

Actorの利用箇所を見極める

Mutableな状態をdata raceを起こさず読み書きするための手段として、Actorはとても便利です。

Actorは、そのContextに隔離されたものに外からアクセスする場合、非同期で扱う必要があります。そのため、Actorの利用は、呼び出し側をも非同期になることによる複雑性の高まりとのトレードオフ関係にあります。このデメリットを受け入れられない場合は次の選択肢を検討すると良いでしょう。

  • OSAllocatedUnfairLock
  • Mutex
  • Atomic
  • Lock
  • etc…

また、呼び出し側の元々の処理の流れが同期実行を想定していた場合、順番が担保されるかは注意が必要です。TCAの利用有無に限らない話ではありますが、私たちが利用しているTCAを例に話を進めます。例えば、元々は Effect.concatenate(_:) で同期的な処理を行うEffectをいくつか順番に実行していたところの一部Effectを非同期実行(Effect.run(priority:operation:catch:file:fileID:line:) )にした場合、operationに渡された処理の完了を待たずに次のEffectが実行されてしまいます。これへの対策としては、以下の手段を検討すると良いでしょう。

  1. Effectをまとめる or Taskを作成し、その中で順番を保証する(Actor内の処理内容次第では解決できない)
  2. NotificationCenterの通知を用いる
  3. Actor以外の方法を用いることで、見かけ上は同期処理として扱う

非同期処理の再入可能性の考慮

StrictConcurrencyに限定した話ではありませんが、非同期処理処理の途中に実行条件がある場合は、その条件チェックの前でSuspendして再入する可能性を考慮する必要があります。詳しくは "Interleaving" execution with reentrant actors を参照してください。

MainActorかどうかが、Xcode16で変わるかを調べてから進める

利用するXcode versionにより、MainActorが付与されている型や範囲が変わることがあります。

例 :

  • Xcode15では、MainActorの対象はSwiftUI.View.body → Xcode 16では、SwiftUI.View全体が対象
  • Xcode 16から、ASAuthorizationControllerDelegate Protocol が @MainActor

これらの変化により、実装側の対応が変わり得る箇所はView周りだと少なくないでしょう。こうした変化をあらかじめ認識した上で、いくつかの箇所に対して、Xcode15では意図的に対応を遅らせていました。

SwiftGenのConcurrency対応

ImageResource・ColorResourceはinternalで定義されます。そこで、NEWTではImage・Color・Stringを型として扱いながらも、依存元のTargetからの参照を容易に行うためにSwiftGenを利用しています。

この型はstencilを元に生成されているわけですが、2024/09前半時点でSendableのサポートがされていませんでした。そのため、こちらのIssue で紹介されているstencilを自動生成時のtemplateとして用いることで、自動生成された型がSendableに準拠していないことで発生するエラーを回避しました。

その他

Dark・Tintedカラーのアプリアイコンに対応

Xcode16から、DarkとTintedカラーのアプリアイコンを設定できるようになります。iOS18のbetaで他のアプリのアイコンも含めて見てみたところ、今まで通りの設定でもそれっぽく見えるアプリがほとんどでした。

しかし、Appleのドキュメントにもある通り、Tintedでの見せ方の推奨デザインがあったりと、意図した見せ方にするためにも、対応する方が好ましいでしょう。

// AppIcon.appiconset/Contents.json

{
  "images" : [
    {
      "filename" : "Light.png",
      "idiom" : "universal",
      "platform" : "ios",
      "size" : "1024x1024"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "filename" : "Dark.png",
      "idiom" : "universal",
      "platform" : "ios",
      "size" : "1024x1024"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "tinted"
        }
      ],
      "filename" : "Tinted.png",
      "idiom" : "universal",
      "platform" : "ios",
      "size" : "1024x1024"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

最後に宣伝

いかがでしたか?iOS18対応を例に、事業成長・品質・最新技術への追従のバランスに関して、NEWTを開発するエンジニアがどのように考えているかが垣間見えたかと思います。

旅行サービスを展開する私たちだからこそ重要視するポイントがあり、その中でどう試行錯誤しながらメンテナビリティ・スケーラビリティを高めていくかを考えられる特有の面白さがあります。

NEWTにはまだまだ成長の余地が多分にあります。将来複雑になり得るプロダクト要件に対して、どんな技術をどう使えばカスタマーにより良い旅行体験を提供できるかを、一緒に考えていきたい仲間を募集しています。

私のように、旅好きなメンバーが多い会社ではありますが、もちろんそうでない方も大歓迎です!

スタートアップの事業グロースにコミットしたい、社会やカスタマーに価値提供できるプロダクトに携わりたい、など令和トラベルにジョインする理由はさまざまです。

旅行が好きかどうかに関わらず、弊社のミッションやビジョン、テクノロジーで実現しようとしている世界観に少しでも興味を持っていただけたら、ぜひ一度お話させてください!

令和トラベルでは一緒に働く仲間を募集しています

絶賛、会社としてのフェーズ・人数ともに急激に変化しています。

そんな中、多様なバックグラウンドを持つメンバーが集まってプロダクトを進化させようとしているため、正直カオスに見える瞬間が垣間見える場面も少なくありません。しかし、そういう状況下で終わりなきミッションの達成を追い続ける過程では、何にも変えられない経験や成長が得られると思います。

皆さんも、NEWTを操縦する一員として、共に世界を旅しませんか? 🌍

この記事を読んで令和トラベル・NEWTに少しでも興味をお持ちいただけましたら、ご連絡お待ちしています!

https://www.reiwatravel.co.jp/recruit

『【特別編】NEWT Tech Talk』のお知らせ

令和トラベルでは、定期的に技術や組織に関する情報発信を開催しています。2024年9月にプレスリリースした資金調達を記念して、10月はNEWT Tech Talk特別バージョンで開催します!

代表取締役CEO篠塚がNEWT Tech Talkに初参戦🎊 2度目の資金調達を終え、旅行業界において令和トラベルが成し遂げようとしている変革やミッション、テクノロジーに投資していく背景や目指しているビジョンについてお話します。

https://reiwatravel.connpass.com/event/329609/

さらに、定期的に令和トラベルの技術や組織に関する情報発信を開催予定です。connpass にてメンバー登録して最新情報をお見逃しなく!

それでは次回のブログもお楽しみに!Have a nice trip!!

令和トラベル Tech Blog

Discussion