LoginSignup
260
203

More than 3 years have passed since last update.

こんにちは、たなたつです :cat:

SwiftUIが発表されて半年ほど経ちましたね。あっという間に時間は過ぎていき、iOS 13以降じゃないと使えないし、まだ気にしなくていいでしょなんて言ってられなくなるのもあっという間な気がします。

iOS Advent Calendarの5日目ということで今回は、いくつかSwiftUIでサンプルアプリを作ったり、実際にアプリをリリースしたりした中でたまってきた知見を書こうと思います。

SwiftUIは様々なプラットフォームで動きますがiOSアプリに注目し、開発する前に知っておきたい実践的なポイントなどを共有します。

※ Xcode 11.2.1、iOS 13.2.1 での動作を元に記事を書いています。

SwiftUIの特徴

  • 少ないコードでUIを作れる (コードレイアウト)
  • 宣言的に記述できる
  • Appleのすべてのプラットフォームで動く
    • ただし、iOS 13、macOS 10.15、tvOS 13.0、watchOS 6.0以上

このような特徴がよく言われていますが、実際にアプリを作るうえで現状のSwiftUIはどうなのでしょうか。

実際のところSwiftUIってどう?

SwiftUIでiOSアプリをいくつか作ってみてこのように感じました。

  • 細かなUIの動きまでアプリの仕様に沿って実装する必要がある場合はかなり難しい
  • アプリの仕様をSwiftUIが得意としている仕様に柔軟に変更できる場合は採用しても良い

アプリの仕様通りに細かいUI/UXを実現するのは大変

SwiftUIのAPIはまだUIKitほど柔軟ではないため、UIKitでは実現できるUI/UXを再現できない場合があります。

SwiftUIはUIKitと組み合わせて利用することができるため、SwiftUIではできない部分をUIKitで代わりに実装するというアプローチもできます。
しかし、実際に試してみると組み合わせるために必要なボイラープレートコードが多く、負担になります。

また、SwiftUIの特徴的な機能の一つにStateをバインディングしてUIを自動的に更新するというものがありますが、UIKitと組み合わせたときにそれらの機能の活用が難しくなるケースがあります。
そしてそれを回避するためのワークアラウンド的なコードが必要となり、本質的な実装に集中できなくなりがちです。

SwiftUIの挙動に合わせてUIを変更可能ならあり

SwiftUIが不得意としている仕様を実装するのは、開発の大きなボトルネックになってしまうため、実験的なアプリや個人アプリのように、アプリの仕様を柔軟に変更できる場合は採用してみてもよいと思いました。 (もちろん対応OSバージョンが狭まることを許容できる場合です)

ユーザーファーストの視点とは全く逆になってしまいますが、開発者視点でUI変更できれば、SwiftUIの強みを活かして爆速でアプリを開発することができるかもしれません。

開発の進め方

ここからは実際にSwiftUIでアプリを作るときにおすすめな開発の進め方を紹介します。
現時点ではSwiftUIの情報が少なく、開発者の知識もUIKitほどはないと思いますので、その状況を想定しています。

前述したようにSwiftUIには不得意なUIがあるため、想定しているUIが実現しやすいものなのかどうかを作りながら判断し、難しい場合はアプリの仕様を調整するというサイクルを回していきます。

また、SwiftUIの優れた機能の一つにプレビュー機能があります。
プレビュー機能を使うことで早いサイクルでUIの実現性の確認とレイアウトの調整ができるため、積極的に活用したほうが良いです。

画面の漸進的な開発

まずは作りたいレイアウトになるように、Viewのbodyにべた書きしていくとレイアウトしやすいです。

struct ListCell: View {
    var body: some View {
        HStack {
            Button(action: {
                #warning("TODO")
            }, label: {
                Image("usericon")
                    .resizable()
                    .scaledToFit()
                    .clipShape(Circle())
                    .frame(width: 60, height: 60)
                    .padding(8)
            })
            VStack(alignment: .leading) {
                HStack {
                    Text("たなたつ")
                    Text("@tanakasan2525・10m")
                        .foregroundColor(.gray)
                }
                Text("ここは本文が表示されるテキスト領域です。改行することもできます。")
                    .lineLimit(nil)
                    .fixedSize(horizontal: false, vertical: true) // workaround
                HStack {
                    Button(action: {
                        #warning("TODO")
                    }, label: {
                        Image(systemName: "bubble.left")
                    })
                    Spacer()
                    Button(action: {
                        #warning("TODO")
                    }, label: {
                        Image(systemName: "arrow.2.squarepath")
                    })
                    Spacer()
                    Button(action: {
                        #warning("TODO")
                    }, label: {
                        Image(systemName: "heart")
                    })
                    Spacer()
                    Button(action: {
                        #warning("TODO")
                    }, label: {
                        Image(systemName: "square.and.arrow.up")
                    })
                }
                .foregroundColor(.gray)
                .padding(8)
            }
        }
        .padding(8)
    }
}

