前置き
- まだ調べ中の部分があります。
- 主にSwift UI 1の仕様で確認しているのでSwift UI 2だと違う部分があるかもしれないです。
SwiftUIの特徴
- iOS 13から利用可能
- UIKitの完全なリプレイス
- Objective-Cとの決別
- 今まで培ってきたUIKitの大部分の知見が失われたため、一から学ぶ必要があり学習コストが高い(UIViewControllerの階層構造の概念は変わっていないのでそういう部分はまだ活用できますが)。
- MVCの廃止
- SwiftUIのデータバインディング機構のおかげでUIViewControllerが不要になった
- Storyboard, XibのXMLからプレビューのGUIを構築してUIの変更してXMLを書き換えていたのを、SwiftコードからプレビューのGUIを構築してUIを変更しSwiftコード自体を書き換えるように変わった
- XMLのコンフリクト問題が解消!
- Swift 5.1以前の言語仕様から見ると、いくつか魔法のような挙動がある
- State, ObservedObject, @propertyWrapper, @_functionBuilder
役に立つ資料
以下は一通り目を通すことをおすすめします。
-
Swift UI Tutorials
- 特にSwiftUI Essentialsの3つは必ず見た方がいいです
- WWDC 2019
- THE SWIFT PROGRAMMING LANGUAGE
その他役立つ資料
SwiftUIの動作原理
検証環境
- Xcode 11.5とXcode 12.0 beta3で検証
SwiftUIはどうやってUIの再レンダリングをコントロールしているか
-
State
とObservedObject
のみが値を変更したときにView
の再レンダリングが行われる- 自前で定義する方法については後述
- 値は
View.body
内で変更しないと再レンダリングされない?- ドキュメント探し中
State
- Stateをプロパティの属性として定義すると、そのプロパティが変更されたらViewのbodyが再計算される
struct ContentView: View {
// @State属性のプロパティが変更されたらbodyを再生成している
@State var flag = false
var body: some View {
print(#function)
return List {
Button(action: {
self.flag.toggle()
}) {
Text("self.flagを変更: \(self.flag.description)")
}
}
}
}
- この挙動は以下のようにドキュメントに記載されている
SwiftUI manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body. Use the state as the single source of truth for a given view.
- StateがどうやってViewを更新しているかは不明だった
- Is it correct to expect internal updates of a SwiftUI DynamicProperty property wrapper to trigger a view update?
ObservableObjectとObservedObject
- ObservableObjectプロトコルに準拠したクラス内の@Published属性があるプロパティが変更されると、そのクラスを@ObservedObject属性でプロパティを持つViewは変更のたびにbodyを再生成して画面を更新してくれるそのプロパティがバインディングされたSwiftUIのViewが更新される
final class Content: ObservableObject {
// @Publshed属性はwillSetでobjectWillChangeのsend()メソッドを呼び出している
@Published var flag = false
// flagを@Published属性なしで実装すると以下
var flag2 = false {
willSet {
print("manual objectWillChange.send()")
// objectWillChangeはObservableObjectのメソッド
// willSetのタイミングでsendしている
objectWillChange.send()
}
}
}
struct ContentView: View {
// @ObservedObject属性はObservableObject.objectWillChangeを
// sink (サブスクライブ)して、変更をreceiveしたタイミングでbodyを再生成している
@ObservedObject var content = Content()
private var cancellables = [AnyCancellable]()
var body: some View {
print(#function)
return List {
Button(action: {
self.content.flag.toggle()
}) {
Text("@Publishedのflagを変更")
}
Button(action: {
self.content.flag2.toggle()
}) {
Text("flag2を変更(@Publishedなし)")
}
}
}
init() {
content
.objectWillChange // objectWillChangeが呼び出されていることを確認
.print("check")
.sink(receiveValue: {})
.store(in: &cancellables)
}
}
- この挙動は以下のようにドキュメントに記載されている
By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its @Published properties changes.
A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
- @ObservedObject属性をつけられるプロパティはObservableObjectを準拠している必要がある(
where ObjectType : ObservableObject
になっている)
自前実装でViewを再レンダリングをさせる方法
- 現状は完全に独自実装でViewを再レンダリングさせることは不可で、StateとObservedObjectのみが値を変更したときにViewの再レンダリングが行われるので、それらをラップする方法で実現できる
- Swift Forumsでも独自実装は不可と回答があった。
@propertyWrapper
struct Refreshing<Value> : DynamicProperty {
let storage: State<Value>
init(wrappedValue value: Value) {
self.storage = State<Value>(initialValue: value)
}
public var wrappedValue: Value {
get { storage.wrappedValue }
nonmutating set { self.process(newValue) }
}
public var projectedValue: Binding<Value> {
storage.projectedValue
}
private func process(_ value: Value) {
// do some something here or in background queue
DispatchQueue.main.async {
self.storage.wrappedValue = value
}
}
}
struct TestPropertyWrapper: View {
@Refreshing var counter: Int = 1
var body: some View {
VStack {
Text("Value: \(counter)")
Divider()
Button("Increase") {
self.counter += 1
}
}
}
}
bodyは変更のたびに再生成されるがパフォーマンス上の問題はないか
- SwiftUIのレンダリングシステムが賢く差分でレンダリングしてくれるので問題なし
propertyWrapper宣言属性
- propertyWrapperはDeclaration Attributes(宣言属性)
- propertyWrapper宣言属性がある定義は
wrappedValue
というプロパティ名のgetterが必須になる(大抵はStored property)。 - propertyWrapper宣言属性がある定義はプロパティの属性に定義可能で、暗黙的に以下の3つのプロパティに変換される
@State private var flag = false
↓ 変換
private var _flag: State<Bool> = State(initialValue: false)
private var $flag: Binding<Bool> { return _flag.projectedValue }
private var flag: Bool {
get { return _flag.wrappedValue }
nonmutating set { _flag.wrappedValue = newValue }
}
*※ Why I Can Mutate @State var? How Does @State Property Wrapper Work Inside?
@propertyWrapper
struct SomeWrapper {
var wrappedValue: String
}
struct ContentView: View {
@SomeWrapper var wrapper = ""
init() {
print(wrapper)
print(_wrapper.wrappedValue) // wrapperと等価
}
}
DynamicPropertyプロトコル
- StateやObservedObjectはView.bodyが再生成されても前回のプロパティの値を保持している仕組みはDynamicPropertyプロトコルが処理している
- View.bodyの再生成される直前にDynamicProperty.update()が呼ばれるので、そこで
@propertyWrapper
struct Wrapper: DynamicProperty {
var wrappedValue: Int
mutating func update() {
// StateはどうにかしてwrappedValueに前回の値を入れている
}
}
その他 Swift 5.1の機能
関数でのreturnを省略できるようになった
some 構文
- Opaque Result Type (不透明な戻りの型、具体的な型はこうかいしないけど、プロトコルに準拠した何らかの型)
- 内部実装を隠蔽しながらパフォーマンスにも影響しない手段
- Protocolを返すとオーバーヘッドがありパフォーマンスに影響するので、Swiftの標準ライブラリでもほとんど使われていない
- Opaque Result Type はリバースジェネリクスのシンタックスシュガー
- 参考: Swift 5.1 に導入される Opaque Result Type とは何か
// リバースジェネリクス(実際はこういう構文はないけど概念)
func makeAnimal() -> <A: Animal> A {
return Cat()
}
// Opaque Result Type
func makeAnimal() -> some Animal {
return Cat()
}
ViewBuilder
- 以下のTextは複数ありreturnしてるわけでもないのに、なぜか複数のTextをVStackに渡せている
VStack {
Text("Turtle Rock")
Text("Joshua Tree National Park")
}
- VStackの定義は以下でクロージャーに@ViewBuilder属性がついている
@inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
- ViewBuilderの定義は以下で@_functionBuilder属性(宣言属性?)がついている
@_functionBuilder public struct ViewBuilder {
/// Builds an empty view from an block containing no statements, `{ }`.
public static func buildBlock() -> EmptyView
/// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through
/// unmodified.
public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}
- @_functionBuilderはドキュメントに記載がない
- @ViewBuilderは改行区切りの要素から、変数宣言と関数呼び出しを生成する
- ViewBuilderの定義は
@_functionBuilder public struct ViewBuilder
- @_functionBuilderはクロージャーの中で複数の子Viewを生成できるパラメータ属性
- ViewBuilderのextensionに1から10個までViewが渡せる関数が定義されている。以下は引数10個ある定義
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
- クロージャに書くだけで、複数の子Viewを渡せる関数のパラメータに入れてくれるシンタックスシュガーと言える
- 10個を超えて渡したい場合はGroupを使用
- 参考: @_functionBuilderを理解してSwiftUIイリュージョンのタネをあかす
State, ObservedObject, EnvironmentObject について
struct ContentView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
NavigationView {
VStack {
// A button that writes to the environment settings
Button(action: {
self.settings.score += 1
}) {
Text("Increase Score")
}
NavigationLink(destination: DetailView()) {
Text("Show Detail View")
}
}
}
}
}
struct DetailView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
// A text view that reads from the environment settings
Text("Score: \(settings.score)")
}
}
EinvironmentObjectとEnvironmentについて
- スコープの違いがある
- Environmentはグローバル
- EinvironmentObjectはNavigationLinkでViewの子ならokだけど、sheetではアクセス不可なのでView階層内のみにスコープを限定
- Environmentはアプリ全体で使う環境変数として使ったり、共通で使うService層のDIとして使うのがいいと思った。
protocol APIClientType {
func request()
}
final class APIClient {}
extension APIClient: APIClientType {
func request() {
print(#function)
}
}
struct APIClientKey: EnvironmentKey {
static let defaultValue: APIClientType = APIClient()
}
extension EnvironmentValues {
var apiClient: APIClientType {
get { self[APIClientKey.self] }
set { self[APIClientKey.self] = newValue }
}
}
@Environment(\.apiClient) private var apiClient
@_dynamicReplacement(for: )
について
- 調べ中
Appleが推奨するアーキテクチャはMVCから変わった?
- 今まではMVCでしたがData Flow Through SwiftUIの資料にはFluxっぽい図が書かれていた
疑問
- @Stateなどの変更がどういう仕組みでViewに対しての更新処理を行わせているのかが不明
- ObservableObjectはobjectWillChangeがあるのまだ理解可能なのですが…
- setNeedsBodyUpdateみたいなメソッドがあるのかなと想像(WKHostingControllerにはあります)
まとめ
- Storyboard, XibのXMLベースでGUIからUIを構築していたものから、SwiftコードベースのGUIでUIを構築するものに変わった
- CombineフレームワークのおかげでUIViewControllerが不要になった
- Swift UIはまだ発展と途上っぽいので本格的に採用は難しそう
- 現状はSwift UIよりCombineとApple純正Fluxに期待したい