Flutterメインの開発者がSwiftUIのチュートリアルする
筆者について
2024年6月時点
Flutter
バージョン1.0.0のリリース(2018年12月)よりも前から触っていて、iOSとAndroid同時に実装できるなんてお得!!!と思ってそれ以来注目している。
商用アプリも対応可能。
今は個人開発ではモノレポでミニアプリを量産中。
好きな状態管理はriverpod
SwiftUI
2012年ぐらいまではObjective-CでOS XやiOSアプリをStoryboardで作っていた。
2014年にSwift出た時にiOSアプリ作って、その後Swift 2.0へのマイグレーションも行ったが、当時の記憶は全くない。
Swiftは覚えていないし。SwiftUIも未経験。基本の動画は見た。
チュートリアルがあるので沿ってやります
開発環境
macOS 15 beta
Xcode 16.0 beta
Chapter 1 Creating and combining views
Section 1
プレビューは便利。
Flutterでもホットリロードしてるのと同じ感じではあるが、ダークモードやデバイスの向き、フォントサイズなども一気に確認できるのが良い。
device_previewやmedia_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
これすごい
習熟したらいらないと思うけど何があるか分かりやすい。
使い方の説明もあるしリッチ。
VStack
やHStack
で並べられる。
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)
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のサイドバー対応も完了してる
すごいね
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
Chapter 2 Drawing paths and shapes
Section 1
特筆すべきことはない
Section 2
特筆すべきことはない
Section 3
特筆すべきことはない
Section 4
ZStackはFlutterのStackのようなもの
パス操作系はFlutterでもあるけど、ほぼ使わないのでよく分かっていない
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))
}
}
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をタイポしててもエラーにはならない
アイコンが表示されないだけみたい
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の役割を変えられる
キャンセルボタンは用意されていないのか
cancel
と destructive
があるみたい。
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
}
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だけでも実装できる?
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]
)
}
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
所感
お作法通りの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のフレームワークに特化している(する)だろうから今後に期待。
総括
SwiftUI面白い。
FlutterでAppleらしいUIを作る場合はCupertinoWidgetになるが、CupertinoWidgetを頑張って使ってAppleらしいUI作るより、SwiftUIでいいんじゃないかと感じた。
複数のAppleプラットフォームに対応するなら、なおさらSwiftUIの方が楽だと思う。
AppleらしいUIではなく、マテリアルデザインなどでAndroidも含めてマルチプラットフォームなアプリを構築するなら当然Flutterの方が良い。
基本はSwiftUIで、一部だけFlutterにするような悪魔合体も確か可能なので、これまでFlutterで実装したリソースを使い回すといったことも可能かもしれない。