struct ListCell_Previews: PreviewProvider {
    static var previews: some View {
        ListCell()
            .previewLayout(.sizeThatFits)
    }
}

レイアウトがある程度出来たら、bodyの可読性を上げるためにメソッドに切り出します。

この時にXcodeのリファクタリング機能を使うこともできます。

var body: some View {
    HStack {
        userIconView()
        VStack(alignment: .leading) {
            userNameView()
            messageView()
            bottomButtonView()
                .padding(8)
        }
    }
    .padding(8)
}

どこまでメソッド化するか悩ましいですが、bodyを見ればざっくりのレイアウトがわかるくらいまで切り出すのが良いと思います。

また、他の画面でも使いそうなViewのレイアウトはカスタムViewとして切り出していきましょう。
上記の場合、画像のボタンはImageButtonというカスタムクラスを作っても良さそうです。

画面のレイアウトのポイント

SwiftUIのエラーは読みにくいので細かく分ける

現状ではSwiftUIのエラーは非常に読みにくく、Xcodeの気持ちを読み取るエスパー力が必要な状態です。

例えばこの実装、どこが悪いでしょうか?

正解は

var body: some View {
    VStack {
        Text("エラーわかりにくい")
            .frame(width: 300, height: 60)
        TextField("名前", text: self.$viewModel.name)
    }
}

TextFieldの第二引数で渡しているtextが期待している型はBinding<String>です。エラーの実装は間違えてPublishedのnameに$をつけてしまっています。

ですが、Xcodeが提示しているエラーはなぜかframeのところになっています。
上記は短い実装なのでパッと見てわかるかもしれませんがbodyがかなり長い行数になっていた場合、見つけるのは非常に困難です。

そのためにもできるだけメソッドやカスタムViewに切り出してbody部分を短いコードに留めるようにしておきたいです。

プレビューしやすいView

Viewを作っていく際に、SwiftUIのプレビュー機能を使うとリアルタイムで表示を確認できるだけでなく、自然と依存関係の少ない (正しい) Viewができていくように思いました。
プレビューするためにはダミーの値を用意してViewに渡す必要があるため、例えば、後述する EnvironmentObject の良くない使い方をしていると「あれ、プレビューするためにいろいろデータを用意しないといけないぞ、面倒だ」ということに気づき、Viewの粒度やStateの設計などを早いサイクルで改めることができます。

なので、積極的にプレビューを利用しながら、プレビューしやすいViewになっているかを常に意識して開発をすると綺麗なViewを保っていけると思いました。

画面遷移の実装

次に画面遷移の実装をします。

画面遷移のよくあるパターンとしては3通りです。

  1. present (モーダル遷移)
  2. push (プッシュ遷移)
  3. 今の画面を新しい画面に置き換える

個人的に感じたSwiftUIでの実装の難易度は簡単順に 3 > 1 >>> 2 です。UIKitでは 1 > 2 > 3 だと思っています。

モーダル遷移

モーダル遷移は sheet を使います。

@State private var isPresented = false

var body: some View {
    Button("Present") {
        self.isPresented = true
    }
    .sheet(isPresented: $isPresented) {
        NextView()
    }
}

シンプルで柔軟性があり、実装が容易です。

ただし、iOS 13から pageSheet スタイルがデフォルトになったため、SwiftUIでもそのスタイルになります。

画面を全部覆うfullScreen スタイルをSwiftUIで実現するにはUIKitのpresentを使うか、モーダル遷移アニメーションを自作し、ZStackなどを使って似た表示を再現する必要があります。

https://stackoverflow.com/questions/56756318/swiftui-presentationbutton-with-modal-that-is-full-screen
https://stackoverflow.com/questions/56709479/how-to-modally-push-next-screen-to-be-full-in-swiftui

