しめ鯖日記

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

Swiftでframeworkを作成する

アプリ開発で良く出てくる.frameworkという拡張子のライブラリを自作してみました。

frameworkを作成する

Xcodeで新規プロジェクト作成時にFrameworkを選択します。

f:id:llcc:20200412163759p:plain

プロジェクトは下のようなファイル構成になっています。

f:id:llcc:20200412164450p:plain

Productsの中に入っているMyFramework.frameworkの構成は下の通りです。

f:id:llcc:20200412164630p:plain

続けてライブラリにファイルを追加してみます。
追加したのは下のようにメソッドが一つだけあるクラスです。

別プロジェクトから呼び出すためにアクセス修飾子はpublicにしています。

import UIKit

public class MyClass {
    static public func test() {
        print("MyFramework")
    }
}

ビルドするとMyFramework.frameworkも更新されます。
Finderで見るとSwift用のヘッダーが作られている事が分かります。

f:id:llcc:20200412164908p:plain

frameworkを使ってみる

まずはプロジェクトにライブラリを追加します。
追加はXcodeFrameworks, Libraries, and Embedded Contentにframeworkファイルをドラッグするだけです。

f:id:llcc:20200412165056p:plain

以下のように書くことでMyClassのメソッドを使う事ができます。

import UIKit
import MyFramework

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        MyClass.test()
    }
}

Frameworkがプロジェクト外に配置されている場合、Framework Search Pathsにライブラリのパスを追加する必要があります。

f:id:llcc:20200412165840p:plain

frameworkのユニバーサル対応

上記の方法では、シミュレータか実機のどちらかでだけ動くライブラリのみ作れます。
これを両方の環境で動くように修正してみようと思います。

修正は下URLを参考にしました。

Swiftでフレームワークを作成する(第1回) | GMOアドパートナーズグループ TECH BLOG byGMO

まずは両対応ライブラリをつくためのターゲットを作成します。
XcodeのFile → New → Targetからターゲットを選んで、その中のAggregateを選びます。

f:id:llcc:20200412175631p:plain

AggregateにRun scriptを追加して下コードを貼り付けます。

#!/bin/sh
 
UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal
 
# make sure the output directory exists
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"
 
# Step 1. Build Device and Simulator versions
xcodebuild -target "${PROJECT_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos  BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
xcodebuild -target "${PROJECT_NAME}" -configuration ${CONFIGURATION} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
 
# Step 2. Copy the framework structure (from iphoneos build) to the universal folder
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework" "${UNIVERSAL_OUTPUTFOLDER}/"
 
# Step 3. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory
SIMULATOR_SWIFT_MODULES_DIR="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework/Modules/${PROJECT_NAME}.swiftmodule/."
if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]; then
cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework/Modules/${PROJECT_NAME}.swiftmodule"
fi
 
# Step 4. Create universal binary file using lipo and place the combined executable in the copied framework directory
lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework/${PROJECT_NAME}"
 
# Step 5. Convenience step to copy the framework to the project's directory
cp -R "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework" "${PROJECT_DIR}"
 
# Step 6. Convenience step to open the project's directory in Finder
open "${PROJECT_DIR}"

あとはAggregateを実行するだけで両対応フレームワークを作る事ができます。

続けて上記スクリプトの内容を見ていきます。
下ではユニバーサルフレームワーク用のディレクトリの作成をしています。

#!/bin/sh
 
UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal
 
# make sure the output directory exists
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"

以下ではシミュレータ/実機それぞれ向けのフレームワークを作っています。
xcodebuildはXcodeに付いているコマンドラインツールで、プロジェクトのビルドをする事ができます。

# Step 1. Build Device and Simulator versions
xcodebuild -target "${PROJECT_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos  BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
xcodebuild -target "${PROJECT_NAME}" -configuration ${CONFIGURATION} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build

以下では生成したフレームワークやモジュールファイルのコピーをしています。

# Step 2. Copy the framework structure (from iphoneos build) to the universal folder
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework" "${UNIVERSAL_OUTPUTFOLDER}/"
 
# Step 3. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory
SIMULATOR_SWIFT_MODULES_DIR="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework/Modules/${PROJECT_NAME}.swiftmodule/."
if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]; then
cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework/Modules/${PROJECT_NAME}.swiftmodule"
fi

以下ではlipoというツールを使ってユニバーサルフレームワークを作っています。
lipoはXcodeに付属しているツールで、シミュレータ/実機向けのフレームワークを結合する事ができます。

# Step 4. Create universal binary file using lipo and place the combined executable in the copied framework directory
lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework/${PROJECT_NAME}"

最後にプロジェクトのルートディレクトリにフレームワークをコピーしています。

# Step 5. Convenience step to copy the framework to the project's directory
cp -R "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework" "${PROJECT_DIR}"
 
# Step 6. Convenience step to open the project's directory in Finder
open "${PROJECT_DIR}"

実際にUniversal対応になったかどうかはfileコマンドで確認する事ができます。

file MyFramework.framework/MyFramework

fileコマンドを実行すると下のように両環境に対応できている表示が出てきます。

MyFramework.framework/MyFramework: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamically linked shared library x86_64] [arm64:Mach-O 64-bit dynamically linked shared library arm64]
MyFramework.framework/MyFramework (for architecture x86_64):    Mach-O 64-bit dynamically linked shared library x86_64
MyFramework.framework/MyFramework (for architecture arm64): Mach-O 64-bit dynamically linked shared library arm64

iOSシミュレータ用と実機用のバイナリの場合は下のような表示になります。

# iOSシミュレータ
MyFramework.framework/MyFramework: Mach-O 64-bit dynamically linked shared library x86_64
# 実機
MyFramework.framework/MyFramework: Mach-O 64-bit dynamically linked shared library arm64

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