Swiftでframeworkを作成する
アプリ開発で良く出てくる.frameworkという拡張子のライブラリを自作してみました。
frameworkを作成する
Xcodeで新規プロジェクト作成時にFrameworkを選択します。
プロジェクトは下のようなファイル構成になっています。
Productsの中に入っているMyFramework.frameworkの構成は下の通りです。
続けてライブラリにファイルを追加してみます。
追加したのは下のようにメソッドが一つだけあるクラスです。
別プロジェクトから呼び出すためにアクセス修飾子はpublicにしています。
import UIKit public class MyClass { static public func test() { print("MyFramework") } }
ビルドするとMyFramework.frameworkも更新されます。
Finderで見るとSwift用のヘッダーが作られている事が分かります。
frameworkを使ってみる
まずはプロジェクトにライブラリを追加します。
追加はXcodeのFrameworks, Libraries, and Embedded Content
にframeworkファイルをドラッグするだけです。
以下のように書くことでMyClassのメソッドを使う事ができます。
import UIKit import MyFramework class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() MyClass.test() } }
Frameworkがプロジェクト外に配置されている場合、Framework Search Pathsにライブラリのパスを追加する必要があります。
frameworkのユニバーサル対応
上記の方法では、シミュレータか実機のどちらかでだけ動くライブラリのみ作れます。
これを両方の環境で動くように修正してみようと思います。
修正は下URLを参考にしました。
Swiftでフレームワークを作成する(第1回) | GMOアドパートナーズグループ TECH BLOG byGMO
まずは両対応ライブラリをつくためのターゲットを作成します。
XcodeのFile → New → Targetからターゲットを選んで、その中のAggregateを選びます。
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の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") ) } } }