しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

Sequenceプロトコルを使ってみる

Sequenceというプロトコルを試してみました。
Sequenceは配列に使われているプロトコルで、準拠することでforEachやmapなどのメソッドを使うことができます。

実装例は下の通りです。
deferを使ってreturnの後にcountを-1しています。

これで10から1までを順番に取り出せる構造体を作ることができました。

struct Countdown: Sequence, IteratorProtocol {
    var count: Int

    mutating func next() -> Int? {
        if count == 0 {
            return nil
        } else {
            defer { count -= 1 }
            return count
        }
    }
}

Countdown(count: 10).forEach { print($0) } // → 10, 9, 8...2, 1

サンプルは下URLのものを使っています。

https://developer.apple.com/documentation/swift/sequence

下のようにInt以外を扱うことも可能です。

struct MySequence: Sequence, IteratorProtocol {
    var index = 0
    let texts = ["a", "b", "c", "d", "e"]
    
    mutating func next() -> String? {
        if index >= texts.count {
            return nil
        } else {
            defer { index += 1 }
            return texts[index]
        }
    }
}

MySequence().forEach { print($0) } // → "a", "b", "c", "d", "e"

Swiftで文字列を一定の長さ毎に区切って配列にする方法

下のように文字を一定の長さで区切って配列にする方法を調べてみました。

"あいうえお" → ["あい", "うえ", "お"]

標準のメソッドは用意されてないようなので、下のように独自のメソッドを追加しました。

extension String {
    func split(length: Int) -> [String] {
        var texts: [String] = []
        var start = self.startIndex
        while start < self.endIndex {
            let end = self.index(start, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
            texts.append(String(self[start..<end]))
            start = end
        }
        return texts
    }
}

使い方は下の通りです。

"あいうえお".split(length: 2) // → ["あい", "うえ", "お"]
"あいうえお".split(length: 1) // → ["あ", "い", "う", "え", "お"]

メソッドでは下の書き方で文字列の一部を取り出しています。
startとendはString.Index型になります。

self[start..<end]

endは下の書き方で作っています。
limitedByはlengthが大きすぎる時にFatal error: String index is out of boundsエラーが発生するので付けています。

let end = self.index(start, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex

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 5.2の新機能 - Qiita

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")
        }
    }
}

f:id:llcc:20200322142651p:plain

上記のコードですが、下のような記述と同等になります。
複数個並べられていた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のデフォルトフォントサイズが変更されます。

f:id:llcc:20200308204035p:plain

セットした値にアクセスするためには、以下のように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