プッシュ遷移

プッシュ遷移は NavigationLinkNavigationView を使います。

var body: some View {
    NavigationView {
        NavigationLink("Push", destination: NextView())
            .navigationBarTitle("Title")
    }
}

リンクボタンをタップしてプッシュ遷移する場合は、このようにシンプルになりますが、例えば、通信成功後にプッシュ遷移する場合はこのようになります。

@State private var isPushed = false

var body: some View {
    NavigationView {
        VStack {
            Button("Fetch data") {
                // 通信の代わりに遅延させる
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    // データの取得後Push
                    self.isPushed = true
                }
            }
            // 見えないリンクを置いて遷移先を設定する
            NavigationLink(destination: NextView(), isActive: $isPushed, label: EmptyView.init)
        }
        .navigationBarTitle("Title")
    }
}

ちょっと違和感のある実装になってしまいますね。調べた限り、今のAPIではこのようになります。

そして、非同期で取得したデータを次の画面に渡す方法はどのようになるでしょうか。
何通りもやり方はありますが、素直に実装する場合はこのようになると思います。

@State private var isPushed = false
@State private var fetchedData: String?

var body: some View {
    NavigationView {
        VStack {
            Button("Fetch data") {
                // 通信の代わりに遅延させる
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    // データの取得後Push
                    self.isPushed = true
                    // 取得したデータの設定(new dataというデータを取得できたとする)
                    self.fetchedData = "new data"
                }
            }
            if fetchedData != nil { // ViewBuilder内ではif letは使えない
                NavigationLink(destination: NextView(data: fetchedData!), isActive: $isPushed, label: EmptyView.init)
            }
            // if letの代わりにmapを使うこともできる(こっちの方が綺麗)
//            fetchedData.map {
//                NavigationLink(destination: NextView(data: $0), isActive: $isPushed, label: EmptyView.init)
//            }
        }
        .navigationBarTitle("Title")
    }
}

ちょっとずつらみが出てきましたね。画面遷移とデータ渡しがセットになっているケースを単純に実装するとプロパティの数がどんどん増えてしまいます。

そのため、ObservableObjectやカスタムView、structでデータをきれいにまとめるなどの工夫によって、Viewを清潔に保つように頑張る必要があります。

この辺りは次のState設計の部分でいくつかパターンを紹介します。

プッシュ遷移周りはUIKitよりも明らかに面倒です。ナビゲーションバーやエッジスワイプ周りでも厄介な部分があるので、後で軽く紹介します。

今の画面を新しい画面に置き換える

今の画面をまるっと新しい画面に置き換える実装はかなり簡単です。

@State private var isBlueView = false
var body: some View {
    VStack {
        if isBlueView {
            BlueView()
        } else {
            RedView()
        }
        Button(isBlueView ? "Red" : "Blue") {
            self.isBlueView.toggle()
        }
    }
}

bodyの中でif文を使うことができるので、そこで表示するViewを出し分けるだけで簡単に画面切替が可能です。

画面遷移実装のポイント

基本的にはUX優先で遷移方法を選択して良いと思います。ですが現在のSwiftUIのバグなどによっては問題を回避するワークアラウンドを考えるよりも遷移方法を見直すほうが良い場合も多いため、柔軟に仕様を変えられるようにしておきたいところです。

執筆時現在に起きているいくつかの問題/複雑なポイントを紹介します。

NavigationBarItemに置いたNavigationLinkでPushした後、Popするとクラッシュする

ナビゲーションバーにボタンを置いてPush遷移する動作はよくあるものですが、iOS 13.2 ではNavigationBarItemに置いたNavigationLinkでPushした後、Popするとクラッシュしてしまいます。

回避方法としてはNavigationLinkではなく、普通のButtonを置くようにし、前述したようなEmptyViewを持つNavigationLinkを用いて画面遷移するようにするとうまくいきます。

navigationBarHiddenは親の設定が優先される

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink("Push", destination: NextView())
                .navigationBarHidden(true)
                .navigationBarTitle("")
        }
    }
}

private struct NextView: View {
    var body: some View {
        Color.blue
            .navigationBarHidden(false)
            .navigationBarTitle("Next View")
    }
}

