Closed16

Flutterメインの開発者がSwiftUIのチュートリアルする

kingukingu

筆者について

2024年6月時点

Flutter

バージョン1.0.0のリリース(2018年12月)よりも前から触っていて、iOSとAndroid同時に実装できるなんてお得!!!と思ってそれ以来注目している。
商用アプリも対応可能。
今は個人開発ではモノレポでミニアプリを量産中。
https://github.com/KoheiKanagu/garage

好きな状態管理はriverpod

SwiftUI

2012年ぐらいまではObjective-CでOS XやiOSアプリをStoryboardで作っていた。
2014年にSwift出た時にiOSアプリ作って、その後Swift 2.0へのマイグレーションも行ったが、当時の記憶は全くない。

Swiftは覚えていないし。SwiftUIも未経験。基本の動画は見た。
https://www.youtube.com/watch?v=HyQgpxX__-A

kingukingu

Chapter 1 Creating and combining views

Section 1

プレビューは便利。
Flutterでもホットリロードしてるのと同じ感じではあるが、ダークモードやデバイスの向き、フォントサイズなども一気に確認できるのが良い。
device_previewmedia_query_preview が公式サポートされてるようなもの。

仕組み的にはどうなってるんだ?
SimulatorでiOSデバイスは作成しておく必要があったけど、Simulatorが動いてるわけではなさそう。
UI部分だけ動かしてる?すごく変態チックなことやってそう

Section 2

SwiftUI Inspector でフォントとか変更できるのいいね。

Storyboardでも似たようなことはできたけど、管理しやすいSwiftUIの方が良い

FlutterだとtextStyleなどでラップするが、SwiftUIではメソッドチェーンでスタイルを記述する。

なんのスタイルが当たっているのか読みやすい気もする。
スタイルを別のTextに使いまわしたい場合はどうなる?

        Text("Turtle Rock")
            .font(.title)
            .foregroundColor(.green)

.title.greenのようにクラス名を省略できる。

クラス名は冗長なので書くときは楽そうだが読むときは慣れないと何の何?ってなりそう。

Section 3

これすごい

習熟したらいらないと思うけど何があるか分かりやすい。
使い方の説明もあるしリッチ。

VStackHStackで並べられる。

Flutterで言うところのColumnやRowと同じようなものか。
画面からはみ出ないようにうまいこと調整されている?

.padding() 不思議

あらかじめいい感じのpaddingが定義されているのか...

Section 4

Image("turtlerock") の指定でパスは出ないか

でも出してくれそう
設定の問題かもしれない

Imageの操作系はSwiftUIの方がシンプル

FlutterだとImageにエフェクト付けたりは面倒でややこしいイメージがある

        Image("turtlerock")
            .clipShape(Circle())
            .overlay {
                Circle().stroke(.white, lineWidth: 4)
                
            }
            .shadow(radius: 7)

Section 5

MapView

プラットフォームに標準搭載されてるから当然っちゃ当然だけど、地図出すの簡単すぎる

Section 6

DefaultTextStyleみたいなことできる

HStack{
    Text("Joshua Tree National Park")
    Spacer()
    Text("California")
}
.font(.subheadline)
.foregroundStyle(.secondary)
kingukingu

Chapter 1 Building lists and navigation

Section 1

特筆すべきことはない

Section 2

特筆すべきことはない

Section 3

複数のPreviewを定義できる

すごい!

#Preview {
    LandmarkRow(landmark: landmarks[1])
}

groupにもできる

すごい!

#Preview {
    Group {
        LandmarkRow(landmark: landmarks[0])
        LandmarkRow(landmark: landmarks[1])
    }
}

Section 4

Listで括っただけなのに綺麗にiOSらしく表示されるなぁ

Section 5

Listで引数がidのメソッド多い

Dartはオーバーロードがサポートされてないけど、Swiftはオーバーロードがサポートされている。

バックスラッシュ

\なんてエスケープするときぐらいしか使わないので違和感

List(landmarks, id: \.id)

Section 6

詳細画面作るの簡単すぎる

NavigationSplitViewでiPad向けの対応もできてるそうだし...

