見出し画像

SwiftUI導入時にiOS13.0だけクラッシュが起きた話

REALITYでは、SwiftUIを2021年の4月から一部導入を開始しました。
初のSwiftUIの画面の実装時にiOS13.0限定のクラッシュが発生しましたが、QAでリリース前に検出する事ができたので事なきをえる事ができました。
今回のnoteではSwiftUI導入までの経緯と、発生したクラッシュの原因と解決方法についての話です。

実は、REALITYはSwiftUIを2021年4月にプロダクトに導入していました!!!!
2021年の3月31日をもってiOS12のサポートを終了したため、SwiftUIが導入可能となりました。

こちらがREALITYで初めてSwiftUIが導入された画面です。
配信で獲得したLIVEポイントをガチャなどで利用できるコインへ交換できます。

プロジェクトの既存の画面はUIKit+RxSwiftで実装されていますが、
この画面のView, ViewModelは完全にSwiftUI+Combineで実装されています。

画像1

今回のnoteは、この画面にSwiftUIを導入することになった経緯と、この画面の実装時にiOS13.0のみクラッシュが発生した話です。

LIVEポイントコイン交換画面にSwiftUIを導入した経緯

SwiftUIは宣言的にUIを記述できたり、プレビューが利用できたりなど開発効率の向上が見込まれるため、ぜひREALITYにも導入したいと考えていました。

そこで、SwiftUIを導入する画面の条件として大まかに次の2つを設定しました。
1. Pull to refreshとpaginationがないこと
2. SwiftUIからUIKitに画面遷移がないこと
これらの条件を満たした、新しい画面実装の必要のある施策がLIVEポイントコイン交換でした。

それぞれの条件を設定した背景はこちらです。
1. Pull to refreshとpaginationがないこと
これは、Pull to refreshとpaginationそのものの問題ではなく、iOS13でSwiftUIのScrollViewのスクロール量が取得できないためです。
REALITYがiOS14以降のみサポートになることで作りやすくなる予定です。
2. SwiftUIからUIKitに画面遷移がないこと
既存部分は当然UIKitで記述されているので、SwiftUIで作成した画面はUIKitから呼び出されます。
UIKit→SwiftUI→UIKitのようにUIKitの間にSwiftUIが挟まれる場合、遷移の記述が複雑になりがちなため、画面遷移の端の方からSwiftUIを導入する戦略を取りました。

iOS13.0のみ確定クラッシュが発生した話

LIVEポイントコイン交換の画面をSwiftUIで作成した結果、iOS13.0のみクラッシュが発生しました。
QAで未然に発見し、修正したので本番に影響は出ていないです!!!!!
QAの担当の方にはいつも大変お世話になっております🙇‍♀️🙇‍♀️🙇‍♀️🙇‍♀️🙇‍♀️

iOS13.0とそれ以降のiOSバージョンでは、SwiftUIの挙動に差分があることが、事前の調査で分かっていたので、iOSのバリエーションを通常より多いiOS13.0, 13.4, 14.0, 14.4の4つでお願いしていました。
また、初のSwiftUIの画面ということもあり、これらの4つのiOSバージョンと端末サイズを組み合わせてケースをおこしてQAして頂きました。
その結果、iOS13.0のみのクラッシュを検出することが出来ました。

iOS13.0のみクラッシュが発生した原因

こちらが原因になった部分を簡易に記述したサンプルです。
UIViewRepresentableを利用して、SwiftUIの一部にUIKitを組み込もうとしています。

import SwiftUI

struct ContentView: View {
   var body: some View {
       LabelWrapperView(text: "Hello, REALITY!")
   }
}
struct LabelWrapperView: UIViewRepresentable {
   @State var text: String
   func makeUIView(context: Context) -> UILabel {
       let label = UILabel()
       return label
   }
   func updateUIView(_ uiView: UILabel, context: Context) {
       // Fatal error: Accessing State<String> outside View.body が発生する
       uiView.text = text
   }
}

iOS13.0のシミュレータで実行すると、uiView.text = text の部分でクラッシュします。
Fatal error: Accessing State<String> outside View.body というエラーです。
View.body以外でStateにアクセスすると発生するFatal errorのようです。
UIViewRepresentableはView protocolを継承しているので、@State 使えるでしょ〜くらいの感覚でこのような実装にしていました。

Fatal errorが発生する条件を確認したコードです。

class Hoge {
   @State var message: String = "Hello, REALITY!"
   func check() {
       print("message: \(message)")
   }
}

Viewなどとは一切関係ないHogeクラスで@Stateを呼び出してみました。

スクリーンショット 2021-09-12 2.44.12

iOS13.0でcheck関数を実行すると、当然アサーションが発生します。
しかし、iOS14.4で実行した所、アサーションは発生しませんでした
この結果から、iOSのバージョン更新で@Stateの挙動が変更されたと推測しました。

今回作成したLIVEポイントコイン交換の画面では、UIViewRepresentableの内部で画面を更新したくて@Stateを利用していたのですが、
画面の更新のロジックをUIViewRepresentableの外に出し、@Stateを止めることで解決しました。

import SwiftUI

struct ContentView: View {
   var body: some View {
       LabelWrapperView(text: "Hello, REALITY!")
   }
}
struct LabelWrapperView: UIViewRepresentable {
   let text: String
   func makeUIView(context: Context) -> UILabel {
       let label = UILabel()
       return label
   }
   func updateUIView(_ uiView: UILabel, context: Context) {
       uiView.text = text
   }
}

まとめ

REALITYにSwiftUIを導入した経緯と、iOS13.0でクラッシュが発生した話でした。
SwiftUIについて、社内のiOSの勉強会で調査、共有していたため、適切なテストケースを設計し、リリース前のQAでクラッシュを検出することで事なきをえることが出来ました。勉強会について詳しくはこちら

REALITYではSwiftUIやCombineなど一緒に新しい技術を学び挑戦してくれる仲間を募集中です!