このようなコードなら、プッシュ後にナビゲーションバーが表示されるようになりそうですが、実際はこのようになります。

動作を観察すると、ViewGraphの親の設定が優先されるようでした。

この現象を回避するためにはこのように親側の状態を更新する必要があります。

struct ContentView: View {
    @State private var isPushed = false
    var body: some View {
        NavigationView {
            NavigationLink(destination: NextView(), isActive: $isPushed, label: { Text("Push") })
                .navigationBarHidden(!isPushed)
                .navigationBarTitle("")
        }
    }
}

private struct NextView: View {
    var body: some View {
        Color.blue
            .navigationBarTitle("Next View")
    }
}

sheetをメソッドチェインor入れ子にすると最後(親)のsheetしか動かなくなる

複数のモーダルを表示したいときに以下のように書きたくなりますが、Modal 1が動かなくなります。

struct SheetChain: View {
    @State private var isModal1Presented = false
    @State private var isModal2Presented = false

    var body: some View {
        VStack {
            Button("Modal 1") {
                self.isModal1Presented = true
            }
            Button("Modal 2") {
                self.isModal2Presented = true
            }
        }
        .sheet(isPresented: $isModal1Presented, content: { NextView(color: .red) })
        .sheet(isPresented: $isModal2Presented, content: { NextView(color: .blue) })
    }
}

private struct NextView: View {
    let color: Color
    var body: some View {
        color
    }
}

sheetがチェインしたり入れ子になっていると、最後(親)のsheetしか動かなくなるようです。

これを回避するためにはsheetがチェインしないように各ボタンにsheetを付けるようにします。

struct SheetChain: View {
    @State private var isModal1Presented = false
    @State private var isModal2Presented = false

    var body: some View {
        VStack {
            Button("Modal 1") {
                self.isModal1Presented = true
            }
            .sheet(isPresented: $isModal1Presented, content: { NextView(color: .red) })
            Button("Modal 2") {
                self.isModal2Presented = true
            }
            .sheet(isPresented: $isModal2Presented, content: { NextView(color: .blue) })
        }
    }
}

シンプルな画面であればあまり問題になりませんが、複雑な画面でViewを細かくコンポーネント化し、入れ子にしていくと発生しやすいのでsheetはできるだけ子のViewにつけるようにしたほうが良いです。

ちなみに入れ子で動かなくなるというのはこのような例です。

var body: some View {
    VStack {
        Button("Modal 1") {
            self.isModal1Presented = true
        }
        Button("Modal 2") {
            self.isModal2Presented = true
        }
        .sheet(isPresented: $isModal2Presented, content: { NextView(color: .blue) })
    }.sheet(isPresented: $isModal1Presented, content: { NextView(color: .red) })
}

この場合は、Modal 2が動かなくなります。最後(親)のsheetしか動かなくなるためです。

State設計

SwiftUIの便利な機能の一つであるバインディングに必要なStateはアプリを作り進めていくとだんだんと増えていき、Viewの可読性が落ちていきがちです。

そこで、Stateをできるだけきれいに保つためにいくつかの便利なテクニックを紹介します。

関連性のあるStateはstructにまとめる

いろいろなサンプルコードで @State@Published をプリミティブ型に対して利用していることが多いですが、structでも利用可能です。

前述したAPIからデータを取得した後にPush遷移する時のStateはこのように書くこともできます。

struct NavigationStateWithData<T> {
    var isActive: Bool = false {
        didSet {
            if !isActive, data != nil {
                data = nil
            }
        }
    }
    var data: T? {
        didSet {
            // 無限ループしないように代入前のチェックが必要
            if (data == nil) == isActive {
                isActive.toggle()
            }
        }
    }
}

struct ContentView: View {
    @State private var fetchedData = NavigationStateWithData<String>()
    var body: some View {
        NavigationView {
            VStack {
                Button("Fetch data") {
                    // 通信の代わりに遅延させる
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                        self.fetchedData.data = "new data"
                    }
                }

                fetchedData.data.map {
                    NavigationLink(destination: NextView(data: $0), isActive: $fetchedData.isActive, label: EmptyView.init)
                }
            }
            .navigationBarTitle("Title")
        }
    }
}

まとまるとStateの関連性がわかりやすくなってよいですね。
上記の場合はデータがあるときに自動でPush遷移をするカスタムNavigationLinkを作るのもありかもしれません。