NavigationSplitView {
    List(landmarks) { landmark in
        NavigationLink {
            LandmarkDetail()
        } label: {
            LandmarkRow(landmark: landmark)
        }
    }
    .navigationTitle("Landmarks")
} detail: {
    Text("Select a Landmark")
}

Section 7

ScrollView でスクローラブル

Listとは違う

Section 8

iPadのサイドバー対応も完了してる
すごいね

kingukingu

Chapter 1 Handling user input

Section 1

Flutterと同じようにif で出しわけできる

if landmark.isFavorite {
    Image(systemName: "star.fill")
        .foregroundStyle(.yellow)
}

Section 2

特筆すべきことはない

Section 3

これだけで@Stateの変数とbindingできる

Flutterで言うところのonChanged() 的なイベントを受け取ってstateにsetするみたいなことも不要。
ポインタの参照渡してる感じ。実際そうなのかも?

Toggle(isOn: $showFavoritesOnly) {
    Text("Favorites only")
}

表示非表示のアニメーションも簡単

.animation(.default, value: filteredLandmarks)

Section 4

@Observable

よく分からないけどwatch/listenしてくれるみたいなこと?

Section 5

Viewの外からデータを与えるにはenvironment

.environment(ModelData())

Section 6

@Bindingの値を読み書きしたらUIにも伝搬される

setState() も勝手にやってくれてる感じかな

    @Binding var isSet: Bool
kingukingu

Chapter 2 Drawing paths and shapes

Section 1

特筆すべきことはない

Section 2

特筆すべきことはない

Section 3

特筆すべきことはない

Section 4

ZStackはFlutterのStackのようなもの

パス操作系はFlutterでもあるけど、ほぼ使わないのでよく分かっていない

kingukingu

Chapter 2 Animating views and transitions

Section 1

特筆すべきことはない

Section 2

簡単なアニメーションは簡単にできる

nilのanimationを間に挟むとそれより上のアニメーションをオフにできる

.animation(nil, value: showDetail)

Section 3

withAnimationで括ればアニメーションされるようになる

AnimatedContainerなどのアニメーションのウィジェットと同じようなシンプルさ

withAnimation {
    showDetail.toggle()
}

Section 4

combinedで別のアニメーションも組み合わせられる

言語としてextensionもある

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        .asymmetric(
            insertion: .move(edge: .trailing).combined(with: .opacity),
            removal: .scale.combined(with: .opacity)
        )
    }
}

それっぽいアニメーション簡単にできる

すごい簡単

extension Animation {
    static func ripple(index: Int) -> Animation {
        Animation.spring(dampingFraction: 0.5)
            .speed(2)
            .delay(0.03 * Double(index))
    }
}
kingukingu

Chapter 3 Composing complex interfaces

Section 1

特筆すべきことはない

Section 2

特筆すべきことはない

Section 3

paddingはそれぞれで定義できる

.padding(.leading, 15)
.padding(.top, 5)

Section 4

フィットさせるかとかサイズなどはメソッドチェーン

FlutterだとFittedBoxとかSizedBoxのようなWidgetで囲わないといけないのでネストが深くなりがちだけど、そうはならないっぽい。
順番が重要にはなりそう。

modelData.features[0].image
    .resizable()
    .scaledToFit()
    .frame(height: 200)
    .clipped()

Section 5

TabView簡単すぎる

画面遷移のルーティングが隠されてるからすごく簡単

TabView(selection: $selection) {
    CategoryHome()
        .tabItem {
            Label("Featured", systemImage: "star")
        }
        .tag(Tab.featured)
    
    LandmarkList()
        .tabItem {
            Label("List", systemImage: "list.bullet")
        }
        .tag(Tab.list)
}

systemImageをタイポしててもエラーにはならない

アイコンが表示されないだけみたい

kingukingu

Chapter 3 Working with UI controls

Section 1

予約語と同じ変数が定義できる

static let `default` = Profile(username: "g_kumar")

アクセシビリティ

.accessibilityLabel("Badge for \(name).")

UIのプレビューがクラッシュ

クラッシュするのはいいけど、どこがエラーなのか教えてほしい
stacktraceが不親切?なぜか直った

.sheetで遷移先を渡せばsheetになる

