blog

DeNAのエンジニアが考えていることや、担当しているサービスについて情報発信しています

2024.01.18 技術記事

プロダクトに初めてSwiftUIを採用して困ったこととその対応

by fuka takashima

#ios #swift #swiftui #skyleap

はじめに

こんにちは。SkyLeapというWebブラウザアプリを開発している23新卒の高嶋です。

SkyLeapで初めて一つの機能実装にSwiftUIを採用し、開発を行いました。 この記事では、SwiftUIにおけるiOS バージョン差分で困ったこととそれに対してSkyLeapが行った対応を紹介していきたいと思います。

今回実装した機能はタイマー機能です。 タイマー機能は設定した時間が経過したときに通知が送られる機能です。 設定したタイマーをリスト形式で表示する一覧画面と、タイマーのタイトルと時間を設定するタイマーの追加、編集画面があります。

▲一覧画面(左)と追加・編集画面(右)

タイマーの時間設定はStepperとPickerの2つの方法で行うことができます。Stepper表示時はPickerを非表示に、Picker表示時はStepperを非表示にするという仕様になっています。

2023年6月にiOS 13をサポート対象外としたこととタイマー機能がリスト形式の画面で構成されていることからSwiftUIの採用を決めました。

出会った問題とその対応

iOS 16以上の時、リストの背景色が反映されない

▲iOS 15のリスト画面(左)とiOS 16のリスト画面(右)

iOS 16以上の時にListの背景色がSkyLeap独自で指定した背景色ではなくデフォルトの grouped style の色が適用されてしまう問題がありました。

List {
    HStack {
        Image(.timer)
        Text("cell1")
    }
    HStack {
        Image(.timer)
        Text("cell2")
    }
    HStack {
        Image(.timer)
        Text("cell3")
    }
}
.listStyle(.grouped)

この問題の調査を進めたところ、iOS 16以上の時に指定した背景色が適用されるようにするためには、scrollContentBackground(.hidden)を指定する必要がありました。
また、今後リスト画面をSwiftUIで作った時に同じ問題が起きることを防ぐため以下のviewModifierを作成しました。

struct ScrollContentBackgroundHiddenModifier: ViewModifier {
    func body(content: Content) -> some View {
        if #available(iOS 16.0, *) {
            content.scrollContentBackground(.hidden)
        } else {
            content
        }
    }
}
extension View {
    func scrollContentBackgroundHidden() -> some View {
        modifier(ScrollContentBackgroundHiddenModifier())
    }
}

このviewModifierを各リスト形式の画面に使うことで、iOS 16以上で指定した背景色が適用されるようになりました。

Listの隣り合うRowの表示・非表示を同時に切り替えるとiOS 15以下のアニメーションに違和感がある

▲iOS 16のアニメーション(左)とiOS 15のアニメーション(右)

iOS 15以下の時、Listの隣り合うRowを同時に切り替える処理を行うと横に動くような違和感のあるアニメーションになってしまうことがわかりました。

List {
    Section {
        HStack {
            Image(.resetTime)
            Text("時間")
            Spacer()
            Button(
                action: {
                    withAnimation() {
                        isPickerActive.toggle()
                        isStepperActive.toggle()
                    }
                },
                label: { Text("Tap") }
            )
        }
        if isPickerActive {
            HStack {
                Picker(selection: $selected, label: Text("")) {
                    ForEach(0..<101, id: \.self) { index in
                        Text("\(index)").tag(index)
                    }
                }
            }
        }
        if isStepperActive {
            HStack {
                Spacer()
                Button(action: {}, label: {Text("Button")})
            }
        }
    } header: { Text("Section1") }
    Section {
        HStack { Text("cell") }
    } header: { Text("Section2")}
}
.listStyle(.grouped)
.scrollContentBackgroundHidden()

この問題はList形式の画面にセクションが複数あり、1つ目のセクションにヘッダーがついている時にのみ再現することがわかりました。

▲iOS 15の修正後のアニメーション

セクションのヘッダーを消すのはデザイン的に避けたかったため、それ以外の方法での解決策を探しました。 Tapボタンで行うaction内でListRowの切り替えが起こらないように、同時に行っていた切り替えを少し待ってから行うように変更することでこの問題を回避することができました。元々は1つのフラグで表示非表示を切り替えようと思っていましたが、この問題の回避のためにフラグを2つに分けました。

Buttonで行うactionを修正したコード

action: {
    withAnimation() {
        isPickerActive.toggle()
        if #available (iOS 16, *) {
            isStepperActive.toggle()
        }
    }
    if #unavailable (iOS 16) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
            isStepperActive.toggle()
        }
    }
}

▲iOS 14で起きた意図しない挙動(左)とiOS 14の修正後の挙動(右)

このコードにすることでiOS 15のアニメーションの違和感は解消されましたが、iOS 14の時にisStepperActive.toggle()での切り替えが1回目のタップだけうまくいかないという別の問題が起きてしまいました。
この問題を調査しましたが、アニメーションをなくす以外にこの問題を解決する方法がなかったため、iOS 14はアニメーションを行わないという対応になりました。

Buttonで行うactionを再度修正したコード

action: {
    if #available(iOS 16, *) {
        withAnimation() {
            isPickerActive.toggle()
            isStepperActive.toggle()
        }
    } else if #available(iOS 15, *) {
        withAnimation() {
            isPickerActive.toggle()
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
            isStepperActive.toggle()
        }
    } else {
        isPickerActive.toggle()
        isStepperActive.toggle()
    }
}

SwiftUIを使って機能を実装してみて

初めてSkyLeapでSwiftUIを採用した実装を行いました。リスト形式の画面はUIKitよりもSwiftUIで実装した方が簡単に実装できると思っていましたが、想像以上に苦労した部分が多かったです。今回紹介した2つ以外にも以下のようなバージョン差分での挙動の違いで困ったことが多くありました。

  • iOS 14で.sheetを呼び出すとsheet内にあるTextFieldのフォントが太字になる
  • iOS 14だけセクションヘッダー分リストの表示位置が下がってしまう
  • iOS 15以下でリストがEditModeのときに.sheetが開かない

SwiftUIは単純な画面を作りたい時にはとても便利で、今回もiOS 16以上をサポート対象にしていたら、楽に実装することができていたと思います。ですが、複数のバージョンをサポートする場合には多くの課題があると感じました。 また、OS差分を把握するためにプロトタイピングをするなどして、できることとできないことを事前に調査しておくことが重要だと感じました。

最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。

recruit

DeNAでは、失敗を恐れず常に挑戦し続けるエンジニアを募集しています。