ObeservableObject

@Stateとstructの組み合わせでは表現しにくいViewの全体の状態を管理する時はObservableObjectを使います。
ObservableObjectはクラスにしか適合できないプロトコルのため、structでは値がコピーされてしまうようなViewを跨ぐ状態管理や、値更新時にViewを更新する必要がないプロパティなどを保持したりするのに便利です。

また、ObservableObjectで使用する@Published$でアクセスすることで値を監視できるPublisherとして扱えるので、値の変化に伴って処理を挟むことができます。

struct SettingView: View {
    @ObservedObject private var viewModel = SettingViewModel()
    var body: some View {
        Picker("テーマ", selection: $viewModel.theme) {
            Text("ライトモード").tag(UIUserInterfaceStyle.light)
            Text("端末の設定に従う").tag(UIUserInterfaceStyle.unspecified)
            Text("ダークモード").tag(UIUserInterfaceStyle.dark)
        }.pickerStyle(SegmentedPickerStyle())
    }
}

class SettingViewModel: ObservableObject {
    @Published var theme = UIUserInterfaceStyle.unspecified

    private var cancellables: Set<AnyCancellable> = []

    init() {
        $theme.sink { [weak self] theme in
            // 外観モードを切り替える
            let keyWindow =  UIApplication.shared.windows.first { $0.isKeyWindow }
            keyWindow?.overrideUserInterfaceStyle = theme
        }.store(in: &cancellables)
    }
}

ロジックをObservableObjectに詰め込むと状態の更新タイミングが複雑になりがちです。できるだけUIに関係のある処理だけをここに記述するようにして、複雑なロジックは別の型に記述したほうが良いと思いました。

EnvironmentObject

EnvironmentObjectは子View全てにオブジェクトを伝搬させることができる機能で、これを使うとViewを細かくコンポーネント化していったときに毎回initでオブジェクトを渡す必要がなくなるためとても便利です。

ただ、なんでもEnvironmentObjectで渡してしまうと、データがどこで更新されたのか分かりにくくなったり、Viewの使いまわしにくくなったりします。

実際に使ってみて思ったGood/Badパターンはこちらです。

EnvrionmentObjectのGoodパターン

  • アプリ全体で使う表示に関係する状態を管理する
    • ログイン状態など
  • 他の画面で使いまわさない子Viewにオブジェクトを渡す
    • Fluxで実装した場合のStoreを子Viewに渡すときなど

EnvrionmentObjectのBadパターン

  • 表示に全く関係しない状態を管理する
    • それはシングルトンなオブジェクトで管理するほうが適しているかもしれません
  • 子Viewに必要のないデータを含むオブジェクトをEnvironmentObjectで渡す
    • そのデータの監視方法は@State@ObservedObjectに置き換えられるかもしれません
  • 他の画面でも使い回されるViewが特定のViewに依存したEnvironmentObjectを参照している
    • そのデータはinitで渡すようにしたほうが良いかもしれません

State実装時のTips

Single Source of Truth

Appleは「Single Source of Truth」を推奨しています。これはデータソースは1つにしましょうという意味です。

SwiftUIの実装的には同じ意味を持つデータを別々の@State@Publishedで保持しないようにするということになります。値を保持せず参照だけしたい時は@Bindingを使いましょう。

Bindingを使ったチュートリアル
https://developer.apple.com/tutorials/swiftui/handling-user-input

また、あるStateに変更があった時に別のStateを変更したいというケースでは、Combineフレームワークを使って値を監視すると、同一ソースを複数のStateで保持する (Single Source of Truthに反する) 必要があるときにも多少安全です。

// ユーザーの入力によってリストの表示をフィルターする処理
class SearchViewModel: ObservableObject {
    @Published var keyword = ""
    @Published private(set) var items: [Item] = []
    @Published private(set) var filteredItems: [Item] = []
    // ↑computed propertyにすることも可能ですが、結果をキャッシュしたいという意図です
    // ...

    private var cancellables: Set<AnyCancellable> = []

    init() {
        // 入力キーワードがnameに含まれているものをfilteredItemsにセットする
        $keyword.combineLatest($items).map { keyword, items in
            items.filter { item in
                item.name.localizedCaseInsensitiveContains(keyword)
            }
        }
        .assign(to: \.filteredItems, on: self)
        .store(in: &cancellables)
    }
}

