- 投稿日:2020-05-19T23:49:49+09:00
PodsディレクトリをGit管理するならBitriseのキャッシュからも外そう
はじめに
BitriseでiOSアプリのビルドを実行した時に、Build Phasesで行われるCocoaPodsのチェックで以下のようなエラーとなるケースがありました。結論としてはBitriseのキャッシュの影響でしたが、そのエラーとなるケースと解決策についてまとめます。
error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.前提条件
このエラーが起きるのに関係していた前提条件はこちら。
- パッケージマネージャーにCocoaPodsとCarthateを併用している
- CocoaPodsの成果物であるPodsディレクトリもGit管理対象にしている
- Carthageの成果物であるCarthageディレクトリはGit管理対象外にしている
- Carthageの成果物をキャッシュするために Bitrise.io Cache Pull/Push ステップを使用している
- キャッシュ対象としてCarthageディレクトリのみを明示
なぜエラーになったのか
特に指定はしていないのですが 暗黙的にPodsディレクトリがキャッシュ対象 になっていました。つまり、Git管理下にあるPodsディレクトリはキャッシュにより上書きされてしまうことになり、Pods/Manifest.lockが前回実行時のものとなってしまうのです。結果、CocoaPodsのライブラリを更新したプッシュの後に、最初に書いたとおりのエラー(Podfile.lockとManigest.lockが一致しない)になっていたのでした。
解決策
Bitrise.io Cache Push ステップの bitrise_cache_exclude_paths にPodsディレクトリパスを指定してキャッシュ対象にならないようにしましょう。
bitrise_cache_exclude_pathsのデフォルト値は
$BITRISE_CACHE_EXCLUDE_PATHS
になっているので、Env Vars に BITRISE_CACHE_EXCLUDE_PATHS を定義してPodsディレクトリのパスを指しておけば、すべてのワークフローで有効になりますね。
- 投稿日:2020-05-19T23:49:49+09:00
PodsディレクトリをGit管理するならBitriseのキャッシュ対象外にしよう
はじめに
BitriseでiOSアプリのビルドを実行した時に、Build Phasesで行われるCocoaPodsのチェックで以下のようなエラーとなるケースがありました。結論としてはBitriseのキャッシュの影響でしたが、そのエラーとなるケースと解決策についてまとめます。
error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.前提条件
このエラーが起きるのに関係していた前提条件はこちら。
- パッケージマネージャーにCocoaPodsとCarthateを併用している
- CocoaPodsの成果物であるPodsディレクトリもGit管理対象にしている
- Carthageの成果物であるCarthageディレクトリはGit管理対象外にしている
- Carthageの成果物をキャッシュするために Bitrise.io Cache Pull/Push ステップを使用している
- キャッシュ対象としてCarthageディレクトリのみを明示
なぜエラーになったのか
特に指定はしていないのですが 暗黙的にPodsディレクトリがキャッシュ対象 になっていました。つまり、Git管理下にあるPodsディレクトリはキャッシュにより上書きされてしまうことになり、Pods/Manifest.lockが前回実行時のものとなってしまうのです。結果、CocoaPodsのライブラリを更新したプッシュの後に、最初に書いたとおりのエラー(Podfile.lockとManigest.lockが一致しない)になっていたのでした。
解決策
Bitrise.io Cache Push ステップの bitrise_cache_exclude_paths にPodsディレクトリパスを指定してキャッシュ対象にならないようにしましょう。
bitrise_cache_exclude_pathsのデフォルト値は
$BITRISE_CACHE_EXCLUDE_PATHS
になっているので、Env Vars に BITRISE_CACHE_EXCLUDE_PATHS を定義してPodsディレクトリのパスを指しておけば、すべてのワークフローで有効になりますね。
- 投稿日:2020-05-19T23:15:25+09:00
IonicでfcmのrequestPushPermissionIOS()を呼び出すのに手こずった話
IonicのfcmでiOSにPUSH通知を送りたい
Ionicを利用してAndroid/iOSのアプリを作っていたときに困った問題。
iOSにPUSH通知が受け取れない...
AndroidではPUSH通知は受け取れている。
PUSH通知のための証明書は用意した。
pod install
やらなんやらの問題も解決して、ビルドも成功している。
なのになぜ動かない...。~しばらくして~
なるほど。
FCMプラグインのgithubを見ると
https://github.com/andrehtissot/cordova-plugin-fcm-with-dependecy-updatedOn iOS, first run doesn't automatically request Push permission.
The permission, as it is still required, may now be requested from javascript at any moment by executing:
//FCMPlugin.requestPushPermissionIOS( successCallback(), errorCallback(err) );
FCMPlugin.requestPushPermissionIOS();
とある。
ああそうか、iOSではPUSH通知の許可をユーザーに要求しないといけないのか。じゃあIonicでこう書けばいいんだな。
app.component.tsthis.fcm.requestPushPermissionIOS();結果:
requestPushPermissionIOS() is not a function.
おかしいな、github見る限りはメソッドとして定義されているのに...?
pluginのソースコードを見てもちゃんと定義されているぞ...?なんでだ...?
~またしばらくして~
あれ?
@ionic-native/fcm/ngx
の中身のコードだと、requestPushPermissionIOS()が定義されてない...だと...?@ionic-native/fcm/ngx/index.d.tsexport declare class FCM extends IonicNativePlugin { /** * Gets ios device's current APNS token * * @returns {Promise<string>} Returns a Promise that resolves with the APNS token */ getAPNSToken(): Promise<string>; /** * Gets device's current registration id * * @returns {Promise<string>} Returns a Promise that resolves with the registration id token */ getToken(): Promise<string>; /** * Event firing on the token refresh * * @returns {Observable<string>} Returns an Observable that notifies with the change of device's registration id */ onTokenRefresh(): Observable<string>; /** * Subscribes you to a [topic](https://firebase.google.com/docs/notifications/android/console-topics) * * @param {string} topic Topic to be subscribed to * * @returns {Promise<any>} Returns a promise resolving in result of subscribing to a topic */ subscribeToTopic(topic: string): Promise<any>; /** * Unsubscribes you from a [topic](https://firebase.google.com/docs/notifications/android/console-topics) * * @param {string} topic Topic to be unsubscribed from * * @returns {Promise<any>} Returns a promise resolving in result of unsubscribing from a topic */ unsubscribeFromTopic(topic: string): Promise<any>; /** * Checking for permissions on iOS. On android, it always returns `true`. * * @returns {Promise<boolean | null>} Returns a Promise: * - true: push was allowed (or platform is android) * - false: push will not be available * - null: still not answered, recommended checking again later. */ hasPermission(): Promise<boolean | null>; /** * Watch for incoming notifications * * @returns {Observable<any>} returns an object with data from the notification */ onNotification(): Observable<NotificationData>; /** * Removes existing push notifications from the notifications center * * @returns {Promise<void>} */ clearAllNotifications(): void; static ɵfac: ɵngcc0.ɵɵFactoryDef<FCM>; static ɵprov: ɵngcc0.ɵɵInjectableDef<FCM>; }なるほど,
Plugin自体には記述があっても、ionicのほうでは呼び出せないようになっているのか。@ionic-native/fcm/ngxのコードを書き替えて、requestPushPermissionIOS()を無理やり呼び出す
- 先ほどのコードに下記を追加。
@ionic-native/fcm/ngx/index.d.tsrequestPushPermissionIOS(): void;
- 関連してそうなファイルをどんどん書き換える。
@ionic-native/fcm/index.d.tsrequestPushPermissionIOS(): void;@ionic-native/fcm/__ivy_ngcc__/ngx/index.jsvar FCM = /** @class */ (function (_super) { __extends(FCM, _super); function FCM() { return _super !== null && _super.apply(this, arguments) || this; } FCM.prototype.getAPNSToken = function () { return cordova(this, "getAPNSToken", {}, arguments); }; FCM.prototype.getToken = function () { return cordova(this, "getToken", {}, arguments); }; FCM.prototype.onTokenRefresh = function () { return cordova(this, "onTokenRefresh", { "observable": true }, arguments); }; FCM.prototype.subscribeToTopic = function (topic) { return cordova(this, "subscribeToTopic", {}, arguments); }; FCM.prototype.unsubscribeFromTopic = function (topic) { return cordova(this, "unsubscribeFromTopic", {}, arguments); }; FCM.prototype.hasPermission = function () { return cordova(this, "hasPermission", {}, arguments); }; FCM.prototype.onNotification = function () { return cordova(this, "onNotification", { "observable": true, "successIndex": 0, "errorIndex": 2 }, arguments); }; FCM.prototype.clearAllNotifications = function () { return cordova(this, "clearAllNotifications", {}, arguments); }; FCM.prototype.requestPushPermissionIOS = function () { return cordova(this, "requestPushPermissionIOS", {}, arguments); }; // この記述を追加 FCM.pluginName = "FCM"; FCM.plugin = "cordova-plugin-fcm-with-dependecy-updated"; FCM.pluginRef = "FCMPlugin"; FCM.repo = "https://github.com/andrehtissot/cordova-plugin-fcm-with-dependecy-updated"; FCM.platforms = ["Android", "iOS"];@ionic-native/fcm/index.jsvar FCM = /** @class */ (function (_super) { __extends(FCM, _super); function FCM() { return _super !== null && _super.apply(this, arguments) || this; } FCM.prototype.getAPNSToken = function () { return cordova(this, "getAPNSToken", {}, arguments); }; FCM.prototype.getToken = function () { return cordova(this, "getToken", {}, arguments); }; FCM.prototype.onTokenRefresh = function () { return cordova(this, "onTokenRefresh", { "observable": true }, arguments); }; FCM.prototype.subscribeToTopic = function (topic) { return cordova(this, "subscribeToTopic", {}, arguments); }; FCM.prototype.unsubscribeFromTopic = function (topic) { return cordova(this, "unsubscribeFromTopic", {}, arguments); }; FCM.prototype.hasPermission = function () { return cordova(this, "hasPermission", {}, arguments); }; FCM.prototype.onNotification = function () { return cordova(this, "onNotification", { "observable": true, "successIndex": 0, "errorIndex": 2 }, arguments); }; FCM.prototype.clearAllNotifications = function () { return cordova(this, "clearAllNotifications", {}, arguments); }; FCM.prototype.requestPushPermissionIOS = function () { return cordova(this, "requestPushPermissionIOS", {}, arguments); }; // この記述を追加 FCM.pluginName = "FCM"; FCM.plugin = "cordova-plugin-fcm-with-dependecy-updated"; FCM.pluginRef = "FCMPlugin"; FCM.repo = "https://github.com/andrehtissot/cordova-plugin-fcm-with-dependecy-updated"; FCM.platforms = ["Android", "iOS"];ここまでして、ようやく下記コードが正常に動きました。
app.component.tsthis.fcm.requestPushPermissionIOS();めでたしめでたし(?)
おわりに
ionic / cordovaでのアプリ開発はplugin頼みのことが多い+iOS依存の課題で思わぬ工数を使ってしまうことがあるのでなかなか大変ですね。
今回の件も相当な時間を使って調べてようやく解決した問題でした。
(ほかに同じ問題で悩んで解決したような投稿がどこにもなかった)
この記事がどなたかのお役に立てれば幸いです。
- 投稿日:2020-05-19T16:42:26+09:00
【Swift】キーボードの上のボタンの設置方法
①UIViewで作る方法
画面サイズに応じてボタンの配置を変更する必要あり
class ViewController: UIViewController,UITextFieldDelegate { @IBOutlet var textField: UITextField! override func viewDidLoad() { super.viewDidLoad() textField.delegate = self // Make a bar on the keyboard. let keyboardBar = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 45)) keyboardBar.backgroundColor = UIColor.systemOrange // Make an enter button. let enter = UIButton(frame: CGRect(x: 280, y: 5, width: 80, height: 35)) enter.backgroundColor = UIColor.white enter.layer.cornerRadius = 17 enter.setTitle("決定", for: UIControl.State.normal) enter.setTitleColor(UIColor.black, for: UIControl.State.normal) enter.addTarget(self, action: #selector(tapEnter), for: UIControl.Event.touchUpInside) // Make a cancel button. let cancel = UIButton(frame: CGRect(x: 10, y: 5, width: 120, height: 35)) cancel.backgroundColor = UIColor.white cancel.layer.cornerRadius = 17 cancel.setTitle("キャンセル", for: UIControl.State.normal) cancel.setTitleColor(UIColor.black, for: UIControl.State.normal) cancel.addTarget(self, action: #selector(tapCancel), for: UIControl.Event.touchUpInside) // Set the buttons. keyboardBar.addSubview(enter) keyboardBar.addSubview(cancel) // Set the bar. textField.inputAccessoryView = keyboardBar } @objc func tapEnter(_ sender: UIButton){ // タップした時の処理 } @objc func tapCancel(_ sender: UIButton){ // タップした時の処理 } }完成形
②UIToolBarで作る方法
画面サイズに応じてキーボードの上のバーのサイズが変わる
class ViewController: UIViewController,UITextFieldDelegate { @IBOutlet var textField: UITextField! override func viewDidLoad() { super.viewDidLoad() textField.delegate = self let keyboardBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 40)) keyboardBar.barStyle = UIBarStyle.default keyboardBar.sizeToFit() let spacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: self, action: nil) let done = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.done, target: self, action: #selector(self.done)) done.tintColor = .systemOrange let cancel = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.cancel, target: self, action: #selector(self.cancel)) cancel.tintColor = .systemOrange keyboardBar.items = [cancel, spacer, done] textField.inputAccessoryView = keyboardBar } @objc func done(_ sender: UIButton){ // タップした時の処理 } @objc func cancel(_ sender: UIButton){ // タップした時の処理 } }完成形
- 投稿日:2020-05-19T09:27:31+09:00
Swift5.3の変更点まとめ
はじめに
Swift5.3がリリース最終段階に入ったらしいので、変更が入った機能についてまとめた。
変更点は下記ページのProposalを元に確認した。
https://apple.github.io/swift-evolution/#?version=5.3(理解が異なる箇所があればご指摘ください)
Swift5.3
リリースプロセス
スナップショット
変更サマリ
- 品質とパフォーマンスの強化
- Swiftが利用可能できるプラットフォームの拡張
- Windowsサポート
- 対応Linuxディストリビューションの追加
- 0263-string-uninitialized-initializer:初期化されていないバッファを使用するStringのイニシャライザを追加
- 0266-synthesized-comparable-for-enumerations:定義値のみでのenumのComparable準拠
- 0267-where-on-contextually-generic:関数定義に対するGenericsの条件指定
- 0268-didset-semantics:didSetでの不要なゲッター呼び出し見直し
- 0269-implicit-self-explicit-capture:不必要なselfの省略
- 0270-rangeset-and-collection-operations:非連続な位置を参照するコレクション操作の追加
- 0271-package-manager-resources :Swift Package Managerでのリソースファイル対応
- 0272-swiftpm-binary-dependencies:Swift Package Managerでのバイナリパッケージ利用対応
- 0273-swiftpm-conditional-target-dependencies:プラットフォームやConfigurationを条件にしたSPMターゲットの構成
- 0276-multi-pattern-catch-clauses:複数パターンcatch句のサポート
- 0277-float16:Float16の標準ライブラリ追加
- 0278-package-manager-localized-resources:Swift Package Managerリソースのローカライズ対応
- 0279-multiple-trailing-closures:複数TrailingClosure記述方法の改善
- 0280-enum-cases-as-protocol-witnesses:enumのプロトコル準拠判定の改善
- 0281-main-attribute:アプリのエントリーポイントを指定できるmain属性の追加
try! Swift5.3
Dockerのイメージが公式で提供されているので、簡易的にこれを使って試してみた。
https://hub.docker.com/_/swift/$ mkdir swift5.3; cd $_; $ touch main.swift # 好みのエディタで編集 $ docker pull swiftlang/swift:nightly-5.3-bionic $ docker run --rm --privileged -v $(pwd):/swift5.3 -it swiftlang/swift:nightly-5.3-bionic /bin/bash root@a526a6b578b6:/# swift swift5.3/main.swift[SE-0263] Add a String Initializer with Access to Uninitialized Storage
SE-0263:初期化されていないバッファを使用するStringのイニシャライザを追加
新しいイニシャライザを使うことで、UnsafeMutableBufferPointerを自前で取り扱わずに、初期化されていないバッファを使用したStringの生成ができるようになった。
// Swift5.3 let myCocoaString = NSString("The quick brown fox jumps over the lazy dog") as CFString var myString = String(unsafeUninitializedCapacity: CFStringGetMaximumSizeForEncoding(myCocoaString, …)) { buffer in var initializedCount = 0 CFStringGetBytes( myCocoaString, buffer, …, &initializedCount ) return initializedCount } // myString == "The quick brown fox jumps over the lazy dog"[SE-0266] Synthesized
Comparable
conformance forenum
typesSE-0266:定義値のみでのenumのComparable準拠
Swift5.2以前はenumの値同士を比較したい場合、Comparableに準拠させた上で、明示的に比較メソッドを実装しておく必要があった。
// Swift5.2 enum Size: Comparable { case small case medium case large private var comparisonValue: Int { switch self { case .small: return 0 case .medium: return 1 case .large: return 2 } } // この実装が必要 static func < (lhs: Size, rhs: Size) -> Bool { lhs.comparisonValue < rhs.comparisonValue } } Size.small < Size.medium // true Size.medium < Size.large // true Size.large < Size.small // false let sizes: [Size] = [.medium, .small, .large] sizes.sorted() // [Size.small, Size.medium, Size.large]Swift5.3からは該当メソッドを実装しなかった場合、定義順で比較されるようになり、後に定義された方が大きいとみなされるようになる。
// Swift5.3 enum Size: Comparable { case small case medium case large } let sizes: [Size] = [.medium, .small, .large] sizes.sorted() // [Size.small, Size.medium, Size.large]値付きenumの場合も対応可能で、この場合はassociatedValueもComparableに準拠していることが求められる。
同一のcaseだが値が異なるものの場合、比較はペイロードの値を元に行われる。// Swift5.3 enum Category: Comparable { case tops(Size) case bottoms(Size) } let categories: [Category] = [.bottoms(.medium), .tops(.large), .bottoms(.small), .tops(.medium)] categories.sorted() // [Category.tops(Size.medium), Category.tops(Size.large), Category.bottoms(Size.small), Category.bottoms(Size.medium)]所感
これを活用することでボイラープレートコードをより少なくできそう。
一方でカジュアルに定義の順番を変更しちゃったりするとバグる可能性があるので注意はした方がいい。[SE-0267]
where
clauses on contextually generic declarationsSE-0267:関数定義に対するGenericsの条件指定
下記のコード例に関して、「ElementがComparableに適合している場合のみsortedメソッドを実行できる」という挙動を実現したい場合、Swift5.2以前では対象クラスのextensionに対してwhere句で条件指定をする必要があった。
// Swift5.2 struct Stack<Element> { private var elements = [Element]() mutating func push(_ obj: Element) { elements.append(obj) } mutating func pop(_ obj: Element) -> Element? { elements.popLast() } } // extensionでGenericsに関する条件を指定する extension Stack where Element: Comparable { func sorted() -> [Element] { elements.sorted() } }これが、Swift5.3からは直接関数に対して
where句
でGenericsの条件指定を加えられるようにもなった。// Swift5.3 extension Stack { // extensionにではなく、関数にwhere句を付与できる func sorted() -> [Element] where Element: Comparable { elements.sorted() } }これにより、extensionで切り出さなくても直接class, struct内にGenericsの条件を指定した関数を定義することも可能になる。
// Swift5.3 struct Stack<Element> { private var elements = [Element]() mutating func push(_ obj: Element) { elements.append(obj) } mutating func pop(_ obj: Element) -> Element? { elements.popLast() } // extensionで切り出さなくても条件指定をした関数を宣言できる func sorted() -> [Element] where Element: Comparable { elements.sorted() } }所感
Extensionでは区切らない方が関数を意味的にグルーピングしやすい場合などに便利そう。
使い方によっては冗長性を排除できる気がするので、積極的に使っていきたい。[SE-0268] Refine
didSet
SemanticsSE-0268:didSetでの不要なゲッター呼び出し見直し
Swift5.2以前では、プロパティにdidSetを追加した場合、値に変更があると常にoldValueの値が内部的に参照されるようになっていた。
これにより、意図せずパフォーマンスを低下させてしまうコードを実装してしまう恐れがあった。// swift5.2 struct Container { var items: [Int] = .init(repeating: 1, count: 100) { didSet { // 実装としてoldValueを参照していなくても、内部的に参照されていた } } mutating func update() { (0..<items.count).forEach { items[$0] = $0 + 1 } } } var container = Container() container.update() // この時点でitemsのoldValueを参照するために100回配列のコピーが行われるSwift5.3ではこの挙動が改善され、didSetの内部でoldValueを明示的に参照しない限り、内部的にも参照されないようになった。
所感
oldValueが常に参照されていることを意識せずに実装していたので、こういった改善はありがたい。
また、もともとdidSet内でoldValueが呼び出されることに依存しているような処理があった場合は、挙動が変わってしまうことになるため注意が必要(そもそもそういった実装はよくないが)。[SE-0269] Increase availability of implicit
self
in@escaping
closures when reference cycles are unlikely to occurSE-0269:不必要なselfの省略
Swift5.2では循環参照の可能性がない場合でも、
@escaping
クロージャを受け渡す場合には必ずself.
を記述する必要があった。func someFunction(closure: @escaping () -> Void) { closure() }// Swift5.2 struct Main { func run() { someFunction { // 必ずself.で参照する必要がある self.helloWorld() } } func helloWorld() { print("Hello World") } } Main().run()これがSwift5.3では、循環参照の可能性がないと判断できる場合には
self.
を省略することができるようになった。selfが値型の場合は、循環参照の可能性がないため、
self.
をそのまま省略できる。// Swift5.3 // 値型 struct Main { func run() { someFunction { // selfが不要 helloWorld() } } }selfが参照型の場合は、
[self]
をつけるとselfを省略できるようになり、循環参照の可能性がある場合は従来通り[weak self]
を付与してselfを記述して利用する形になる。// Swift5.3 // 参照型 class Main { func run() { // [self]をつければself.が省略可能 someFunction { [self] in helloWorld() } } }つまり、
[weak self]
にするべきか[self]
にするべきか、一度決めてしまえばあとはself.
を書かなくてよくなるため、循環参照を意識させつつも不要なselfは書かなくてよくなることになる。所感
循環参照の可能性を強制的に考慮させる仕組みは残しつつ、不要なselfの記述も省略もできるのでとても良い。
self.の記述がたびたび必要になるSwiftUIのソースコードもきれいになりそう。[SE-0270] Add Collection Operations on Noncontiguous Elements
SE-0270:非連続な位置を参照するコレクション操作の追加
RangeSet
という配列内の非連続な値の位置を表す型が追加される。
また、これをベースに配列を操作するオペレーションも追加される。Collectionに
subranges
メソッドが追加されており、引数として条件のクロージャを渡すことでRangeSetオブジェクトが取得できる(返却されるのは値自体ではない)。// Swift5.3 var numbers = Array(1...6) let multipleOf2 = numbers.subranges(where: { $0.isMultiple(of: 2) }) // RangeSet(1..<2, 3..<4, 5..<6) let multipleOf3 = numbers.subranges(where: { $0.isMultiple(of: 3) }) // RangeSet(2..<3, 5..<6)RangeSetは集合を操作するメソッドも使える。
// 和集合 multipleOf2.union(multipleOf3) // RangeSet(1..<4, 5..<6) // 積集合 multipleOf2.intersection(multipleOf3) // RangeSet(5..<6)元の配列に対して、subscriptでRangeSetを渡すとスライス(DiscontiguousSlice) が取得できる。
これに対してメソッドを呼び出したり、値を取り出したりしてあげることで、対象のデータのみを取り扱うことも可能。let count = numbers[multipleOf2].count // 3 let total = numbers[multipleOf2].reduce(0, +) // 12 let values = multipleOf3.ranges.flatMap { numbers[$0] } // [3, 6] let array = Array(numbers[multipleOf3]) // [3, 6]また、RangeSetに含まれる要素をまとめて指定ポイントに移動する便利関数なども用意されている
numbers.moveSubranges(multipleOf2, to: numbers.endIndex) // 3..<6(移動先のRangeが返却される) numbers // [1, 3, 5, 2, 4, 6]所感
複雑な配列操作をしたいケースなど、これを使うと泥臭い実装をしなくて済む時がありそう。[SE-0271] Package Manager Resources
SE-0271 : Swift Package Managerでのリソースファイル対応
これまでSwift Package Managerでできなかった画像、オーディオ、Storyboard、JSONなどのリソースファイルをバンドルできるようになった。
targetにresourcesが追加されており、これにResourceのインスタンスを渡すことで設定する。
public static func target( name: String, dependencies: [Target.Dependency] = [], path: String? = nil, exclude: [String] = [], sources: [String]? = nil, resources: [Resource]? = nil, // <=== NEW publicHeadersPath: String? = nil, cSettings: [CSetting]? = nil, cxxSettings: [CXXSetting]? = nil, swiftSettings: [SwiftSetting]? = nil, linkerSettings: [LinkerSetting]? = nil ) -> Target/// Represents an individual resource file. public struct Resource { /// Apply the platform-specific rule to the given path. /// /// Matching paths will be processed according to the platform for which this /// target is being built. For example, image files might get optimized when /// building for platforms that support such optimizations. /// /// By default, a file will be copied if there is no specialized processing /// for its file type. /// /// If path is a directory, the rule is applied recursively to each file in the /// directory. public static func process(_ path: String) -> Resource /// Apply the copy rule to the given path. /// /// Matching paths will be copied as-is and will be at the top-level /// in the bundle. The structure is retained for if path is a directory. public static func copy(_ path: String) -> Resource }SPMはBundleのextensionとして
module
を提供するので、これを使って実行時にリソースにアクセスできる。
(このextensionはモジュールごとinternalで提供されるので、各モジュールで実装が干渉することはない。)extension Bundle { static let module: Bundle = { ... }() }// DefaultSettings.plistへのパスを取得する let path = Bundle.module.path(forResource: "DefaultSettings", ofType: "plist") // モジュールに含まれるリソースを利用して画像を取得する let image = UIImage(named: "MyIcon", in: Bundle.module, compatibleWith: UITraitCollection(userInterfaceStyle: .dark))所感
UI系のライブラリなどもをSPMで提供しやすくなってそう。[SE-0272] Package Manager Binary Dependencies
SE-0272:Swift Package Managerでのバイナリパッケージ利用対応
Swift Package Managerでソースコード自体は公開せずにパッケージを提供できるようになる。
これにより、FirebaseSDKなどのクローズドSDKがSPMに対応できるようになる。※ この機能は、最初のフェーズではAppleのプラットフォームに限定し、将来的に拡張しておく模様。
let package = Package( name: "SomePackage", platforms: [ .macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v2), ], products: [ .library(name: "SomePackage", targets: ["SomePackageLib"]) ], targets: [ .binaryTarget( name: "SomePackageLib", url: "https://github.com/some/package/releases/download/1.0.0/SomePackage-1.0.0.zip", checksum: "839F9F30DC13C30795666DD8F6FB77DD0E097B83D06954073E34FE5154481F7A" ), .binaryTarget( name: "SomeLibOnDisk", path: "artifacts/SomeLibOnDisk.zip" ) ] )ソースターゲットとバイナリターゲットを混在させることもできる。
所感
この流れでCocoapods, Carthageで管理しているライブラリもSPMに移行できたら嬉しい。[SE-0273] Package Manager Conditional Target Dependencies
SE-0273:プラットフォームやConfigurationを条件にしたSPMターゲットの構成
以下のように、条件ごとに設定を変えるような対応ができるようになる
- macOS向けとLinux向けでフレームワークを使い分ける
- debugビルドの時のみ追加のフレームワークを組み込む
// swift-tools-version:5.3 import PackageDescription let package = Package( name: "BestPackage", dependencies: [ .package(url: "https://github.com/pureswift/bluetooth", .branch("master")), .package(url: "https://github.com/pureswift/bluetoothlinux", .branch("master")), ], targets: [ .target( name: "BestExecutable", dependencies: [ .product(name: "Bluetooth", condition: .when(platforms: [.macOS])), .product(name: "BluetoothLinux", condition: .when(platforms: [.linux])), .target(name: "DebugHelpers", condition: .when(configuration: .debug)), ] ), .target(name: "DebugHelpers") ] )所感
1つのパッケージでより汎用的に使えるライブラリが提供できて便利そう。[SE-0276] Multi-Pattern Catch Clauses
SE-0276:複数パターンcatch句のサポート
Swift5.2では例外ハンドリング時のcatch句には、単一のパターンとwhere句のみしか設定できなかった。
そのため、各エラー発生時に同一の処理を行いたい場合にも、処理を重複して記述する必要があった。// Swift5.2 enum SampleError: Error { case badRequest(String) case unAuthorized(String) case internalServerError(String) case serviceUnavailable(String) } do { try doSomething() } catch SampleError.badRequest(let message) { handleClientError(with: message) } catch SampleError.unAuthorized(let message) { handleClientError(with: message) } catch SampleError.internalServerError(let message) { handleServerError(with: message) } catch SampleError.serviceUnavailable(let message) { handleServerError(with: message) }Swift5.3ではcatch句の文法が拡張され、パターンをカンマで区切ることでエラーハンドリングの共通化ができるようになった。
// Swift5.3 do { try doSomething() } catch SampleError.badRequest(let message), SampleError.unAuthorized(let message) { handleClientError(with: message) } catch SampleError.internalServerError(let message), SampleError.serviceUnavailable(let message) { handleServerError(with: message) }[SE-0277] Float16
SE-0277:Float16の標準ライブラリ追加
所感
グラフィックスプログラミングと機械学習で一般的に使用されている型のようなので、界隈のひとには嬉しい変更かもしれない。[SE-0278] Package Manager Localized Resources
SE-0278:SPMリソースのローカライズ対応
ローカライズされたリソースを、
<IFTF言語タグ>.lproj
ディレクトリに配置することで、ロケーションに応じたリソースの使い分けができるようになる。例として、以下のようなローカライズされたリソースが存在する場合、Package.swiftで指定する際は
<IFTF言語タグ>.lproj
を除いたパスで指定する。
- Resources/en.lproj/Icon.png
- Resources/fr.lproj/Icon.png
let package = Package( name: "BestPackage", defaultLocalization: "en", targets: [ .target(name: "BestTarget", resources: [ .process("Resources/Icon.png"), ]) ] )またSPMはApple固有プラットフォームのリソース対応のために、
Base.lproj
配下のリソースもBaseInternationalization
を使用するものとして認識する。[SE-0279] Multiple Trailing Closures
SE-0279:複数TrailingClosure記述方法の改善
Swiftでは、末尾の引数にクロージャを渡すコードを、明快で読みやすくできるTrailing Closureという機能が搭載されている。
// Swift5.2 // Trailing Closureを使わない UIView.animate(withDuration: 0.3, animations: { self.view.alpha = 0 }) // Trailing Closureを使うと、末尾のクロージャのラベルを省略した上で記述を外に出すことができる UIView.animate(withDuration: 0.3) { self.view.alpha = 0 }しかし、もし末尾に連続でクロージャが渡されると、それらが何を表すものかが不明瞭になり、ネストも深くなってしまう課題があった。
// Swift5.2 // Trailing Closureを使わない UIView.animate(withDuration: 0.3, animations: { self.view.alpha = 0 }, completion: { _ in self.view.removeFromSuperview() }) // Trailing Closureを使う UIView.animate(withDuration: 0.3, animations: { self.view.alpha = 0 }) { _ in // このクロージャが何なのかが不明瞭 self.view.removeFromSuperview() }そのため、Swift5.2以前では以下のように使い分ける対応をすることが多かった。
- 末尾のクロージャが単一 → Trailing Closureを利用する
- 末尾のクロージャが連続 → Trailing Closureを利用しない
Swift5.3ではこの記述方法に改善が入り、1つめのクロージャには既存と同様の省略記法を適用し、後続するクロージャはラベルを付与した上で表現するといった記述が可能になった。(これまでと同様の記述方法も可能)
// Swift 5.3 UIView.animate(withDuration: 0.3) { self.view.alpha = 0 } completion: { _ in self.view.removeFromSuperview() }所感
関数の定義方法を工夫することで読みやすいコードにできそうな気がする。
メソッド名に対して関連度が高いクロージャを最初に指定して、後続するクロージャは追加の意味合いが強いものを持ってくると読みやすくなりそう。// Swift5.3 func listen(onReceive: () -> Void, onCancel: () -> Void) {} listen { print("onReceive") } onCancel: { print("onCancel") }[SE-0280] Enum cases as protocol witnesses
SE-0280:enumのプロトコル準拠判定の改善
下記のJSONDecodingError定義は、struct, enumでそれぞれ全く同じように呼び出すことができる。
struct JSONDecodingError { static var fileCorrupted: Self {} static func keyNotFound(_ key: String) -> Self {} } enum JSONDecodingError { case fileCorrupted case keyNotFound(_ key: String) } let error1 = JSONDecodingError.fileCorrupted let error2 = JSONDecodingError.keyNotFound("hoge")しかし、これを下記のDecodingErrorプロトコルに準拠させようとすると、Swift5.2の場合ではstructで定義した場合のみプロトコルに準拠していると見なされ、enumの場合は不適合と見なされてしまっていた。
// Swift5.2 protocol DecodingError { static var fileCorrupted: Self { get } static func keyNotFound(_ key: String) -> Self } // o struct JSONDecodingError: DecodingError { ... } // x enum JSONDecodingError: DecodingError { ... }Swift5.3ではこれに改善が入り、以下のルールに合致した場合、プロトコルに適合していると見なされるようになった。
- 値を持たないenumのケースは、列挙型もしくはSelfを保持する静的なget-onlyプロパティと関連付けられる
- 値を持つenumのケースは、associatedValueと同じ値を引数に取り、列挙型もしくはSelfを返す静的関数と関連付けられる
// Swift5.3 protocol DecodingError { static var fileCorrupted: Self { get } static func keyNotFound(_ key: String) -> Self } // OK enum JSONDecodingError: DecodingError { case fileCorrupted case keyNotFound(_ key: String) }[SE-0280]
@main
: Type-Based Program Entry PointsSE-0281:アプリのエントリーポイントを指定できる@main属性の追加
Swiftはmain.swiftを自動的にトップレベルのコードとみなすため、Swift5.2以前では以下のような実装をすることでエントリーポイントを定義していた。
main.swift// Swift5.2 struct MyApp { func run() { print("Running!") } } let app = MyApp() app.run()Swift5.3では、main.swiftを追加していない場合、
@main
属性が付与されたクラスのstatic main関数がエントリーポイントと見なされるようになる。
これはモジュールの任意のソースファイルに付与することができる。app.swift// Swift5.3 @main struct MyApp { static func main() { print("Running!") } }
@main
属性を使用するには下記の制限がある。
- main.swiftが存在するアプリでは
@main
属性は使用できない(既存と同様の動作)- 複数の
@main
属性は付与できない所感
ライブラリの実装者などは、protocol extensionなどでstatic main関数をあらかじめ定義しておけば、利用者側のエントリーポイントに該当のプロトコルの準拠+@main属性の付与をさせるだけで、望んだ動作をさせられるのは良さそう。参考
- 投稿日:2020-05-19T01:21:56+09:00
【iOS】iOSアプリ開発入門~ SwiftとUIの接続編1~
前回はiOSアプリ開発をするためのUIを配置するためのAuto Layoutについて投稿しました。
レイアウト編2:https://qiita.com/euJcIKfcqwnzDui/items/31f6b11afb317275f684今回はいよいよSwiftコードとStoryboardを接続していきます。
とはいっても接続はかなり簡単でおそらくすぐに覚えられるかと思います。プログラムと接続するとどうなるの?
そんなんわかるわ!と思う方は読み飛ばしてください。
当たり前ですがソースコードとUIを結び付けなければアプリとして成り立ちません。
アプリの基本的な流れとしては
1.ユーザ操作を受け付ける
2.操作に合わせた処理をする
3.処理結果をユーザにフィードバックする
という流れになります。UI、つまりユーザインタフェースはその名の通りユーザとPCの仲介役です。
ユーザが関わることはすべてUIを通して行われます。
上の例でいくと1,3がUIの仕事です。
そして実際の処理を行うのがプログラムというわけです。
つまりUI->Swift->UI...という流れを実現するために接続します。ボタンのタップ処理
とりあえず接続してみる
それではさっそく接続してみましょう。
前回から使い続けてるプロジェクトを開き、Main.storyboardを開いてください。まずUIとSwiftコードを紐付けるためにはstoryboardとswiftファイル両方を開く必要があります。
Inspectorのすぐ左にある[+]のようなボタンを押してください。
画面が二分割されます。
Inspectorは邪魔なので一度非表示にしましょう。
右上の青くなっているボタンをクリックすると非表示になります。
もう一度クリックすると再度表示されるのでご心配なく。
Swiftファイルを開きます。どちらでも構いませんが私の画面と合わせるために左側の画面の適当な位置をクリックしてください。
その後ファイル一覧からViewController.swiftを選択します。
ViewController.swiftが開かれましたか?
それでは接続します。
まずstoryboardでボタンを選択します。
controlを押しながらボタンをドラッグします。
ボタンとマウスカーソルの間に青い線が出るのでその先端をclass ViewController: UIViewController {
の1行下まで持っていき離してください。
持っていったところあたりから下図のようなポップアップが表示されればOKです。
ポップアップを以下のように変更し、[Connect]ボタンを選択します。
・Connection:Action
・Name:tapButton
以下のように
@IBAction func tapButton(_ sender: Any) {
}
が追加されていれば成功です。
これでボタンのタップ処理が接続されました。
実際に接続されているか確かめてみましょう。tapButtonの{}の中にprint("Hello World")と入力してください。
ViewController.swift@IBAction func tapButton(_ sender: Any) { print("Hello World") }シミュレータで実行し、ボタンをタップしてみてください。
Xcodeの下の方の画面に「Hello World」と表示されました。
またボタンを押せば押した回数分表示されるので試してみてください。
※Xcodeの下にそんな画面がないという方はXcodeの右上にある3つのボタンの真ん中をクリックしてください。
内容の解説
まずボタンをマウスで引っ張っていったときに表示されたポップアップについて。
このポップアップでは接続方法の設定を行っています。
少しプログラミングのオブジェクト指向の用語が出てきますが1つずつ見ていきましょう。
- Connection
- 接続の方法を指定します。後述しますが接続には種類があり、イベントを接続する方法とUI自体を接続する方法があります。今回はボタンに対するイベントを接続したかったので
Action
を選択しました。- Object
- ここはあまり変更することはありませんが接続先のオブジェクトを指定します。
storyboardを確認してください。UIの階層構造の中にView Controller
というUIがあるかと思います。これのことです。
実はこのView ControllerとViewController.swiftはプロジェクト作成時に自動的に紐付けられています。
とりあえずはViewController.swiftの中に処理が書かれるんだなぁと思ってください
(※正確にはViewControllerというクラスから作られたオブジェクトに紐付いています)- Name
- イベントが発生したときに呼ばれるメソッドの名前を指定します。メソッドとは処理そのものというように認識してください。
今回tapButton
と名付けましたが、そのままボタンがタップされたときの処理というような意味で名付けています。- Type
- 1つ目は引数の型を指定します。型とは種類のようなものです。
引数とは@IBAction func tapButton(_ sender: Any)
とありますがsender
のことです。senderの実態は操作されたUI自体、今回でいうとボタン自体です。タップしたときにボタン自体も一緒に渡しますよという感じです。
TypeをAny
で指定したのでsender: Any
senderはAny型ですよとなっています。Anyはボタンに限らずどんな種類になるかわかりませんというようなイメージです。そんなものがあるのかーくらいでいいです。- 2つ目はイベントの詳細を指定します。一口にボタンのイベントといっても「ボタンをタップした」、「ボタンをタップしたけどキャンセルした」など様々なイベントが考えられます。今回指定した[Touch Up Inside]はボタンをタップしたというイベントです
- 3つ目は引数の種類です。
sender
は先述したようにボタン自体です。他にもイベントの情報、例えばどの座標がタップされたというようなものも指定できます。以上が接続方法それぞれの項目の内容です。
今はわからないことが多いかもしれませんが、もう少し勉強を進めて見直すとわかってくるかと思うのでので今はイメージを掴むだけでもいいかと思います。次は接続された後の解説です。
@IBAction func tapButton(_ sender: Any)
の左側を見ると●のようなマークが出ています。
これは「このメソッドは何かのUIと紐付いている」という意味です。先程ボタンと紐付けたため表示されました。
Interface Builderと紐付いているものにはこれが付きます。
また@IBAction
とあります。これは「このメソッドはInterface Builder(IB)からActionとして紐付けられるもの」というような意味です。Interface Builderから紐付けられないメソッドには付きません。
今度はstoryboardを見てみます。
ボタンを選択し右クリック or 二本指でクリックしてください。小さいウィンドウが表示されます。
[Sent Events]のなかに[Touch Up Inside]があり、右側に[View Controller]、[tapButton:]が表示されています。
これはソースコードと紐付いている証で「Touch Up InsideしたときView ControllerのtapButtonが呼び出されますよ」ということです。
View Controllerの右の[✕]ボタンをクリックすると接続が解除されます。
また再度tapButtonまでボタンをドラッグすると紐付けることができます。
備考として[Sent Events]のなかに[Touch Up Inside]以外にも色々あります。これが先述したイベントの詳細です。
ViewController.swiftに戻りましょう。
接続後print("Hello World")
というコードを書きました。
これはデバッグコンソールに「Hello World」と表示するという処理です。
デバッグコンソールは先程「Hello World」と表示された場所です。
print()はデバッグコンソールに文字列を表示するための関数で開発の際よく使います。
(関数とはメソッドと似たようなもので処理の塊というイメージです)ViewController.swift@IBAction func tapButton(_ sender: Any) { print("Hello World") }ということで
ボタンをタップ → tapButtonが呼ばれる → print()が呼ばれる
という流れで処理が実行されたというわけです。最後に
今回はボタンタップ処理のの仕方について説明しました。
プログラミングに初めて触るという方にとっては聞いたことがない単語がいくつか出てきたかと思いますが、実際に行った作業としては単純なものだったのではないでしょうか?
また簡単な処理ではありますが少しずつアプリとして動くようになってきているかと思います。
この調子で着実に進めましょう。プログラミングの基本については近々Swiftベースでまとめます。
次回はボタンタップからのUI更新の方法を紹介しようと思います。
SwiftとUIの接続編2:https://qiita.com/euJcIKfcqwnzDui/items/93f010b989a4d333f0b9本連載ではプログラミング未経験からiOSアプリ開発が行えるようになることを目的としています。
今までの投稿をまとめていますのでこちらもご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684