『1人でアプリを作る人を支えるSwiftUI開発レシピ』などを読み、SwiftUIを軽く触ったときのメモ。(※理解度がまだ浅いので、間違っている記載がある可能性があります)
出典 : Xcode - SwiftUI - Apple Developer
概要
- WWDC2019で発表
- iOS13以上で利用可能
- iOS14 SDKからさらにAPI追加されてる。実質使える状態なのはiOS14からかな...
- iOS14から、フルSwiftUIでアプリ作れるようになった
- iOS14から登場したWidget機能を実装するには、SwiftUIがマストで必要
※チュートリアルが充実してる
特徴
- 宣言的UI
- データバインディング (ステートドリブンなシステム)
- リアルタイムのViewレイアウトプレビュー
ReactやFlutterやってる人には馴染みやすいはず
今までのビュー作成との違い
従来は、命令型 (宣言型の逆) プログラミングでビューを作っていた
let imageView = UIImageView(image: UIImage(named: "unko"))
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 10
imageView.clipsToBounds = true
view.addSubview(imageView)
↑こんな感じのコードが、↓以下のように。
宣言的に書いて、View修飾子 (ex. .cornerRadius
) をチェーンさせて新しいViewを返していく。
Image("unko")
.cornerRadius(10)
プレビューもXcodeで出せる!
※ただし、すぐ止まったりする(謎
また、ビューの重ねていき方のパラダイムも異なる。
SwiftUIでは、親Viewが表示可能領域・子の配置位置を決め、子Viewは自身のView領域を決める。つまり、親は子のサイズ決定には基本的に関与しない。子が自身のサイズを決める
コンポーネント
例えば [Swift] SwiftUIのチートシート - Qiita にUIKitとの対応がけっこう書いてある。
こういうの見て、あとは実際に書いて理解してくしかないかな...
アプリの作成
プロジェクトを新しく作成すると、以下のようなファイルが自動でできる。
import SwiftUI
@main
struct SwiftUISampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@main
がエントリポイントを表す。
- App -> Scene (WindowGroup) -> View という構成で作る
- 今のところiPhoneアプリの場合はSceneが1つ
- iPadアプリではマルチウィンドウ機能があるのでSceneが2つとかになる
- アプリの状態検知は、
scenePhase
つかう (↓のようなイメージ)
@main
struct SwiftUISampleApp: App {
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .background:
...
}
}
}
}
}
データ管理
'Property Wrapper'というやつでデータをViewにバインディングする機能が標準で備わってる
例えばシンプルな例。
struct ToggleView: View {
@State private var isOn = false
var body: some View {
VStack {
Toggle("スイッチON", isOn: $isOn)
Text("\(isOn ? "ON" : "OFF")")
}
.frame(width: 160, height: 64)
}
}
@State
というPropertyWrapperをつけたプロパティに$
をつけることで、データがバインディング対象になる
Property Wrapper8種類
データは何か、データはどのように処理するか、データはどこからくるか、で用途を分ける
-
@State
- 扱うデータが値型 and View自身でデータを保持して更新する場合に使う
-
@Binding
- 扱うデータが値型 and 親Viewなど外部からデータを渡され更新する場合に使う
-
@State
のデータの先頭に$
マークをつけることで、@Binding
のデータに変換可能
-
@Environment
- 環境値を読み取る場合に使う。KeyPathを指定して読み込み
-
@StateObject
- ※iOS14以降
- 扱うデータが参照型 and View自身でデータを保持して更新する場合に使う
- データ更新→再レンダリングの際も、Viewインスタンスは破棄されない(=最初の一度しか作成されない)
-
@ObservedObject
- 扱うデータが参照型 and 親Viewからデータを渡され更新する場合に使う
- こっちはViewインスタンスが再レンダリング時に破棄される
-
@EnvironmentObject
- 階層を飛び越えてViewにデータオブジェクトを渡せる
-
@ObservedObject
のバケツリレーを避けられる
- ※
ObservableObject
プロトコル- 参照型のデータを監視する場合、対象のオブジェクトはObservableObjectプロトコルに準拠しないといけない
-
@AppStorage
- 値型のデータを格納できる
- 格納先はUserDefaults。ライフサイクルがアプリが消されるまで
-
@SceneStorage
- マルチウインドウをサポートするアプリで、シーンごとに値型のデータを格納できる
- 使い方は、
@AppStorage
とほぼ同じ
Combine
こちらもWWDC19で発表されたフレームワーク。
- データ処理を宣言的に扱えるようになり、非同期イベントのハンドリングがしやすくなる
- RxSwiftやReactiveSwiftなど、Reactive系のフレームワークに馴染みのある人には扱いやすい?
- ※まだ慣れてないので調べきれてないが、異なる部分や足りない部分も当然あるはず
- SwiftUIの備えるデータバインディング機構と相性がいいので、SwiftUIにAPI通信処理etc.を組み込む際はCombineの利用が前提になるはず
登場人物はざっくり3つ
- Publisher
- Subscriber
- Operator
役割分担はこんな感じで、RxSwiftとかの経験あるなら概念は理解しやすいと思う
- Publisherが発行するデータストリームをSubscriberが受け取る。
- = 雑にいうとイベント駆動で処理を書ける
- Operatorは、両者の間でデータストリームの値の変換を行う。
例えば以下のようにCombineを使う。(他にもやり方はある)
import Combine
class Unko {
var touched = PassthroughSubject<String, Never>()
}
let unko = Unko()
let subscriber = unko.touched.sink { str in
print("Unko touch: \(str)")
}
unko.touched.send("1回目")
unko.touched.send("2回目")
subscriber.cancel()
unko.touched.send("3回目はない")
出力されるのは、
Unko touch: 1回目
Unko touch: 2回目
※Unkoクラスのtouchプロパティに、@Published
をつけるやり方もある。
これをつけとくと、変数の値が更新されたタイミングで、監視してるSubscriberに更新が伝わる。
RxSwift, RxCocoaとのAPI対応表みたいのがあるので、Rxに慣れてる人はそれ見ながら覚えてくのが良さそう
CombineCommunity/rxswift-to-combine-cheatsheet: RxSwift to Apple’s Combine Cheat Sheet
Widget
- iOS14の新機能。ホーム画面におけるようになったアレ
- Widgetに配置できるのはLabelやImageなど表示系のみ。スクロールとかスイッチは無理
- WidgetのUIイベントはタップのみ
- アプリを起動できる
- DeepLinkが設定できるので、特定の画面に遷移させられる
実装
アプリ本体のエクステンション (追加ターゲット) として、作成する
- エントリポイントで、
Widget
プロトコルに準拠-
WidgetConfiguration
プロトコルのインスタンスをbodyとして返す-
StaticConfiguration
: ユーザが設定を編集できない -
IntentConfiguration
: ユーザが設定を編集できる
-
-
- 表示 (更新) のロジックを
TimelineProvider
プロトコルが担う- WidgetはタイムラインにそってViewを更新
- 3つのメソッドの実装が必要
-
placeholder(in:)
- 初期表示
-
getSnapshot(in:completion:)
- ホーム画面に追加されたときや、Widget Gallleryで表示されたとき
-
getTimeline(in:completion:)
- タイムラインに沿った更新時など?
-
- タイムラインの更新ポリシーにも種類がある :
TimelineReloadPolicy
-
.atEnd
,.after(_:)
,.never
-
- ※ホストアプリからWidgetKitを通して、更新処理をキックすることももちろんできる
-
WidgetCenter
を使う
-
タップ時の処理は、DeepLinkの仕組みでゴニョゴニョする。.widgetURL
修飾子を使う
- Widget側に
.widgetURL
を追加して、特定のURLセット - ユーザがタップすると、ホストアプリで
.onOpenURL
が呼ばれURLを受け取る - ホスト側でURLをハンドリング
雑にまとめ
- 動きの少ないビューであれば、UIKitで作るよりもサクッと作れそう
- 応用が効くかどうか。HIGにのっとって作るなら大丈夫かな...?
- TableViewやCollectionViewのときにメモリを効率的につかってくれるのかがちょい不安
- iOS14でAPI増えて改善したようなので、たぶん大丈夫...
- Combineとの組み合わせで、必然的にデータバインディングを利用したリアクティブプログラミングで作ることになる
- ちゃんと作れば、状態の複雑さに起因するバグが確実に減る
- (たぶん)デファクトにある程度なってるRxSwift系の流れを汲みつつ、RxSwift依存から離れられる
- アプリがiOS14以上サポートになるまで(あと1-2年くらい?)は、既存アプリに組み込むのは面倒が増えるだけかも
- 練習的に、OSバージョンで条件分岐させて簡単な画面作ってみるのはあり。シンプルな設定画面とか
- 新規アプリで採用するどうかはもう少し使ってみて判断したい。...が、UIKitとのハイブリッド構成もいけるので、困ったらそっちに逃げれる?
- 既存アプリに突っ込むなら、Widget機能つくるタイミングが一番な気がする
- そもそもSwiftUI使わないとリリースできないので
とりあえずもっと使ってみたい! (あと、RxSwiftをCombineでリプレイスしたい)