.sheet(isPresented: $showingProfile) {
    ProfileHost()
        .environment(modelData)
}

Section 2

編集中かどうかのフラグが既に用意されている

そして EditButton() で切り替えられる
すごいお膳立てされてる

@Environment(\.editMode) var editMode

Section 3

そのままのTextFieldなのにoverflowしない

HStack {
    Text("Username")
    Spacer()
    TextField("Username", text: $profile.username)
        .foregroundStyle(.secondary)
        .multilineTextAlignment(.trailing)
}

Section 4

roleでButtonの役割を変えられる

キャンセルボタンは用意されていないのか
canceldestructive があるみたい。

Button("Cancel", role: .cancel) {
    draftProfile = modelData.profile
    editMode?.animation().wrappedValue = .inactive
}

onAppearとonDisappearでやりとりするの分かりにくい

Cancelボタンで変更を破棄する処理もあるし美しくはない

ProfileEditor(profile: $draftProfile)
    .onAppear {
        draftProfile = modelData.profile
    }
    .onDisappear {
        modelData.profile = draftProfile
    }
Button("Cancel", role: .cancel) {
    draftProfile = modelData.profile
    editMode?.animation().wrappedValue = .inactive
}
kingukingu

Chapter 4 Interfacing with UIKit

Section 1

昔はこんなUIPageViewController実装してたけど今見るとややこしい

SwiftUIよりはカスタマイズ性はありそうだけど...

import SwiftUI
import UIKit

struct PageViewController<Page: View>: UIViewControllerRepresentable {
    var pages: [Page]
    
    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewControlelr = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        
        return pageViewControlelr
    }
    
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        pageViewController.setViewControllers(
            [UIHostingController(rootView: pages[0])], direction: .forward, animated: true)
    }
}

Section 2

PageViewを実装しているって実感するぐらい複雑

Section 3

Delegateパターン

Section 4

@objc#selector など古典的な方法も利用できる

現役なのかもしれないけど、個人的には懐かしい感覚

AppleらしいPageViewできる

UIKitとの組み合わせの例だけど、実際はSwiftUIだけでも実装できる?

kingukingu

Chapter 4 Creating a watchOS app

Section 1

watchOSターゲットの追加

Section 2

Target Membershipに追加したらwatchOS アプリからも参照できる

iOSとwatchOSとで共有するパッケージを作るとかではなくて共有できるのか

Section 3

同じ名前の変数が定義できる

Dartではできなかったはず...
たまに困って変な名前にしがち

@Bindable var modelData = modelData

watchOS向けも同じように簡単にUI実装できる

レイアウトに違和感はあるけどiOSでも動く

Section 4

Viewの名前を同じにしていればwatchOSとiOSで別のLandmarkDetailが実行される

これはすごい
ターゲットに応じて適切にリンクしてくれる
importでパス書かなくていい理由かぁー

Section 5

.taskでasynchronous taskとして通知許可リクエスト出せる

LandmarkList()
    .task {
        let center = UNUserNotificationCenter.current()
        _ = try? await center.requestAuthorization(
            options: [.alert, .sound, .badge]
        )
    }
kingukingu

Chapter 4 Creating a macOS app

Section 1

macOSのターゲットを追加

Section 2

macOS向けのLandmarkDetail

Section 3

watchOSでは出さないみたいなこともできる

#if !os(watchOS)
Text(landmark.park)
    .font(.caption)
    .foregroundStyle(.secondary)
#endif

Section 4

ToolbarにMenuでPickerなど置いてフィルターかけたりできる

ToolbarItem {
    Menu {
        Picker("Category", selection: $filter) {
            ForEach(FilterCategory.allCases) { category in
                Text(category.rawValue).tag(category)
            }
        }
        .pickerStyle(.inline)
        Toggle(isOn: $showFavoritesOnly) {
            Label("Favorites only", systemImage: "star.fill")
        }
    } label: {
        Label("Filter", systemImage: "slider.horizontal.3")
    }
}

Section 5

これだけでメニューにコマンド追加できる

お膳立てがすごいな

struct LandmarkCommands: Commands {
    var body: some Commands {
        SidebarCommands()
    }
}

