Xcodeの[プロジェクト名].entitlementsというファイルについて調べてみる
Xcodeで自動的に作られる[プロジェクト名].entitlementsについて調べてみました。
[プロジェクト名].entitlementsとは
entitlementsとはプッシュ通知やApp Groupsなど、プロジェクトのCapabilityでの設定に関するファイルです。
Capabilityで通知の追加やApp Groupsのcontainer追加時に自動更新されます。
ファイルをXcodeで開くと下のようになっています。
App Groupsのコンテナ名やプッシュ通知の環境などが設定されています。
設定可能なキーは下のドキュメントで確認する事が可能です。
https://developer.apple.com/documentation/bundleresources/entitlements
entitlementsファイルはプロジェクト設定でパス指定する事でDebugとReleaseのファイルを分けることも可能です。
Realm Swift + SwiftUIでテーブル表示・編集・削除
Realm SwiftとSwiftUIを使ったテーブル実装を試してみました。
注) 2020/04/26時点でRealmはSwiftUIを公式サポートしていません。
対応状況は下Issueを参考にしてみて下さい。
表示
プロジェクトを作ったらCocoaPodsでRealmSwiftを追加します。
platform :ios, '13.0' target 'MyApp' do use_frameworks! pod 'RealmSwift' end
Realmの追加が終わったらEntityを作成します。
定義は下の通りです。
import UIKit import RealmSwift class MyModel: Object { @objc dynamic var id = UUID().uuidString @objc dynamic var title = "" }
次はこのデータをテーブル表示します。
後々のことを考えて、データ一覧はViewModelに入れています。
import SwiftUI import RealmSwift struct ContentView: View { @ObservedObject var model = ContentViewModel() var body: some View { List { ForEach(model.myModels, id: \.id) { myModel in Button(action: { }) { Text("\(myModel.title)") } } } } } class ContentViewModel: ObservableObject { @Published var myModels: [MyModel] = (try? Realm().objects(MyModel.self).map { $0 }) ?? [] }
テストデータを3件作ってから実行すると下のようになります。
日付をそのままtitleに入れたので下のような表示になっています。
編集
次はデータの更新をします。
Cellをタップしたら現在時刻をtitleにセットするような実装にしたいと思います。
まずはViewModeを以下のように修正します。
ここではRealmのobserveを使って更新を受け取るように変更しています。
class ContentViewModel: ObservableObject { private var token: NotificationToken? private var myModelResults = try? Realm().objects(MyModel.self) @Published var myModels: [MyModel] = [] init() { token = myModelResults?.observe { [weak self] _ in self?.myModels = self?.myModelResults?.map { $0 } ?? [] } } deinit { token?.invalidate() } }
続けてView側も修正します。
struct ContentView: View { @ObservedObject var model = ContentViewModel() var body: some View { List { ForEach(model.myModels, id: \.id) { myModel in Button(action: { try? Realm().write { myModel.title = "\(Date())" } }) { Text("\(myModel.title)") } } } } }
これで、ボタンをタップする度にtitleが更新されるようになりました。
追加
次はデータの追加を実装します。
ナビゲーションバーの追加ボタンを押したらデータが増えるような仕様にしたいと思います。
Viewを下のように変更します。
追加したのはnavigationBarItems以下の処理です。
struct ContentView: View { @ObservedObject var model = ContentViewModel() var body: some View { NavigationView { List { ForEach(model.myModels, id: \.id) { myModel in Button(action: { try? Realm().write { myModel.title = "\(Date())" } }) { Text("\(myModel.title)") } } }.navigationBarItems(trailing: Button(action: { let myModel = MyModel() myModel.title = "\(Date())" let realm = try? Realm() try? realm?.write { realm?.add(myModel) } }) { Text("Add") }) } } }
これでデータの追加ができるようになりました。
削除
最後に削除処理を実装します。
削除処理は下のようにonDeleteを付ける事で実現できます。
struct ContentView: View { @ObservedObject var model = ContentViewModel() var body: some View { NavigationView { List { ForEach(model.myModels, id: \.id) { myModel in Button(action: { try? Realm().write { myModel.title = "\(Date())" } }) { Text("\(myModel.title)") } }.onDelete { indexSet in if let index = indexSet.first { let realm = try? Realm() try? realm?.write { realm?.delete(self.model.myModels[index]) } } } }.navigationBarItems(trailing: Button(action: { let myModel = MyModel() myModel.title = "\(Date())" let realm = try? Realm() try? realm?.write { realm?.add(myModel) } }) { Text("Add") }) } } }
実行すると削除ボタンが出るようになります。
ただ、削除しようとすると以下のようなエラーが出てしまいます。
Terminating app due to uncaught exception 'RLMException', reason: 'Object has been deleted or invalidated.'
これはデータを削除した後もSwiftUIが削除済みデータにアクセスするからです。
今回はcellの対になるViewModelを作る事で対応しました。
ViewModelの定義は下の通りです。
struct ContentViewCellModel { let id: String let title: String }
View全体のViewModelもContentViewCellModelの配列を持つようにします。
class ContentViewModel: ObservableObject { private var token: NotificationToken? private var myModelResults = try? Realm().objects(MyModel.self) @Published var cellModels: [ContentViewCellModel] = [] init() { token = myModelResults?.observe { [weak self] _ in self?.cellModels = self?.myModelResults?.map { ContentViewCellModel(id: $0.id, title: $0.title) } ?? [] } } deinit { token?.invalidate() } }
Viewもこれに合わせて変更します。
struct ContentView: View { @ObservedObject var model = ContentViewModel() var body: some View { NavigationView { List { ForEach(model.cellModels, id: \.id) { cellModel in Button(action: { let myModel = try? Realm().objects(MyModel.self).filter("id = %@", cellModel.id).first try? Realm().write { myModel?.title = "\(Date())" } }) { Text("\(cellModel.title)") } }.onDelete { indexSet in let realm = try? Realm() if let index = indexSet.first, let myModel = realm?.objects(MyModel.self).filter("id = %@", self.model.cellModels[index].id).first { try? realm?.write { realm?.delete(myModel) } } } }.navigationBarItems(trailing: Button(action: { let myModel = MyModel() myModel.title = "\(Date())" let realm = try? Realm() try? realm?.write { realm?.add(myModel) } }) { Text("Add") }) } } }
これで削除しても落ちなくなりました。
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 } }