Upgrade to Pro — share decks privately, control downloads, hide ads and more …

「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。| iOSDC 2021

nkmrh
September 18, 2021

「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。| iOSDC 2021

nkmrh

September 18, 2021
Tweet

Other Decks in Programming

Transcript

  1. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    「スタディサプリ」がFull SwiftUIを選択した
    先に見えてきたもの。
    Hajime NAKAMURA @_nkmrh
    iOSDC Japan 2021

    View Slide

  2. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    @_nkmrh / 中村 肇
    ➔ iOS アプリエンジニア
    ➔ 2020年4月にQuipperに入社
    ◆ 新規プロダクト開発 iOS担当
    ◆ 小中高大学受験向けスタディサプリの一部運用を担当

    View Slide

  3. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    「Full SwiftUI」
    @main
    struct full_swiftuiApp: App {
    var body: some Scene {
    WindowGroup {
    ContentView()
    }
    }
    }

    View Slide

  4. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    Full SwiftUI を選択した背景
    02

    View Slide

  5. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    新規開発アプリの要件
    講座一覧 授業動画 問題演習

    View Slide

  6. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    新規開発アプリの要件
    iOS 14 以降 プッシュ通知
    App内課金
    オンボーディング

    View Slide

  7. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    技術検証
    ➔ 想定するデザインのプロトタイプをSwiftUIで実装
    ➔ Full SwiftUIでのアーキテクチャパターンの検証
    ➔ UIKitを組み合わせる仕組みの検証

    View Slide

  8. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    👀 見えてきたメリット
    👍
    02

    View Slide

  9. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    アニメーション

    View Slide

  10. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    要件
    ➔ ボタンを押してリストに要素を追加/削除し、任意のアニメーションを適用
    ◆ 追加時:左から右へスライドイン
    ◆ 削除時:拡大・縮小

    View Slide

  11. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    追加 削除

    View Slide

  12. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    Button("+1") {
    withAnimation {
    appendItem()
    }
    }
    Button("-1") {
    withAnimation {
    removeItem()
    }
    }
    ボタンの実装
    withAnimation(_:_:)メソッドのbody 引 数
    の中で配列を操作することでアニメーション
    を適用します

    View Slide

  13. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    List {
    ForEach(items, id: \.self) { item in
    Row(item: item)
    .transition(
    AnyTransition.asymmetric(
    insertion: AnyTransition.slide.combined(with: AnyTransition.opacity),
    removal: .identity
    )
    )
    }
    }
    追加時アニメーション
    AnyTransition.asymmetric(insertion:removal:)を使う
    と、Viewの挿入時と削除時で異なる Transitionを適用で
    きる

    View Slide

  14. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    struct ScaleTransitionEffect: GeometryEffect {
    var animationFactor: CGFloat
    var animatableData: CGFloat {
    get { animationFactor }
    set { animationFactor = newValue }
    }
    func effectValue(size: CGSize) -> ProjectionTransform {
    // 次スライドで実装
    }
    }
    削除時アニメーション
    animatableDataプロパティで 値 の 変
    化に基づいたアニメーションを提 供す

    View Slide

  15. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    func effectValue(size: CGSize) -> ProjectionTransform {
    let anchorPoint = CGPoint(x: size.width / 2, y: size.height / 2)
    // 2次関数を利用 `f(x) = ax^2 + bx + c`
    let scale = quadraticFunction(x: animationFactor, a: -6.8, b: 5.8, c: 1)
    return ProjectionTransform(
    CGAffineTransform.identity
    .translatedBy(x: anchorPoint.x, y: anchorPoint.y)
    .scaledBy(x: scale, y: scale)
    .translatedBy(x: -anchorPoint.x, y: -anchorPoint.y)
    )
    }
    削除時アニメーション

    View Slide

  16. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    List {
    ForEach(items, id: \.self) { item in
    Row(item: item)
    .transition(
    AnyTransition.asymmetric(
    insertion: AnyTransition.slide.combined(with: AnyTransition.opacity),
    removal: AnyTransition.modifier(
    active: ScaleTransitionEffect(animationFactor: 1),
    identity: ScaleTransitionEffect(animationFactor: 0)
    )
    )
    )
    }
    }
    削除時アニメーション

    View Slide

  17. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    アニメーションまとめ
    ➔ withAnimation(_:_:)で変数の変化に合わせてア
    ニメーションを簡単適用
    ➔ .transitionモディファイアでViewの追加/削除時の
    トランジションを適用
    ➔ GeometryEffectプロトコルを実装してViewにア
    フィン変換を適用

    View Slide

  18. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    👀 見えてきたデメリット
    😇
    03

    View Slide

  19. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    ライブラリの対応
    ライフサイクル系メソッドの不安定な動作
    ➔ Firebase の自動スクリーントラッキングが対応していない
    ◆ (firebase-ios-sdk v8.2.0)
    ➔ onAppear()が予期しないタイミングで呼ばれる
    ◆ TabViewで非表示タブのView.onAppearが呼ばれたりする

    View Slide

  20. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    class ViewModel: ObservableObject {
    @Published var isPresented = false {
    didSet {
    if isPresented {
    logger?.screenEvent("second_screen")
    } else {
    logger?.screenEvent("first_screen")
    }
    }
    }
    ...
    }
    struct FirstScreen: View {
    @StateObject var viewModel = ViewModel()
    var body: some View {
    NavigationLink(
    destination: SecondScreen(),
    isActive: $viewModel.isPresented,
    label: { EmptyView() }
    )
    ...
    }
    }
    スクリーントラッキングログ実装例

    View Slide

  21. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    ➔ Firebaseの自動スクリーントラッキングログはSwiftUI未対応
    ◆ (firebase-ios-sdk v8.2.0)
    ➔ Viewのライフサイクル系メソッドの挙動が不安定
    ◆ .onAppear()
    見えてきたデメリットまとめ

    View Slide

  22. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    👀 見えてきたtips
    04

    View Slide

  23. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    プログラムで画面遷移を制御する

    View Slide

  24. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    APIと通信して条件に適した場合のみ、
    一覧画面から詳細画面へプッシュ遷移したい
    要件

    View Slide

  25. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    基本のPush遷移実装
    *https://developer.apple.com/tutorials/app-dev-training/creating-a-navigation-hierarchy
    private let items = ["Apple", "Banana", "Cat", "Dog"]
    var body: some View {
    List {
    ForEach(items, id: \.self) { item in
    NavigationLink(
    destination: Text(item)
    ) {
    Text(item)
    }
    }
    }
    NavigationLinkと遷移先のView
    を設定するだけで完成

    View Slide

  26. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    プログラムで画面遷移を制御したい
    ➔ チュートリアルのコードでは、ユーザーのタップ入力を受ける前提なので、プ
    ログラムでの遷移には使えない
    ➔ 通信が完了したら遷移する処理を実装するには、NavigationLinkの
    isActive引数を取るInitializer*を使う
    *https://developer.apple.com/documentation/swiftui/navigationlink/init(_:isactive:destination:)-6xw7h

    View Slide

  27. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    @State private var isSelected = false
    var body: some View {
    ZStack {
    Button(action: {
    // 通信を行う模擬実装
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    isSelected = true
    }
    }) {
    Text("Submit")
    }
    プログラムでの画面遷移方法
    isActiveフラグのBinding先を定義
    非同期処理が終わるとフラグをtrueに変更

    View Slide

  28. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    @State private var isSelected = false
    var body: some View {
    ZStack {
    NavigationLink(
    destination: Text(item),
    isActive: $isSelected) {
    EmptyView()
    }
    Button(action: {

    }
    プログラムでの画面遷移方法
    NavigationLinkを 設 置 して、isActiveフラグを
    isSelectedのStateにBindingさせる

    View Slide

  29. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    struct Selection {
    var isSelected: Bool
    var item: T? {
    didSet {
    isSelected = item != nil
    }
    }

    }
    一覧画面から詳細画面への遷移
    Selection
    ➔ 一覧画面で選択したアイテムを保持
    ➔ 画面遷移のトリガー
    isActiveフラグのBinding先として定義
    アイテムをセットしてフラグを更新

    View Slide

  30. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    Itemを選択すると遷移する隠しNavigationLinkを作成
    Selectionを使った画面遷移
    @State private var selection = Selection(item: nil)
    @ViewBuilder
    private var navigationLinkIfPossible: some View {
    if let selectedItem = selection.item {
    NavigationLink(
    destination: Text(selectedItem),
    isActive: $selection.isSelected) {
    EmptyView()
    }
    } else {
    EmptyView()
    ...
    フラグが必ずtrueになるので自動的に遷移する
    アイテムがあった場合のみ、NavigationLinkを生成する

    View Slide

  31. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    var body: some View {
    ZStack {
    navigationLinkIfPossible
    List {
    ForEach(items, id: \.self) { item in
    Button(action: {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    selection = .init(item: item)
    }
    }) {
    Text("\(item)")
    }
    ...
    アイテムをセットして遷移をトリガー
    隠しNavigationLinkを設置
    非同期処理完了後、アイテムをセット

    View Slide

  32. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    APIと通信して条件に適した場合のみ、
    一覧画面から詳細画面へプッシュ遷移したい
    ➔ 隠しNavigationLinkとSelectionを組み合わせて実装
    NavigationLinkをプログラムで制御するまとめ

    View Slide

  33. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    Full SwiftUI で開発した所感
    05

    View Slide

  34. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    Full SwiftUI で開発した所感
    ➔ UIの実装がスピードアップ
    ◆ レイアウトの記述がシンプル
    ◆ ライブプレビューで素早く動作確認が可能
    ➔ 実装の変更やレビューがしやすい
    ➔ 開発体験が加速!🚀
    ➔ ⚠ 今回紹介した事例の他、ハマりポイントや懸念はあるのでそこ
    は強いやっていき💪が必要

    View Slide

  35. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    https://quipper.hatenablog.com/archive/category/Engineering-Native-iOS
    ➔ 「スタディサプリ」が React Native から卒業するまで、ある
    いは技術的負債への感謝と敬意
    ➔ iOS アプリ開発とユニットテスト
    ➔ iOS/Androidチーム合同でユニットテストクロスレビューを
    行っている話
    ➔ SwiftUIのディープリンク対応:プッシュ通知から画面遷移
    する方法
    本日お伝えした詳細や他のトピックを投稿しています!

    View Slide

  36. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。

    View Slide

  37. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    選考・採用イベント ご応募方法
    リクルートは仲間を積極募集中です!!!
    このページをご覧の方限定の特設応募サイトよりご登録ください
    採用イベント
    カジュアル面談
    まずは気 軽にカジュアルに話を
    してみたい
    選考受験
    スピーディーに選考でこれまでの
    経験が活かせるか確認したい

    View Slide

  38. #iosdc 「スタディサプリ」がFull SwiftUIを選択した先に見えてきたもの。
    ご清聴ありがとうございました

    View Slide