Section 6

フォーカスした時用のBindingがある

@FocusedBinding(\.selectedLandmark) var selectedLandmark

追加のメニューもキーボードショートカットもこれだけ

CommandMenu("landmark") {
    Button("\(selectedLandmark?.isFavorite == true ? "Remove" : "Mark") as Favorite") {
        selectedLandmark?.isFavorite.toggle()
    }
    .keyboardShortcut("f", modifiers: [.shift, .option])
    .disabled(selectedLandmark == nil)
}

Section 7

AppStorageでUserDefaultsにアクセスできる

書き込みとか読み込みの処理を書かなくていいの楽でいいね

@AppStorage("MapView.zoom")
private var zoom: Zoom = .medium
kingukingu

所感

お作法通りのAppleらしいUIを作るならとても簡単。お膳立てが充実している。
iOSやmacOSなど別のプラットフォームでも全く同じコードでiOSらしい、macOSらしいUIになるのは面白い。
HIGに従って組んでいけば変なUIにはならないような気はする。

一方でAppleらしいUI以外を実装したい場合は、どこまで可能なのかが気になる。
UIKitやAppKitを使えば細かいところまでカスタマイズできると思うので必要に応じて連携する必要はありそう。
ただフルにカスタマイズするならFlutterでいいと思う。

状態管理や画面遷移

状態管理や画面のルーティングもいい感じにやってくれるので、Flutterに比べて勉強量は少なくなるのかもしれない。
Flutterだと最近だとgo_routerなりriverpodなりの標準機能ではないものも勉強が必要だと思うので。
SwiftUIの標準機能だけで商用レベルのアプリが実装できるのかは、まだ分からない。

SwiftUIのプレビュー

非常に便利。
FlutterもHot reloadがあるので簡単にUIを確認しつつ実装できるが、iOSなりのシミュレータ上で実行しているに過ぎない。

SwiftUIの方はUIだけをレンダリングしてくれるし、エンドポイントもそれぞれのView毎に作れるのでStorybookのように使うことができて便利だと思う。
watchOSやmacOSでの確認も切り替えれば良いだけなので簡単。
visionOSでもそのまま動いた

ただ安定していないのか実装が悪いのか分からないけど謎のエラーでクラッシュすることがあったし、スタックトレースが親切じゃないような気もする。これは習熟度の問題なのでまだちゃんと分からないけど。

Xcode

普段はVS Codeなので扱い辛い...
当然キーボードショートカットが違うのでカスタムするか慣れるかが必要。

Swift自体はVS Codeでも実装できるけどSwiftUIとか諸々の機能を使うにはやはりXcodeしかない

Predictive Code Completion

使ってみたけど...

GitHub Copilotの方が賢いし、提案までが早いと感じた。(2024/06/21時点)
早さは使ってるMac(今回はM2 Pro)によると思うのでなんとも言えないが、賢さという点ではGitHub Copilotの方が良い。

GitHub Copilotは思考を読み取ってるのか?ってぐらいいい感じのコードを提案してくれることもあるけど、Predictive Code Completionはそんなことなく、どちらかというと余計なことをしてきていた感覚。

Predictive Code CompletionはSwiftはもちろんSwiftUI、AppKit、UIKitや各種Appleのフレームワークに特化している(する)だろうから今後に期待。

kingukingu

総括

SwiftUI面白い。

FlutterでAppleらしいUIを作る場合はCupertinoWidgetになるが、CupertinoWidgetを頑張って使ってAppleらしいUI作るより、SwiftUIでいいんじゃないかと感じた。
https://garage.kingu.dev/locamusic/
はCupertinoWidgetで頑張ってAppleライクにしたけどめんどくさかった...
複数のAppleプラットフォームに対応するなら、なおさらSwiftUIの方が楽だと思う。

AppleらしいUIではなく、マテリアルデザインなどでAndroidも含めてマルチプラットフォームなアプリを構築するなら当然Flutterの方が良い。

基本はSwiftUIで、一部だけFlutterにするような悪魔合体も確か可能なので、これまでFlutterで実装したリソースを使い回すといったことも可能かもしれない。

このスクラップは5ヶ月前にクローズされました