@Stateプロパティはprivate

@StateのプロパティをViewのbody外から操作すると実行時エラーになります。
想定外の用途を防ぐために、@Stateのプロパティにはprivateを付けるようにしたほうが良いです。

こちらはSwiftUIのドキュメントにも明記されています。

you should declare your state properties as private, to prevent clients of your view from accessing it.
https://developer.apple.com/documentation/swiftui/state

また、@Publishedのプロパティもprivate(set)にできるケースは結構多いので、できるだけViewを更新可能な人物を減らすように意識していくと良いと思います。

よく使うサービス/ツールとの相性

fastlane

今のところ何も問題なく利用できています。App Store Connectへのアップロードも全く問題ありませんでした。

Firebase Crashlytics

SwiftUIのViewはView BuilderOpaque Result TypeによってView構造が型になっているため、クラッシュログのスタックトレースがこのようになります。

※自作の型名などはマスク処理しています。

一見複雑ですが、よく見るとViewのどの部分から起きている問題なのかが以外と分かるため、特に大きな問題は感じていません。

Admob

Admobを利用する場合は、ネイティブ広告で自前のSwiftUI Viewを組み立てるか、またはAdmobが提供しているUIKitのインターフェースをラップして利用することになります。

ラップして利用する場合はこのような実装になります。

バナー

struct AdBanner: UIViewControllerRepresentable {
    let adUnitId: String

    private var adSize: GADAdSize {
        UIDevice.current.userInterfaceIdiom == .pad ? kGADAdSizeFullBanner : kGADAdSizeBanner
    }

    func expectedFrame() -> some View {
        let size = adSize.size
        return frame(width: size.width, height: size.height, alignment: .center)
    }

    func makeUIViewController(context: Context) -> UIViewController {
        let view = GADBannerView(adSize: adSize)

        let viewController = UIViewController()
        view.adUnitID = adUnitId
        view.rootViewController = viewController
        viewController.view.addSubview(view)
        viewController.view.frame.size = adSize.size
        view.load(GADRequest())

        return viewController
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

// body内 ---
AdBanner(adUnitId: "***").expectedFrame()

インタースティシャル

final class Interstitial: NSObject {
    private let adUnitId: String
    private var interstitial: GADInterstitial!

    private var completion: (() -> Void)?

    required init(adUnitId: String) {
        self.adUnitId = adUnitId
        super.init()
        load()
    }

    private func load() {
        interstitial = GADInterstitial(adUnitID: adUnitId)
        interstitial.load(GADRequest())
        interstitial.delegate = self
    }

    func show(completion: @escaping () -> Void) {
        guard 
            canShow(), 
            interstitial.isReady, 
            // ViewControllerの取得処理を簡略化していますが、場合により適切なWindowを選択して取得するように変える必要があります
            let root = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController
        else {
            completion()
            return
        }

        self.completion = completion
        interstitial.present(fromRootViewController: root)
        // 何度も表示されないように調整する場合はこの辺りに処理を書く
    }

    private func canShow() -> Bool {
        // 条件を満たしたら表示する
        return true
    }
}

extension Interstitial: GADInterstitialDelegate {
    func interstitialDidDismissScreen(_ ad: GADInterstitial) {
        completion?()
        load() // 再表示に備えて再読み込み
    }
}

// View内 ---

private let interstitial = Interstital(adUnitId: "***")

var body: some View {
    YourCustomView()
        .onAppear {
            self.interstital.show {
                // 広告を閉じた後の処理
            }
        }
}

まとめ

現段階でSwiftUIを使う時の進め方や注意点、Tipsなどを紹介しました。

まだまだAPIが足りず、複雑な画面仕様を実現するには難しいケースもありますが、ある程度SwiftUIにアプリの仕様を寄せることができれば使っても良さそうです。
UIKitでレイアウトを作るよりも圧倒的に早く見た目が作れ、作った後のレイアウト変更も簡単なので、SwiftUIのバグさえ回避できればかなり楽にアプリが作れました。

SwiftUIの破壊的変更や不具合と戦いながらその進化を見ていくのはエンジニアとしては面白い経験かと思うので、SwiftUIアプリ作りに挑戦してみてはどうでしょうか。

その他参考

260
203
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
260
203