SwiftUIで出てくるバックスラッシュの意味を調べる
SwiftUIを使っていると出てくる\.記法について調べてみました。
この記法は以下のようにForEachなどで使われます。
struct ContentView: View { var body: some View { let models = [ MyStruct(id: 1, name: "name1"), MyStruct(id: 2, name: "name2"), ] return ForEach(models, id: \.id) { Text($0.name) } } } struct MyStruct { let id: Int let name: String }
ForEachのinitの引数の型を見ると下のようなKeyPath型になっています。
public init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content)
KeyPathはSwift4で追加された機能で、以下のような形でプロパティーの値を取得できるようになります。
let keyPath = \MyStruct.name let myStruct = MyStruct(id: 1, name: "name1") myStruct[keyPath: keyPath] // → "name1"、myStruct.nameと同等の結果
KeyPathは下のようにクラスを省略することができます。
let keyPath: KeyPath<MyStruct, String> = \.name // let keyPath = \MyStruct.nameと同等
つまりForEach一番最初に出てきたForEachのバックスラッシュは、KeyPathを渡しているだけでした。
分かりやすく書くと下の書き方と同等になります。
struct ContentView: View { var body: some View { let models = [ MyStruct(id: 1, name: "name1"), MyStruct(id: 2, name: "name2"), ] let keyPath: KeyPath<MyStruct, Int> = \MyStruct.id return ForEach(models, id: keyPath) { Text($0.name) } } }
余談ですがKeyPathはSwift5.2でメソッドに変換できるようにもなりました。
これを使うとmapなどは下のように書くことができるようになります。
let models = [ MyStruct(id: 1, name: "name1"), MyStruct(id: 2, name: "name2"), ] models.map(\.id) // → [1, 2]、models.map { $0.id }と同じ結果
内部的には以下のように変換されているようです。
models.map(\.id) // ↓ のように変換 models.map { $0[keyPath: \.id] }
下のように独自のメソッドでも使うことができます。
myMethod(closure: \.id) func myMethod(closure: (MyStruct) -> (Int)) { print(closure(MyStruct(id: 3, name: "name3"))) // → 3 }
参考URL
Swiftのassociatedtype再勉強
Swiftのassociatedtypeの仕様を再度勉強しました。
associatedtypeを使うと下のように独自の型(下の例だとMyType)を定義することができます。
protocol MyProtocol { associatedtype MyType }
定義した型は下のようにメソッドの引数や戻り値に使うことができます。
protocol MyProtocol { associatedtype MyType } extension MyProtocol { func myMethod(myValue: MyType) -> MyType { return myValue } }
MyTypeの型は、MyProtocolに準拠したクラスでtypealiasを使う事で定義できます。
下の例だとMyTypeをIntにしているので、myMethodの引数や戻り値はIntになっています。
class MyClass: MyProtocol { typealias MyType = Int } MyClass().myMethod(myValue: 1) // → 1
typealiasで型の定義をしない場合はコンパイルエラーになります。
class MyClass: MyProtocol { // エラーになる }
associatedtypeの型はprotocolやclassで制限をかけることができます。
class MyAssociatedType { } protocol MyProtocol { associatedtype MyType: MyAssociatedType }
上記制限をかけた場合、先程のようにMyTypeをIntにしようとするとエラーになります。
class MyClass1: MyProtocol { // エラー typealias MyType = Int } class MyClass2: MyProtocol { // OK typealias MyType = MyAssociatedType } class MyClass3: MyProtocol { // OK typealias MyType = MyAssociatedSubType } class MyAssociatedSubType: MyAssociatedType { }
associatedtypeは下のようにジェネリクスを使うこともできます。
protocol MyProtocol { associatedtype MyType } class MyClass<T>: MyProtocol { typealias MyType = T func myMethod(value: MyType) -> MyType { return value } } MyClass<Int>().myMethod(value: 1) // → 1 MyClass<String>().myMethod(value: "AAA") // → "AAA"
以下のような書き方をすることでtypealiasを省略することも可能です。
MyClassのmyMethodの引数をTにすることで、MyTypeの型も決まるようになっています。
protocol MyProtocol { associatedtype MyType func myMethod(value: MyType) -> MyType } class MyClass<T>: MyProtocol { func myMethod(value: T) -> T { return value } }
SwiftUIのViewBuilderについて調べてみる
SwiftUIのVStackやHStackでは、下のようにTextを2行並べて書くだけでTextを縦並びにできます。
今回はこの仕組みについて調べてみました。
struct ContentView: View { var body: some View { VStack { Text("AAA") Text("BBB") } } }
上記のコードですが、下のような記述と同等になります。
複数個並べられていたTextは、ViewBuilderのbuildBlockの引数として使われます。
struct ContentView: View { var body: some View { VStack { ViewBuilder.buildBlock( Text("AAA"), Text("BBB") ) } } }
ViewBuilderのbuildBlockは、以下のようなTupleViewを返すメソッドです。
渡されたViewを元にTupleViewを作って返します。
extension ViewBuilder { public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View }
まとめると、省略記法を使わずにVStackを使う場合は以下のような記述になります。
// 元コード // struct ContentView: View { // var body: some View { // VStack { // Text("AAA") // Text("BBB") // } // } // } // 省略せずに書かれたコード struct ContentView: View { var body: some View { VStack(content: { () -> TupleView<(Text, Text)> in return ViewBuilder.buildBlock( Text("AAA"), Text("BBB") ) }) } }
続けて、ViewBuilderを省略できた理由を見ていきます。
省略できたのは、ViewBuilderの宣言についている@_functionBuilderというAttributesが関係しています。
@_functionBuilder public struct ViewBuilder { }
@_functionBuilderを使ってStructを作ると以下の@MyStruct
のようにクロージャーに付けるAttributesが作られます。
func myFunc(@MyStruct closure: () -> Int) -> Int { return closure() } @_functionBuilder struct MyStruct { static func buildBlock(_ v1: Int) -> Int { return v1 } static func buildBlock(_ v1: Int, _ v2: Int) -> Int { return v1 + v2 } static func buildBlock(_ v1: Int, _ v2: Int, _ v3: Int) -> Int { return v1 + v2 + v3 } }
@MyStructを付けたクロージャーは、以下のような省略記法を使えます。
ViewBuilderもこの機能を使う事でViewBuilder.buildBlock
という呼び出しを省略していました。
myFunc { 1 2 } // 上は以下の省略形 myFunc { MyStruct.buildBlock(1, 2) } func myFunc(@MyStruct closure: () -> Int) -> Int { return closure() }
余談ですが@_functionBuilderはclassにも使うことができます。
@_functionBuilder class MyClass { }
以下のように値の数がbuildBlockの引数の数を超えるとエラーになります。
myFunc { 1 2 3 4 }
ViewBuilderは引数が最大10個なので、下のようにViewを11個渡すとエラーになります。
struct ContentView: View { var body: some View { VStack { Text("1") Text("2") Text("3") Text("4") Text("5") Text("6") Text("7") Text("8") Text("9") Text("10") Text("11") } } }
参考URL
SwiftUIの魔法を実現する仕組み (Custom Attributes, Function Builder) - Qiita
SwiftUIでローカル変数を定義する方法
SwiftUIだとローカル変数の定義でエラーになることが多いので整理してみました。
SwiftUIでのローカル変数定義方法
bodyの中の場合は下のようにreturnを付けることで定義できます。
struct ContentView: View { var body: some View { let a = 1 return Text("Hello, World!") } }
ZStackのように複数Viewを返す場合はreturnを付けた上で() -> TupleView<(Text, Text)>
のように戻り値の型を指定する必要があります。
struct ContentView: View { var body: some View { ZStack { () -> TupleView<(Text, Text)> in let a = 1 return ViewBuilder.buildBlock( Text("AAA"), Text("BBB") ) } } }
定義に関する調査
なぜ上記方法でうまくいくかの調査は下の通りです。
下のように変数を定義するとFunction declares an opaque return type, but has no return statements in its body from which to infer an underlying type
というエラーになります。
これは値をreturnしていないというエラーです。
struct ContentView: View { var body: some View { let a = 1 Text("Hello, World!") } }
以下のようにreturnを付けることでエラーは出なくなります。
struct ContentView: View { var body: some View { let a = 1 return Text("Hello, World!") } }
ローカル変数を定義するまで動いていたのはSwiftのreturn文省略の仕様が理由です。
Swiftはクロージャー内が1行だけの場合はreturnを省略できるので、変数定義をしなければreturnを省略することができます。
struct ContentView: View { var body: some View { Text("Hello, World!") } }
mapメソッドなどでもreturnが不要なのはこの仕様が理由になります。
[1, 2, 3].map { $0 * 2 }
メソッドも同じようにreturnを省略できます。
struct ContentView: View { func test() -> some View { Text("Hello, World!") } }
returnを付ける方法ですが、Zstackの中では使うことができません。
下のように書くとCannot convert return expression of type 'ZStack<_>' to return type 'some View'
というエラーになります。
これはZStack<_>をsome Viewに変換できないというエラーです。
struct ContentView: View { var body: some View { ZStack { let a = 1 return Text("Hello, World!") } } }
このエラーは下のようにZStackの戻り値の型を宣言することで解消できます。
struct ContentView: View { var body: some View { ZStack { () -> Text in let a = 1 return Text("AAA") } } }
複数Viewを返す場合は下のようにViewBuilderを使ってTupleViewを作る必要があります。
struct ContentView: View { var body: some View { ZStack { () -> TupleView<(Text, Text)> in let a = 1 return ViewBuilder.buildBlock( Text("AAA"), Text("BBB") ) } } }
SwiftUIのEnvironment/EnvironmentObjectとはなにか
SwiftUIのEnvironment / EnvironmentObjectという記法について調べてみました。
EnvironmentObjectとは
EnvironmentObjectとはObservableObjectを外部から渡したりできるようになる記法です。
宣言は下のように行います。
struct ContentView: View { @EnvironmentObject var testValue: MyViewModel var body: some View { Text("Hello") } } class MyViewModel: ObservableObject { }
プロパティーの型はObservableObjectプロトコルに準拠している必要があります。
また、通常のプロパティーと違って初期値をセットする必要はありません。
@EnvironmentObject var testValue: MyViewModel
EnvironmentObjectの値は、下のようにenvironmentObjectメソッドで外部から注入することができます。
let contentView = ContentView().environmentObject(MyViewModel())
通常のプロパティー同様、値を以下のように取得することができます。
struct ContentView: View { @EnvironmentObject var testValue: MyViewModel var body: some View { print(testValue) return Text("Hello") } }
MyViewModelのプロパティーを2つ宣言すると、両方に同じ値がセットされます。
struct ContentView: View { @EnvironmentObject var testValue: MyViewModel @EnvironmentObject var testValue2: MyViewModel var body: some View { print(testValue === testValue2) // → true return Text("Hello") } }
environmentObjectでセットした値は、子ビューにも自動でセットされます。
struct ContentView: View { @EnvironmentObject var testValue: MyViewModel var body: some View { ChildView() } } struct ChildView: View { @EnvironmentObject var testValue: MyViewModel var body: some View { print(testValue) // 値が入っている return Text("Hello, World!") } }
Environmentとは
EnvironmentもEnvironmentObjectと似て、外部から値をセットできるようになるクラスです。
下のようにenvironmentメソッドを使って値をセットできます。
let contentView = ContentView().environment(\.font, Font.system(size: 100))
ここでセットできるのは、fontなどEnvironmentValuesのプロパティーとして宣言されているものだけです。
詳細は以下URLに参考にしてください。
https://developer.apple.com/documentation/swiftui/environmentvalues
フォントをセットした場合、以下のようにTextのデフォルトフォントサイズが変更されます。
セットした値にアクセスするためには、以下のようにEnvironmentを使ってプロパティーを宣言する必要があります。
struct ContentView: View { @Environment(\.font) var myFont: Font? var body: some View { print(myFont) return Text("Hello") } }
参考URL
SwiftUIの機能 @State, @ObservedObject, @EnvironmentObjectの違いとは| 開発者ブログ | 株式会社アイソルート
Understanding Property Wrappers in SwiftUI | Majid’s blog about Swift development
SwiftUIのsomeキーワードについて調べてみる
SwiftUIでViewを作る時、以下のようにsomeというキーワードが出てきます。
今回はこのキーワードについて調べてみました。
struct ContentView: View { var body: some View { Text("Hello, World!") } }
調査にあたって以下の記事を参考にさせて頂きました。
Swift 5.1 に導入される Opaque Result Type とは何か - Qiita
someキーワードですが、実行時のパフォーマンス向上の為のキーワードになります。
具体的には「戻り値の型をプロトコルとして宣言するとメモリ使用量が増える」という悩みを解消してくれるものになります。
仮に下のようなクラスとプロトコルがあるとします。
protocol MyProtocol { } class MyClass: MyProtocol { }
その時、下の2つのメソッドを比べるとMyProtocolを戻り値の型にしたほうがメモリ使用量が大きくなってしまいます。
// メモリ使用量が大きい func myProtocol() -> MyProtocol { return MyClass() } // メモリ使用量が小さい func myProtocol() -> MyClass { return MyClass() }
それを解決するのがsomeキーワードです。
someを付ける事で、メソッドの実装によって戻り値の型が最適なものになります。
// メモリ使用量が大きい func myProtocol() -> MyProtocol { return MyClass() } // メモリ使用量が小さい func myProtocol() -> some MyProtocol { return MyClass() }
実際に比較してみた結果は下のとおりです。
1,000,000個のインスタンスを作って、メモリ使用量を比較してみました。
結果を見るとsome MyProtocol
はMyClass
とほぼパフォーマンスを出していることがわかります。
戻り値の型 | 1回目の測定結果 | 2回目の測定結果 | 3回目の測定結果 | 平均 |
---|---|---|---|---|
MyClass | 28.9mb | 27.2mb | 31.1mb | 29.1mb |
MyProtocol | 48.7mb | 58.3mb | 61.4mb | 56.2mb |
some MyProtocol | 28mb | 32.4mb | 31.6mb | 30.7mb |
100万回個のインスタンス生成の実装は以下の通りです。
let results = (0...1000000).map { _ in myProtocol() }
メモリ使用量はXcodeのMemory reportのHighの値を使用しました。
以下のように実行速度についても調べたのですが、こちらは大きな差はありませんでした。
let d = Date() let results = (0...1000000).map { _ in myProtocol() } print(Date().timeIntervalSince1970 - d.timeIntervalSince1970)
someキーワードはクラスにも使えるのですが、当然ながらsomeがあってもなくてもメモリ使用量に差は見えませんでした。
func myProtocol() -> some MyClass { return MyClass() }
余談ですがSwiftUIのViewからsomeを外すとProtocol 'View' can only be used as a generic constraint because it has Self or associated type requirements
というエラーになります。
これはViewが、associatedtypeを使っているためです。
struct ContentView: View { var body: View { Text("Hello, World!") } }
someは引数には使うことができません。
引数はジェネリクスで対処する必要があります。
func myProtocol(myClass: some MyProtocol) { } // → x func myProtocol<A: MyProtocol>(myClass: A) { } // → o
Today Extensionでウィジェット作成
Today Extensionを使ったウィジェット作成を試してみました。
プロジェクトを作成したらメニューからTargetの作成を行います。
Today Extensionを選択します。
作成するとToday Extension関連ファイルが作られます。
Today Extensionを起動するとHello worldが表示されます。
Storyboardでテキストを変更する事で文字を変える事ができます。
ラベルはUIViewControllerと紐付ける事で動的な変更もできます。
class TodayViewController: UIViewController, NCWidgetProviding { @IBOutlet weak var myLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() myLabel.text = "Hello!" } }
ラベルだけでなくボタンも配置する事ができます。
widgetLargestAvailableDisplayModeをセットする事でウィジェットのサイズを大きくできます。
サイズはwidgetActiveDisplayModeDidChangeでセットする事が可能です。
class TodayViewController: UIViewController, NCWidgetProviding { override func viewDidLoad() { super.viewDidLoad() extensionContext?.widgetLargestAvailableDisplayMode = .expanded } func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { switch activeDisplayMode { case .compact: preferredContentSize = maxSize case .expanded: preferredContentSize = CGSize(width: preferredContentSize.width, height: 400) } } }
UserDefaultsなどのデータ共有にはApp Groupsが必要になります。
まずはXcode上でApp Groupsを作成します。
この時はアプリのターゲットだけでなくExtension側にも追加が必要です。
あとは作成したグループを使ってデータ作成すればデータ共有できるようになっています。
UserDefaults(suiteName: "group.mygroup")?.set(100, forKey: "Key") print(UserDefaults(suiteName: "group.mygroup")?.integer(forKey: "Key"))
Realmなどのデータベースのデータ共有もApp Groupsを使います。
var config = Realm.Configuration.defaultConfiguration let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.mygroup") config.fileURL = url?.appendingPathComponent("default.realm") Realm.Configuration.defaultConfiguration = config
データだけでなくクラスも共有できないので、Embedded Frameworkの利用やCompile Sources
への追加などが必要です。
CocoaPodsを使う場合、Extension用の処理も必要があるので注意が必要です。
target 'MyApp' do use_frameworks! pod 'RealmSwift' end target 'MyExtension' do use_frameworks! pod 'RealmSwift' end