- 投稿日:2020-02-04T23:43:38+09:00
SwiftUIとRealmの連携方法
はじめに
SwiftUIとRealmを連携してライブアップデートを効かせる方法を記載します。
要点
- Realmオブジェクトはライブアップデート機能を備えており、該当のインスタンスは常に最新の値で更新されている
- Realmオブジェクトを
@Publishedで保持したとしても、値に変更があった際に自動で更新されるようにはならない- observeメソッドを利用してSwiftUIに再レンダリングの指示を与えることで、常に最新の値がUIに反映されるようにすることができる
Realmのインストール
RealmはSwiftPackageManagerに対応しているので、今回はこれを利用してインストールします
Entityの用意
- まずは準備としてRealmで保持するデータの型を定義します。
- 今回は動作検証用として、以下の挙動を行うようにしています
- setUpメソッドを実行すると2秒ごとにデータを一度全て削除し、ランダムな名称が設定されたItemデータが再セットされる
import RealmSwift class ItemEntity: Object, Identifiable { @objc dynamic var id: String = "" @objc dynamic var name: String = "" override class func primaryKey() -> String? { "id" } override class func indexedProperties() -> [String] { ["id"] } private static var realm = try! Realm() static func setUp() { Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { (timer) in try! realm.write { realm.deleteAll() realm.add(createFixture(), update: .modified) } } } static func all() -> Results<ItemEntity> { realm.objects(ItemEntity.self) } private static func createFixture() -> [ItemEntity] { (0..<10) .map { _ in (0..<1000).randomElement()! } .map { number -> ItemEntity in let item = ItemEntity() item.id = "\(number)" item.name = "item\(number)" return item } } }ObservableObjectを利用する
- Entityを用意したらViewModel(ObservableObject)を介してRealmのデータを取得するようにします
- しかし、このコードは最初にフェッチしたデータしか表示されません。
@Publishedで変更が監視されるのはその変数の値自体が変わったタイミングなので、RealmのResultsインスタンスの内部状態に変更があっても、Viewの再レンダリングが行われないためです。struct ContentView: View { @ObservedObject private var viewModel = ViewModel() var body: some View { List { // DBに変更があってもSwiftUIはその変更を検知できず、UIは初期表示から更新されない ForEach(viewModel.itemEntities) { itemEntity in Text(itemEntity.name) } }.onAppear { ItemEntity.setUp() } } } class ViewModel: ObservableObject { @Published var itemEntities: Results<ItemEntity> = ItemEntity.all() }対応方法
- それではDBのデータに変更があった際に、Viewを再レンダリングさせるにはどうすれば良いでしょうか
- Realmのオブジェクトが持つ
observeメソッドを利用することでこれを実現することができます- このメソッドを利用すると、DBのデータに変更が起きるたびに受け渡したクロージャが実行されるので、そのタイミングをフックしてObservableObjectの状態を更新してあげれば期待した挙動をさせることができます
entity.observe { (change) in switch change { case let .initial(results): // do something case let .update(results, deletions, insertions, modifications): // do something case let .error(error): // do something } }struct ContentView: View { @ObservedObject private var viewModel = ViewModel() var body: some View { List { ForEach(viewModel.itemEntities) { (itemEntity: ItemEntity) in if itemEntity.isInvalidated { EmptyView() } else { Text(itemEntity.name) } } }.onAppear { ItemEntity.setUp() } } } class ViewModel: ObservableObject { @Published var itemEntities: Results<ItemEntity> = ItemEntity.all() private var notificationTokens: [NotificationToken] = [] init() { // DBに変更があったタイミングでitemEntitiesの変数に値を入れ直す notificationTokens.append(itemEntities.observe { change in switch change { case let .initial(results): self.itemEntities = results case let .update(results, _, _, _): self.itemEntities = results case let .error(error): print(error.localizedDescription) } }) } deinit { notificationTokens.forEach { $0.invalidate() } } }※ もしくは、
@Publishedを利用せずに手動で状態の変更を通知する方法でも同様の挙動を実現できます。
この方法を利用すれば、プロパティの値を置換することなく再レンダリングをさせることができます。import Combine class Store: ObservableObject { var objectWillChange: ObservableObjectPublisher = .init() private(set) var itemEntities: Results<ItemEntity> = ItemEntity.all() private var notificationTokens: [NotificationToken] = [] init() { notificationTokens.append(itemEntities.observe { _ in // SwiftUIに再レンダリングが必要なことを伝える self.objectWillChange.send() }) } // ... }この実装によって、自動更新の挙動を実現することができました。(gif参照)
(おまけ)EnvironmentObjectを利用する
- 上記の例はObservableObjectを利用しましたが、EnvironmentObjectとしてデータを管理したいというケースもあると思います
- この変更はとても簡単で以下の修正を入れるだけです
- (ViewModelをStoreにリネーム)
- Viewが保持するインスタンスを
@ObservedObjectから@EnvironmentObjectに変更- Viewの初期化時に
environmentObjectModifierでデータを保持するオブジェクトを受け渡す- これによりEnvironmentObjectがバインドされたViewは、DBに更新があった時に全て自動的に更新されるようになります
- class ViewModel: ObservableObject { + class Store: ObservableObject { // ... }struct ContentView: View { - @ObservedObject(initialValue: ViewModel()) private var viewModel: ViewModel + @EnvironmentObject private var store: Storelet contentView = ContentView() + .environmentObject(Store())サンプルコード
- 投稿日:2020-02-04T23:43:38+09:00
SwiftUIとRealmを連携してUIを自動更新する
はじめに
SwiftUIとRealmを連携してライブアップデートを効かせる方法を記載します。
要点
- Realmオブジェクトはライブアップデート機能を備えており、該当のインスタンスは常に最新の値で更新されている
- Realmオブジェクトを
@Publishedで保持したとしても、値に変更があった際に自動で更新されるようにはならない- observeメソッドを利用してSwiftUIに再レンダリングの指示を与えることで、常に最新の値がUIに反映されるようにすることができる
Realmのインストール
RealmはSwiftPackageManagerに対応しているので、今回はこれを利用してインストールします
Entityの用意
- まずは準備としてRealmで保持するデータの型を定義します。
- 今回は動作検証用として、以下の挙動を行うようにしています
- setUpメソッドを実行すると2秒ごとにデータを一度全て削除し、ランダムな名称が設定されたItemデータが再セットされる
import RealmSwift class ItemEntity: Object, Identifiable { @objc dynamic var id: String = "" @objc dynamic var name: String = "" override class func primaryKey() -> String? { "id" } override class func indexedProperties() -> [String] { ["id"] } private static var realm = try! Realm() static func setUp() { Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { (timer) in try! realm.write { realm.deleteAll() realm.add(createFixture(), update: .modified) } } } static func all() -> Results<ItemEntity> { realm.objects(ItemEntity.self) } private static func createFixture() -> [ItemEntity] { (0..<10) .map { _ in (0..<1000).randomElement()! } .map { number -> ItemEntity in let item = ItemEntity() item.id = "\(number)" item.name = "item\(number)" return item } } }ObservableObjectを利用する
- Entityを用意したらViewModel(ObservableObject)を介してRealmのデータを取得するようにします
- しかし、このコードは最初にフェッチしたデータしか表示されません。
@Publishedで変更が監視されるのはその変数の値自体が変わったタイミングなので、RealmのResultsインスタンスの内部状態に変更があっても、Viewの再レンダリングが行われないためです。struct ContentView: View { @ObservedObject private var viewModel = ViewModel() var body: some View { List { // DBに変更があってもSwiftUIはその変更を検知できず、UIは初期表示から更新されない ForEach(viewModel.itemEntities) { itemEntity in Text(itemEntity.name) } }.onAppear { ItemEntity.setUp() } } } class ViewModel: ObservableObject { @Published var itemEntities: Results<ItemEntity> = ItemEntity.all() }対応方法
- それではDBのデータに変更があった際に、Viewを再レンダリングさせるにはどうすれば良いでしょうか
- Realmのオブジェクトが持つ
observeメソッドを利用することでこれを実現することができます- このメソッドを利用すると、DBのデータに変更が起きるたびに受け渡したクロージャが実行されるので、そのタイミングをフックしてObservableObjectの状態を更新してあげれば期待した挙動をさせることができます
entity.observe { (change) in switch change { case let .initial(results): // do something case let .update(results, deletions, insertions, modifications): // do something case let .error(error): // do something } }struct ContentView: View { @ObservedObject private var viewModel = ViewModel() var body: some View { List { ForEach(viewModel.itemEntities) { (itemEntity: ItemEntity) in if itemEntity.isInvalidated { EmptyView() } else { Text(itemEntity.name) } } }.onAppear { ItemEntity.setUp() } } } class ViewModel: ObservableObject { @Published var itemEntities: Results<ItemEntity> = ItemEntity.all() private var notificationTokens: [NotificationToken] = [] init() { // DBに変更があったタイミングでitemEntitiesの変数に値を入れ直す notificationTokens.append(itemEntities.observe { change in switch change { case let .initial(results): self.itemEntities = results case let .update(results, _, _, _): self.itemEntities = results case let .error(error): print(error.localizedDescription) } }) } deinit { notificationTokens.forEach { $0.invalidate() } } }※ もしくは、
@Publishedを利用せずに手動で状態の変更を通知する方法でも同様の挙動を実現できます。
この方法を利用すれば、プロパティの値を置換することなく再レンダリングをさせることができます。import Combine class Store: ObservableObject { var objectWillChange: ObservableObjectPublisher = .init() private(set) var itemEntities: Results<ItemEntity> = ItemEntity.all() private var notificationTokens: [NotificationToken] = [] init() { notificationTokens.append(itemEntities.observe { _ in // SwiftUIに再レンダリングが必要なことを伝える self.objectWillChange.send() }) } // ... }この実装によって、自動更新の挙動を実現することができました。(gif参照)
(おまけ)EnvironmentObjectを利用する
- 上記の例はObservableObjectを利用しましたが、EnvironmentObjectとしてデータを管理したいというケースもあると思います
- この変更はとても簡単で以下の修正を入れるだけです
- (ViewModelをStoreにリネーム)
- Viewが保持するインスタンスを
@ObservedObjectから@EnvironmentObjectに変更- Viewの初期化時に
environmentObjectModifierでデータを保持するオブジェクトを受け渡す- これによりEnvironmentObjectがバインドされたViewは、DBに更新があった時に全て自動的に更新されるようになります
- class ViewModel: ObservableObject { + class Store: ObservableObject { // ... }struct ContentView: View { - @ObservedObject(initialValue: ViewModel()) private var viewModel: ViewModel + @EnvironmentObject private var store: Storelet contentView = ContentView() + .environmentObject(Store())サンプルコード
- 投稿日:2020-02-04T23:06:43+09:00
iOSとmacのunityのnative pluginを作る
iOSとmacのunityのnative pluginを作る
unity editorで使えるmacOS用のbundleとiOS用のFrameworkのnative pluginを作成する手順です。
Bluetoothや動画作成など、Unityに実装されていない機能をiOS, macOSの機能を使うことで実現します。
UnityPluginXcodeTemplateをベースに作成します。
UnityPluginXcodeTemplateを使うことで、unityのnative pluginを作成するために必要なxcodeの複雑な設定の必要がなくなり、editorで動作確認できるbundleと実機用のframeworkが同時に開発できます。今回作成するnative pluginの目的
- unity(C#)には加算演算が実装されていない(設定)です。そこで、swiftの加算演算子を使って加算演算を実現するnative pluginを実装します。
- 手順に着目しているので、今回は加算演算のみの実装ですが、同じ手順でbluetoothなどのnative pluginを作成しているので、参考になれば嬉しいです。
- iOS, macOSで使えるbluetoothのnative plugin: https://github.com/fuziki/UnityCoreBluetooth
作成するC#クラス
- NativeSuperCalculatorクラスです。
- 加算演算ができるAddメソッドを持っています。
NativeSuperCalculator.cspublic class NativeSuperCalculator { public static int Add(int l, int r); }作成するSuperCalculator Xcodeプロジェクト
5つのターゲットがあります
Target Description SuperCalculator.framework iOS BuildとExample_iOSでの動作確認に使用するframework SuperCalculator_macOS.framework macOS_Exampleで動作確認に使用するframework SuperCalculator_bundle.bundle macのUnity Editorで動作確認に使用するbundle Example_iOS 作成した実装をUnityに組み込む前にiOSで動作確認するために使用する Example_macOS 作成した実装をUnityに組み込む前にmacOSで動作確認するために使用する 作成するswiftクラス
SuperCalculatorクラスです。
加算演算ができるaddメソッドを持っています。SuperCalculator.swift@objcMembers public class SuperCalculator: NSObject { public static func add(l: Int, r: Int) -> Int { return l + r //swiftにしか加算演算子がない(設定) } }Xcode プロジェクトを作成する
1. UnityPluginXcodeTemplateをcloneしてrename
git clone https://github.com/fuziki/UnityPluginXcodeTemplate mv UnityPluginXcodeTemplate/ SuperCalculatorPlugin/ cd SuperCalculatorPlugin2. setting.ymlを編集
setting.ymlPROJECT_NAME: SuperCalculatorPlugin FRAMEWORK_IOS: SuperCalculator FRAMEWORK_MACOS: SuperCalculator_macOS BUNDLE_MACOS: SuperCalculator_bundle EXAMPLE_IOS: Example_iOS EXAMPLE_MACOS: Example_macOS3. xcodeprojを作成する
xcodegenを使用して、Xcodeディレクトリ下にxcodeprojを作成します。
make setup open Xcode/SuperCalculatorPlugin.xcodeproj/Swiftで機能を実装する
1. SuperCalculator.swiftの実装
- xcodeプロジェクトのNavigator AreaからLibrary/Sourcesを右クリック→ new file...を選択
- swift fileを選択し、next
ファイル名はSuperCalculatorを指定、targetはSuperCalculator, SuperCalculator_macOS, SuperCalculator_bundleを選択してcreate
SuperCalculatorクラスを実装する
objective-cから呼び出すために@objcMembers属性をつけます。
SuperCalculator.swift@objcMembers public class SuperCalculator: NSObject { public static func add(l: Int, r: Int) -> Int { return l + r//swiftにしか加算演算子がない(設定) } }2. Unityからの呼び出し部分の実装
- NativePlugin.hの実装
- Unityのnative pluginはcの呼び出しなので、cのインタフェースを作成します。
- superCalculator_addを追加します。
NativePlugin.h#ifdef __cplusplus extern "C" { #endif int add_one(int num); + int superCalculator_add(int l, int r); //追加する #ifdef __cplusplus } #endif
- NativePlugin.mmの実装
- superCalculator_addが呼ばれたら、swiftのSuperCalculator.addを呼び出して計算します。
NativePlugin.mmint superCalculator_add(int l, int r) { return (int)[SuperCalculator addWithL:l r:r]; }3. macOS, iOSでの動作確認(optional)
- viewDidLoadでsuperCalculator_addを呼び出して、正しく動作することを確認する。
※ iOSは実機のみサポートです。ViewController.swiftoverride func viewDidLoad() { super viewDidLoad() // Do any additional setup after loading the view. let res = superCalculator_add(32, 42) print("res: \(res)") }UnityのNative Pluginとして導入する
1. iOS向けframeworkのビルド
2. macOSのunity editor向けbundleのビルド
3. 成果物の確認
- 4つ作成されているはず
- Xcode/Out/Assets/SuperCalculatorPlugin/Plugins/iOS/SuperCalculator.framework.meta
- Xcode/Out/Assets/SuperCalculatorPlugin/Plugins/iOS/SuperCalculator.framework
- Xcode/Out/Assets/SuperCalculatorPlugin/Plugins/macOS/SuperCalculator_bundle_xxxxxxx.bundle
- Xcode/Out/Assets/SuperCalculatorPlugin/Scripts/SuperCalculator_bundle.cs
4. 成果物をUnityにコピーする
- Xcode/Out/Assets/SuperCalculatorPlugin/. を Unity/LibraryUser/Assets/Plugins/. にコピーする
mkdir Unity/LibraryUser/Assets/Plugins/SuperCalculatorPlugin cp -r Xcode/Out/Assets/SuperCalculatorPlugin/. Unity/LibraryUser/Assets/Plugins/SuperCalculatorPlugin/.Unityからnative pluginを使う実装
1. NativeSuperCalculator.csを作成し、NativeSuperCalculatorクラスを実装する
NativeSuperCalculator.csusing System.Runtime.InteropServices; namespace SuperCalculatorPlugin { public class NativeSuperCalculator { [DllImport(SuperCalculator_bundle.IMPORT_TARGET)] private static extern int superCalculator_add(int l, int r); public static int Add(int l, int r) { return superCalculator_add(l, r); } } }2. NewBehaviourScriptなどで呼び出してみる
NewBehaviourScript.csusing SuperCalculatorPlugin; public class NewBehaviourScript : MonoBehaviour { void Start () { int res = NativeSuperCalculator.Add(32, 45); Debug.Log("res: " + res); } }おわりに
Unityに実装されていない(設定の)加算をnative pluginを使って実現するプラグインを作成しました。
iOSの実機のみで使用する場合は、.mmファイルをベタ置きしてunityのビルドしたxcode projectにブリッジヘッダを設定すると言う方法もあるのですが、ブリッジヘッダの設定や、editorで動かないなど手間がかかるので、個人的にはbundleとframeworkを作りたい派です。
同様の手順でbluetoothや、ble keyboard, mp4作成など、unityに実装されていない、楽しい機能のネイティブプラグインを作成できるので、一度試してみて頂けると嬉しいです!
- 投稿日:2020-02-04T22:56:13+09:00
シャッター音なしのアプリを作ってみた
作るに至ったわけ
日本や韓国などではスマホ(ケータイ含め)で写真を撮影しようとするとシャッター音が鳴りますが、海外で販売されたスマホ(iPhone含め)はシャッター音は鳴りません。インスタなどでレストランなどの静かなところで撮影する時など、あの音が鳴ると気を使いますよね。マナーからだという意見もありますが、スクリーンショットもなる必要はありません。法律で決まってるわけでないので、徐々にこの慣習がなくなっていくといいのになと思います。
というわけで、iOS向けにマナーモード時はシャッター音がならないカメラアプリを作ってみました。
無音カメラを作る方法
- ビデオ撮影モードで静止画をキャプチャする方法
割とこの方法は一般的なようでぐぐると多く見つかります。ただ、静止画モードではないため解像度が劣ったりといろいろ制限があります。
- 逆位相の音を再生する方法
ノイズキャンセリングと同じ方法ですが、ジャストのタイミングで再生しないといけないため、難易度は高いようです。
- シャッター音のサウンドファイルを削除する方法
Jailbreakしかできません。
などなど。今回、どうやったかはまだ言えません。審査を乗り越えるには
Appleの審査が通るまで約1ヶ月かかりました。基本的には、ユーザーに気付かないような録音、録画、撮影などは許可できないというもの。シャッター音を消すことはできませんと。特に、アプリ名からも連想させてはいけませんと。最初は「サイレントカム」っていう名前でした。
だって、AppStore見ると「無音カメラ」とかもろいってるアプリだってあるわけで、なんで???って感じでした。アプリの説明にも思いっきり書いてあるし、、神であるAppleが言うのだから仕方ありません。
未だ謎ですが、
- アプリ名には「無音」「サイレント」と言った連想するキーワードは入れない
- シャッター音をなるモードも用意する(今回のようにマナーモード以外はシャッター音鳴ります)
と言うのが今回の審査でわかりました。
使ったフレームワーク
- AVFoundation.framework
ちょっと面倒でしたが、たまたまみたサンプルがこれだったのと、撮影画面とかをカスタマイズできたほうがいいかなと思ったのが理由です。
そしてリリース
C'zCam
https://apps.apple.com/jp/app/czcam/id585273753静か目、静かカメラ、静かカメ、、、というわけで、C'zCamです。
よかったら試してみてください。無料です。参考
- 投稿日:2020-02-04T18:33:38+09:00
【CocoaPods】iOSのライブラリをForkして使う
iOSアプリのライブラリを一部カスタマイズして使う機会があったので。
フォーク
まず普通にFork。
https://help.github.com/ja/github/getting-started-with-github/fork-a-repo
GitHub公式練習用リポジトリSpoon-Knifeを使って説明。Fork元の変更を楽に取り込むローカル構成手順もあるが割愛。
https://help.github.com/ja/github/getting-started-with-github/fork-a-repo#step-1-set-up-gitPodfile
ライブラリを修正したら、Podfileで本来このように書くものを…
pod 'Spoon-Knife'このようにfork先のrepoとbranchを指定して
pod install。pod 'Spoon-Knife', :git => 'git@github.com:{{ your name }}/Spoon-Knife.git', :branch => '{{ your branch }}'参照 : https://guides.cocoapods.org/using/the-podfile.html#from-a-podspec-in-the-root-of-a-library-repo
ただし元のライブラリが既にインストール済みの場合、リポジトリのみの変更は解釈されないので一度この行を消しての再インストールが必要だった。
- 投稿日:2020-02-04T18:31:25+09:00
NavigationBarのBackボタンの見た目を変えずにイベントをつける
はじめに
久々に投稿します!
NavigationBarの「<」Backボタンにイベントをつけたいと思い、調査をしましたが
見た目を変えずにイベントを付与することができる情報がなかなか見つからなかったので
備忘としてまとめてみます
iOS12以上をサポートしている前提で調査をしたため、SwiftUIは使用していません。
開発環境
以下の環境で開発しました。
Tool Version Xcode 11.2.1 (11B500) やりたいこと
今回実現したいことは以下の通りです。
- NavigationBarのBackボタンタップ時にアラートを表示したい
- Backボタンの見た目は変えたくない
- Backボタンで戻る際のアニメーションも変えたくない
1番のみであれば、Backボタンを非表示にしてleftBarButtonItemに自作のBackボタンを追加すれば実現できそうですが、
2番3番の見た目を変えないというのが難しいです。実現に向けたアプローチ
実現方法を2つ考えました。
1. NavigationBarのSubviewsを再帰的に取得して、どうにか「<」の画像を手に入れて自作のBackボタンに使用する
2. Backボタンのイベントを取得して、自作のイベントで上書きをする1番のアプローチでは、画像取得はできましたが結局自分で画像やタイトルの位置を調整する必要が出てきました
2番のアプローチで実現することができました。具体的には、UIViewControllerにExtensionを実装します。
navigationController?.navigationBarのSubviewsを再帰的に探索してUIControlを取得し、
戻るイベントを削除し、新しいイベントを登録します。実装
再帰的にSubViewsを取得する
以下を参考にさせていただきました。
Swiftで再帰的なサブビューの取得とUI層のユニットテストへの応用UIView+RecursiveSubviews.swiftimport UIKit extension UIView { /// 再帰的にサブビューを取得する var recursiveSubviews: [UIView] { return subviews + subviews.flatMap { $0.recursiveSubviews } } /// UIViewの特定サブクラスのビューを取得する func findViews<T: UIView>(subclassOf: T.Type) -> [T] { return recursiveSubviews.compactMap { $0 as? T } } }UIControlのタップイベントをクロージャで登録する
ボタン等のイベントの登録
addTarget(_:action:for:)をクロージャでするためのExtensionを作成しました。UIControl+Tap.swiftimport UIKit typealias TapEvent = () -> Void extension UIControl { /// タップイベントをクロージャで登録する func tap(action: @escaping TapEvent) { self.eventListener(controlEvents: .touchUpInside, forAction: action) } func eventListener(controlEvents control: UIControl.Event, forAction action: @escaping(() -> Void)) { self.actionHandler(action: action) self.addTarget(self, action: #selector(triggerActionHandler), for: control) } } private extension UIControl { func actionHandler(action: (TapEvent)? = nil) { struct ActionHolder { static var action :(TapEvent)? } if let action = action { ActionHolder.action = action } else { ActionHolder.action?() } } @objc func triggerActionHandler() { self.actionHandler() } }NavigationBarのBackボタンにイベントを登録する
上記の2つのExtensionを使用して、
NavigationBarのBackボタンにイベントを登録するUIViewControllerのExtensionを作成しました。UIViewController+AddNavigationBackEvent.swiftimport UIKit extension UIViewController { /// NavigationBarのBackボタンにイベントを登録する func addNavigationBackEvent(action: @escaping TapEvent) { guard let controls = navigationController?.navigationBar.findViews(subclassOf: UIControl.self) else { return } for control in controls { if control.allTargets.isEmpty { continue } control.removeTarget(nil, action: nil, for: .allEvents) control.tap(action: action) break } } }使い方
対象のViewControllerのviewDidLayoutSubviews()で呼び出します。
viewDidLoadで呼び出すと、Subviewsを取得できず、イベント登録ができません。NavigationNextViewControllerfinal class NavigationNextViewController: UIViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() self.addNavigationBackEvent { [weak self] in self?.confirmWhetherToBack() } } /// 前の画面に戻るかどうか確認するアラート表示 private func confirmWhetherToBack() { let alert = UIAlertController(title: "確認", message: "入力内容が保存されていません。\n編集を終了しますか?", preferredStyle: .alert) // OKボタン alert.addAction( .init(title: "OK", style: .default, handler: { [weak self] _ in guard let `self` = self else { return } self.navigationController?.popViewController(animated: true) }) ) // キャンセルボタン alert.addAction( .init(title: "Cancel", style: .cancel) ) present(alert, animated: true) } }実行結果
さいごに
やりたいことは実現できましたが、もう少し簡潔に実装できるのではないかと思っています
もっと良い実装方法等ご存知でしたらご教示ください
- 投稿日:2020-02-04T18:21:43+09:00
iOSアプリのライセンス生成ツール「LicensePlist」のセットアップ&操作方法
はじめに
ライセンス表示を手動で実装するのが手間なので、導入することにしました。
「LicensePlist」とは?
CarthageとCocoaPods、SwiftPMで管理しているライブラリのライセンス表示を自動的に生成するツールです。
設定ファイルを用意することで、任意のライブラリに対しても生成できるようです。環境
- OS:macOS Mojave 10.14.6
- Swift:5.1.3
- Xcode:11.3.1 (11C504)
- LicensePlist:2.12.0
セットアップ
LicensePlistのインストール
Mintからインストールします。
Mintfile+ mono0926/LicensePlist@2.12.0$ mint bootstrapその他の方法でインストールするには、公式ドキュメントをご参照ください。
https://github.com/mono0926/LicensePlist#installationSettings Bundleの追加
プロジェクトに
Settings.bundleがない場合、こちらの記事を参考に追加してください。
記事の内容と異なり、製品ターゲットのフォルダ内に作成していいと思います。Settings Bundleの編集
まず、こちらの記事を参考に、デフォルトで配置されている内容を削除します。
Key Type Value Type String Child Pane Title String Licenses Filename String com.mono0926.LicensePlist
TitleのValueは任意の値でOKです。操作方法
ライセンスファイルの生成
CartfileファイルやPodsフォルダがあるフォルダでlicense-plistコマンドを実行します。$ mint run LicensePlist license-plist --output-path ${PRODUCT_NAME}/Settings.bundle以下の3ファイルが
Settings.bundle内に生成されます。
XcodeGenを使っている場合、プロジェクトファイルを再生成した方がいいと思います。
私はMakefileを作成し、
make generate-licenseを実行すると「ライセンスファイルの生成→プロジェクトファイルの生成→CocoaPods用ワークスペースの生成」を一括で行うようにしています。MakefilePRODUCT_NAME := CurrencyConversion .PHONY: generate-licenses generate-licenses: mint run LicensePlist license-plist --output-path ${PRODUCT_NAME}/Settings.bundle $(MAKE) generate-xcodeproj .PHONY: generate-xcodeproj generate-xcodeproj: mint run xcodegen xcodegen generate $(MAKE) install-cocoapods .PHONY: install-cocoapods install-cocoapods: bundle exec pod installライセンス表示までの画面遷移
ライセンスが表示されているか確認します。
バージョン管理から無視する(任意)
生成されたライセンスファイルをバージョン管理の対象外にします。
Gitを使っている場合、以下を「.gitignore」に追加するのみでOKです。
.gitignore+ com.mono0926.LicensePlist/ + com.mono0926.LicensePlist.latest_result.txt + com.mono0926.LicensePlist.plistこちらの設定は任意です。
バージョン管理するのもありだと思います。おわりに
これでライセンス表示を実装する手間が省けます!
ただし、ライブラリの使用前にライセンスを確認するのは忘れないようにしましょう。参考リンク
- 投稿日:2020-02-04T17:33:01+09:00
【swift】まず簡単なリファクタリングをしよう【初心者向け】
なぜ簡単なリファクタリングをするのか?
1.簡単なリファクタリングだけでも、コードの可読性が激増する。
2.難しい文法でリファクタリングをしようとすると、挫折する。
3.すぐ終わる。大事なこと?
たまにリファクタリングをしたほうがいい。
リファクタリングで
- 開発のしやすさ。
- 説明のしやすさ。
- コード修正のしやすさ。
を手に入れましょう✨
- 説明のしやすさ。
これは、特に大事です。
誰かにアドバイスをもらうためコードを添付する際、
自分ですら読めないコードを誰が読めるのでしょう。リファクタリングしないと??
初心者の方は初めてのアプリ(ポートフォリオなど)を作成する際、
『文法とかコードの可読性とかどうでもいいからとりあえず完成させたい!』という気持ちがあると思います。しかし、コードが増えれば増えるほど
- 以前書いたコードが理解できない
- 変数名、メソッド名がパッと見わからない
などの問題が発生します。
簡単なリファクタリング集
わかりやすい命名をする
三項演算子を使う
let hoge = true if hoge { print("true") } else { print("false") } // ↓三項演算子を使った表現。 hoge ? print("true") : print("false")使用しないコードは削除する
コメントアウトして、放置しているコードはありがちですよね。
思い切って削除しましょう。コメントは分かりやすく
パッと見で理解できるコメントを残しましょう。
参考
selfは省略可能省略できる箇所は、するべきでしょう。
参考参考文献
- https://qiita.com/aomathwift/items/addd2dd5fa9e714b78ea
- https://codic.jp/
- https://qiita.com/y-some/items/8bc06567eee18dfeafbb#%E7%89%B9%E6%AE%8A%E3%82%BF%E3%82%B0
- https://qiita.com/taketomato/items/d5630fe5df45f4093720#31-%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%B8%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%AF-self-%E3%82%92%E7%9C%81%E7%95%A5%E3%81%99%E3%82%8B
- 投稿日:2020-02-04T17:01:30+09:00
Selector完全攻略、そして初学者特有のAddTarget()やAddObserver()のセレクタに変数を渡そうとする願望について
前回の記事はNotificationCenterの基本でした。
その中で、難易度的に省いたトピックを今回書きます。偉く長いタイトルにしましたが、トピックとしては二点で、
- Selectorの話
- AddTarget()/AddObserver()に変数渡したいときどうするか
を扱います。
実行環境
Swift: 5.1.3
Xcode: 11.3.1Selector完全攻略
NotificationCenterのAddObserver()の引数の中に、Selectorが出てきます。
このSelector、僕はUI部品にAddTarget()するときの引数で最初に遭遇して、かなり扱いに苦しみました。addTarget例let button = UIButton() button.addTarget(self, action: #selector(somefunction), for: .touchUpInside) @objc func somefunction() { …… }あるボタンをタップしたら、何か処理を動かすといったコードを書くとき、
こんな指定をすると思うんですが、正直疑問はたくさんありました。
- @objcとは?
- これはでもなんかお約束でつけなきゃいけないし、Xcodeが補完してくれるのでそんな問題じゃない
- #Selectorってなんだ?
- Selector()でもセレクタつくれるけど、どっち使ったらいいの?
- なんか他のSwiftの構文と君全然違くない?
- クロージャの渡し方と全然違うんだけど……
- てか引数どうなってんの
- これは何? 文字型? クロージャ?
- たまにサンプルでsomefunction(_:)ってなってるの、何?
- アメリカ式の顔文字?→何わろとんねん
当時の自分の疑問を解消するような形で、説明していきたいと思います。
Selectorとは
セレクタ(Selector)は、そもそもObjective-Cの概念です。
Swiftには純粋な意味でのセレクタは存在しませんが、
それだとObjective-Cのコードとの互換性で困るので、折衷案的にSwiftでもセレクタが使えます。Objective-Cでは、下記のように指定します。
@selector ( method )Swiftでは実は指定する方法が三通りあります。
よく使うと思われる順で、#Selector、Selector、文字列のみの三つです。実装方法(引数なし)
セレクタで使おうとしているメソッドに引数がないときは指定が楽なので、まずはその前提で説明します。
共通サンプルコード
説明上必要なのはAddTarget()のところだけなんですが、動作確認用に使ったサンプルコード全部を載せときます。
自分で色々試してみたい方はお使いください。サンプルimport UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let button = UIButton(frame: CGRect(x: view.center.x - 50, y: view.center.y - 50, width: 100, height: 100)) button.backgroundColor = .black button.setTitle("Event", for: .normal) button.titleLabel?.textColor = .white view.addSubview(button) //以後の説明では↓この一行だけ使います button.addTarget(self, action: #selector(somefunction), for: .touchUpInside) } @objc func somefunction() { print("動かしたい処理を記述") } }#Selector
button.addTarget(self, action: #selector(somefunction), for: .touchUpInside)詳しくは→Selector Expression
Selector
button.addTarget(self, action: Selector("somefunction"), for: .touchUpInside) //No method declared with Objective-C selector 'somefunction'というワーニングが出るSwiftで#とか@とかの接頭詞がついていると、あまり利用が推奨されないものという感覚があって、
Selectorの方が#SelectorよりSwiftらしいのか? と勉強しはじめの頃は思ったのですが、
Selectorに関しては#Selectorを使う方がいいと思います。
(異論あればコメントください。#なしのSelectorの存在意義が今のところ僕の中ではイマイチ飲み込めてません……)Xcodeから
Wrap the selector name in parentheses to suppress this warningという修正案が出るので、Fixを押すと、え……button.addTarget(self, action: Selector(("somefunction")), for: .touchUpInside)なぜか二重カッコに訂正されます。
まあそれはいい(よくないが)んですが、一番キツイのは、#Selectorと違って、メソッド名のコンパイラチェックがないことです。#Selectorbutton.addTarget(self, action: #selector(samufankusyon), for: .touchUpInside) // →Use of unresolved identifier 'samufankusyon'#Selectorは、メソッド名のチェックをしてくれるので、コンパイルエラーとなります。
一方で、Selectorbutton.addTarget(self, action: Selector(("samufankusyon")), for: .touchUpInside)Selectorは文字列なので存在しないメソッド名でも指定できてしまい、実行時エラーとなります。
Selectorで文字列からメソッドを呼び出す(Swift4.2)
文字列のみ
実は文字列だけ突っ込んでも、ワーニングは出ますが、なんか上手いこと解釈してくれて動きます。
button.addTarget(self, action: "somefunction", for: .touchUpInside) //No method declared with Objective-C selector 'somefunction' //Replace '"somefunction"' with 'Selector("somefunction")' [Fix]動作的にはSelector("somefunction")と一緒ですね。
実装方法(引数あり)
で、引数があるときの書き方。
ちょっと冗長になりますが、3パターン全部書いてみようと思います。
(初学者の頃網羅的に書いてくれてる記事がなくて辛かった思い出があるので)その前に
セレクタで指定したメソッドの引数に何を渡せるかを決めるのは、そのセレクタを引数にとる関数が決めます。
ここではAddTarget()です。#selector に複数の引数を持たせたいです
↑こちらの回答にある通り、AddTarget()でとれる引数は最大2つで、
しかもイベント発生時のUIButtonとUIEventのインスタンスしか取れません。したがって、
button.addTarget(self, action: #selector(somefunction), for: .touchUpInside) @objc func somefunction(some: String, number: Int) { print("動かしたい処理を記述") }こんな書き方をすると、実行時エラーとなります。
ちなみに、NotificationCenterのAddObserver()の引数は、Notificationインスタンス1つです。
aSelector
Selector that specifies the message the receiver sends observer to notify it of the notification posting. The method specified by aSelector must have one and only one argument (an instance of NSNotification).変数の型と中身は制約がありますが、変数名とセレクタの書き方は自由です。
具体的に見ていきましょう!#Selector
button.addTarget(self, action: #selector(somefunction(sender:forEvent:)), for: .touchUpInside) //button.addTarget(self, action: #selector(somefunction), for: .touchUpInside) // ↑これでも呼び出し可 // sender/forEvent/eventなどの変数名は開発者によって変更可 @objc func somefunction(sender: UIButton, forEvent event: UIEvent) { print("動かしたい処理を記述") print(sender.titleLabel?.text) //Optional("Event") print(event) //<UITouchesEvent: 0x282e10640>…… }初学者の頃よく混乱したネットに転がってるサンプルで、
#selector(somefunction(_:))と指定している例がありますが、
あれは呼び出すメソッドの外部引数名を_で省略可としているため、こんな書き方ができるんですね〜button.addTarget(self, action: #selector(somefunction(_:_:)), for: .touchUpInside) @objc func somefunction(_ sender: UIButton, _ event: UIEvent) {……}コロンで変数分けてるのがSwift的には気持ち悪いですが、Objective-Cの名残りだと思われます。
Selector
Selectorの引数指定はちょっとしんどいです。
#Selectorみたいに、button.addTarget(self, action: Selector("somefunction(sender:forEvent:)")), for: .touchUpInside)でイケると思うじゃないですか。
実行時エラーでクラッシュします。
NSInvalidArgumentExceptionです。Selectorで文字列からメソッドを呼び出す(Swift4.2)こちらを参考にしながら、色々試してみましたが、30分くらいクラッシュし続けました。
ダメだった例Selector("somefunctionWithSender:ForEvent:") Selector("somefunction") Selector("somefunctionWithsender:forEvent:") Selector("somefunctionwithsender:forEvent:") Selector("somefunctionWithSender:Event:") Selector("somefunctionWithSender:event:") // 頭おかしくなりそうメソッドの引数を一つにしてみたら動いたので、もうこれ書いてお茶を濁そうかなと思ったとき、参考にしていたページを見直して、気付きました。
あ、これ第二引数、一文字目小文字やん!!!!正解button.addTarget(self, action: Selector("somefunctionWithSender:forEvent:"), for: .touchUpInside)正解はこう。
WithV1:V2:……みたいに引数を指定するのは、Objective-C方式です。
注意すべきは、第一引数はメソッド定義に関わらず一文字目を大文字に、
第二引数以降はメソッド定義に従う、というところでした。うーんやっぱ#Selectorより無印Selectorの方がObjective-Cっぽいですよねえ。
文字列のみ
Selectorと一緒です。
NotificationCenter使うときのUserInfoの扱い方
AddTarget()を例に長々説明してきましたが、AddObserver()についても少し書かせてください。
AddObserver()はセレクタで指定したメソッドにNotification型を渡せるのですが、
NotificationにはuserInfoというプロパティがあり、postするときにちょっとしたデータを渡せます。import Foundation class Subject { let eventName: Notification.Name init(eventName: Notification.Name) { self.eventName = eventName } func post() { NotificationCenter.default.post(name: eventName, object: nil, userInfo: ["test": "Yeah"]) } } class Observer { let eventName: Notification.Name init(eventName: Notification.Name) { self.eventName = eventName NotificationCenter.default.addObserver(self, selector: #selector(doWhenEventOccur), name: eventName, object: nil) // NotificationCenter.default.addObserver(self, selector: "doWhenEventOccurWithNotification:", name: eventName, object: nil) } @objc func doWhenEventOccur(_ notification: Notification) { print(notification.userInfo) //Optional([AnyHashable("test"): "Yeah"]) } deinit { NotificationCenter.default.removeObserver(self) } } let eventName = Notification.Name("Event Occurs") let observer = Observer(eventName: eventName) let subject = Subject(eventName: eventName) subject.post()※このコードについての解説は前回の記事参照
ただ[AnyHashable: Any]型という、ちょっと扱いづらい辞書型で渡るので、正直使いづらいです。
初学者特有のAddTarget()やAddObserver()のセレクタに変数を渡そうとする願望について
以上で本題は終わりなんですが、関連して。
iOSアプリ開発に手を染めて、1〜2ヶ月くらい、やたらAddTarget()のときに値を渡そうとしてもがいた記憶があります。
今思うと、なんでそんな必要があったのかな〜という気持ちなんですが、
もしかしたらこの記事をみている方も、無理やりセレクタで指定したメソッドに値渡したくてこの記事にたどり着いたかもしれないので、
そのことについて書こうと思います。結論:セレクタで変数渡したいときはプロパティ経由で渡すよう検討する
結論的にはこれです。
Qiitaを漁ると、userInfoで渡すとか、UIButtonをExtensionで拡張するとか、やり方は色々出てきます。ただきれいなやり方とは言えないと思います。
ケースバイケースではありますが、クラスなり構造体なりのプロパティとして渡したい変数をつくって、そこ経由で受け渡すのがベストだと思います。
なんらかの理由で渡せないなら、アクセス制御の設計がミスっているので、そこを直した方がいいです。なぜ初学者はセレクタに変数を渡したがるのか
などと正論を書いても、実際僕も最初の頃セレクタに変数渡さないと実装できなかったシーンが何かあったような記憶がうっすらとあります。
具体的なケースは思い出せないのですが、確かにありました。なんでだろうな〜と考えたところ、僕の中で一つの結論が出ました。
初学者がセレクタに変数を渡したい、と思うのは、ViewControllerが世界の全てだからではないでしょうか。
Xcodeで新規プロジェクトつくった際に用意されるサンプルコードを使うと、ViewControllerのViewDidLoad()から書き始めることになります。そこでコード量が多くなって、各処理をメソッドに切り分けていきます。
ViewControllerにはデフォルトだとイニシャライザが書かれていません。
なので、ViewControllerのプロパティを作ろうとすると、イニシャライザがないよ、で怒られます。初期値を設定してやるとか、オプショナル型にするとか、イニシャライザを真面目に書くとか、
なんらか対処できればいいのですが、いかんせん初学者なので、
「ViewControllerに直でプロパティ書くとなんか上手くいかないなあ……」で終わる(僕の場合ですが)。
となると何がなんでもセレクタに変数を渡せないと、自分がやろうとしている機能ができない! となるわけです。遠回りのようですが、iOSアプリ全体の構造とか、クラスの使い方とかについて勉強するのが良いのだと思われます。
まとめ
というわけで、SwiftのSelectorについてでした。
- 投稿日:2020-02-04T16:53:26+09:00
Flutterでパッケージ名(Package Name / Bundle Identifier)をiOS/Androidで修正する箇所まとめ
はじめに
flutterでプロジェクトを新しく作成するとき、
flutter create my_app_nameとすると、Package Name/Bundle Identifier は自動的に
com.example.my_app_nameのように指定されてしまいます。リリースに向けて開発を進めるのであればパッケージ名を正しく修正する必要があるので、その修正箇所をまとめておきます。Android
1. android/app/src/AndroidManifest.xml
- 3行目: package
- 8行目: android:label
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.your.orgname.my_app_name"> <application android:name="io.flutter.app.FlutterApplication" android:icon="@mipmap/ic_launcher" android:label="My App Name">2. android/app/src/debug/AndroidManifest.xml
- 2行目: package
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.your.orgname.my_app_name">3. android/app/build.gradle
- 41行目付近: applicationId
defaultConfig { applicationId "com.your.orgname.my_app_name" minSdkVersion 16 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" }4. MainActivity.kt
- 1行目: package
package com.your.orgname.my_app_name5. ディレクトリ変更
変更前android/app/src/main/java/com/example/my_app_name↓ packageに合わせたディレクトリに変更
変更後android/app/src/main/java/com/your/orgname/my_app_nameiOS
XcodeでBundle Identifierを修正するだけ。
(※アンダースコアが勝手にハイフンに自動変換がかかりました。Xcodeの仕様のようです。)
最初からorg名を指定するには
flutter create --org com.your.orgname my_app_name今度からはこのようにcreateしましょう。
- 投稿日:2020-02-04T16:53:26+09:00
Flutterでパッケージ名がcom.exampleになっているのをiOS/Androidで修正する箇所まとめ
はじめに
flutterでプロジェクトを新しく作成するとき、
flutter create my_app_nameとすると、Package Name/Bundle Identifier は自動的に
com.example.my_app_nameのように指定されてしまいます。リリースに向けて開発を進めるのであればパッケージ名を正しく修正する必要があるので、その修正箇所をまとめておきます。Android
1. android/app/src/AndroidManifest.xml
- 3行目: package
- 8行目: android:label
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.your.orgname.my_app_name"> <application android:name="io.flutter.app.FlutterApplication" android:icon="@mipmap/ic_launcher" android:label="My App Name">2. android/app/src/debug/AndroidManifest.xml
- 2行目: package
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.your.orgname.my_app_name">3. android/app/src/profile/AndroidManifest.xml
- 2行目: package
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.your.orgname.my_app_name">4. android/app/build.gradle
- 41行目付近: applicationId
defaultConfig { applicationId "com.your.orgname.my_app_name" minSdkVersion 16 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" }5. MainActivity.kt
- 1行目: package
package com.your.orgname.my_app_name6. ディレクトリ変更
変更前android/app/src/main/java/com/example/my_app_name↓ packageに合わせたディレクトリに変更
変更後android/app/src/main/java/com/your/orgname/my_app_nameiOS
XcodeでBundle Identifierを修正するだけ。
(※アンダースコアが勝手にハイフンに自動変換がかかりました。Xcodeの仕様のようです。)
最初からorg名を指定するには
flutter create --org com.your.orgname my_app_name今度からはこのようにcreateしましょう。
- 投稿日:2020-02-04T11:26:58+09:00
iOSでビルド時に自動生成するソースコードをGitで管理しないために
はじめに
iOSアプリ開発において、リソースにタイプセーフにアクセスするためのソースコードを自動生成する SwiftGen や R.swift のようなツールをよく利用すると思いますが、みなさんはその自動生成されたソースコードは Git 管理下においていますか?
私の開発中のアプリではこれまで Git 管理下においていたのですが、アプリの要件により次のような状況になりました。
- Build Configuration に応じて自動生成されるコードに違いが出るようになった
- ソースコード以外にも Build Configuration に応じてアプリにバンドルさせるファイルを切り替える必要性がでてきた
このような状況ですと、Build Configuration によってファイルが変化してしまうため、ビルド時に自動生成するファイルは Git 管理下におかないようにしました。が、その中でつまづきポイントがあったのでメモとして残します。
Git 管理外にすると...
Git コマンドで自動生成ファイルを Git 管理対象外にします。
$ git rm path/to/Generated/fileXcode プロジェクトツリー上ではそのファイルが赤色で表示され、存在しないことがわかります。
そして、自動生成されてもコミット対象にならないよう、.gitignore ファイルに自動生成先ディレクトリを追加しました。
.gitignorepath/to/Generatedこの状態でビルドを実行してみたところ、次のようなビルドエラーとなってしまいました。
error: Build input files cannot be found: 'path/to/Generated/Asset.swift', 'path/to/Generated/StoryboardScenes.swift', 'path/to/Generated/Strings.swift', 'path/to/Generated/StoryboardSegues.swift' (in target 'MyApp' from project 'MyApp')自動生成のタイミングを確認するも...
自動生成のタイミングがソースコードのコンパイル時に間に合っていないのかと思い、Xcode プロジェクトの Build Phases を確認してみましたが、SwiftGen を実行する Run SwiftGen は Compile Sources よりも前のタイミングになっていないので問題なさそうです。
プロジェクトツリー上のファイルの存在チェックはこの Build Phases よりも前のタイミングで実施されるのでしょうか?
解決策
いろいろと調べて R.swift リポジトリのこの issue にたどり着きました。
First build always fails using the new build system with Xcode 10 Beta 6
どうやら Build Phases で生成されたファイル等は青果物として明示してあげる必要がありそうです。ということで先程の Run SwiftGen の Output Files に自動生成されるファイルのパスを追加しました。
これにより、自動生成される前の状態でのXcodeプロジェクトツリー上での表示もこのように変わりました!ファイルアイコンは半透明のままですが、ファイル名は赤色ではなくなっています。
もちろん、ビルドも問題なく通るようになりました
![]()
- 投稿日:2020-02-04T11:15:55+09:00
Apple Music API(MusicKit)のDeveloper Token取得方法
いつも忘れてググって色んなサイト飛んだりするので、自分の備忘録として書いておく
Apple Developerページでの作業
https://developer.apple.com/account/
Music IDを取得
Developer Accountの証明書等々の一覧ページにいく
Identifiersの+ボタンよりMusic IDsを選択
Description,Identifierを入力し、作成MusicKit用の秘密鍵を作成
Keysページで+ボタンより作成
Key Nameを入力し、Music Kitのconfigureボタンで紐付けたいMusic IDを設定Registerボタンで作成し、Downloadボタンよりダウンロードしておく。
.p8ファイルがダウンロードされるKey ID, TeamIDを確認
- Key ID ... 先程作成した秘密鍵の詳細ページにて確認
- Team ID ... Developer Membershipページにて確認
これに加え、先程作成した
.p8ファイルを用意apple-music-token-generatorを使用してDeveloper Token作成
apple-music-token-generatorをダウンロードし、READMEに従いPython関連をインストール
music_token.pyの内容を自分の秘密鍵, Key ID, Team IDに書き換える秘密鍵 ... 先程ダウンロードした
.p8ファイルの内容ターミナル上で
music_token.pyを実行python music_token.py
- 出力されたTokenでApple Music APIにアクセスできる
curl -v -H 'Authorization: Bearer 出力されたToken' "https://api.music.apple.com/v1/catalog/us/artists/36954"
- 投稿日:2020-02-04T09:53:55+09:00
なぜiPhone 11 Pro では0.5の線が描画できないのか調べてみた
環境
- Xcode 11.3
何が起こったのか
いつもどおり 0.5 の線を引くために Storyboard の AutoLayout で
Height = 0.5の制約を指定した。すると、デザイナーから「線の太さにばらつきがあるので揃えてほしい」と指摘があった。
↑たしかによく見ると上の線のほうが若干濃くも見える・・。両方とも 0.5 で指定しているが、念のため次のコードを書いて height を出力してみた。
print(separator1.frame.size.height) // 上の線 print(separator2.frame.size.height) // 下の線すると、
0.66666666666666 0.33333333333333と出力された!
目の錯覚ではなく、たしかに上の線のほうが濃いことがわかった。
なぜ0.5で指定しているのに正しく描画されないか
Twitter にてこの疑問をつぶやいてみたところ、 @kishikawakatsumi さんが回答をくださった。
Frameの小数部分は実行時にPixel alignedになるように丸められるからですね。Scaleをかけて割る、みたいな処理が自動的に入ってます。
https://twitter.com/k_katsumi/status/1224279359529205760なるほど!
つまり、iPhone 11 Pro のような x3 でレンダリングされるデバイスは 1pt の線が実際には 3px で描画される。pxに小数を含むことはできないため、小数点以下は丸められてしまう。
例)0.5pt で指定された線は1.5pxで描画しようとするが、小数点が丸められ、1px(=0.3333pt)か2px(=0.6666pt)として描画される。
このとき、どう丸められるかはコントロールできない。 このため、太さに差が出てしまっていた!
参考:x2 や x3 についてはこのデバイス一覧表がわかりやすい
https://www.paintcodeapp.com/news/ultimate-guide-to-iphone-resolutionsどうしたらよいか
やはり同じ悩みを持っている人がいた。
ios - How to create a line width of 0.5 pixels - Stack Overflowつまり、
- x3 デバイスでは 0.3 pt で指定するようにする
- x2 デバイスでは 0.5 pt で指定するようにする
このようにすると、確かにどのデバイスでもばらつきがなく線画描画されるようになった。
しかし、 Storyboard では 0.5 単位でしか height を指定できなので、次のようなコードを書いて線を実装する必要がありそう。
(1.0 / [UIScreen mainScreen].scale)
2020/2/4 追記:
1 / scaleの高さを制約に設定したSeparatorというCustom Viewを作る方法もあるそうです!@IBDesignable
にしておくとStoryboardに置くだけで使える
https://twitter.com/k_katsumi/status/1224503484822671360
なにかおかしなことを書いていたらコメントお願いします
![]()
- 投稿日:2020-02-04T01:36:19+09:00
UITableViewCell の高さを UIImage のアスペクト比を元に決定する方法
はじめに
UITableViewCell にセットする画像のアスペクト比が固定ではない場合、セルの高さの決め方を工夫する必要があります。今回はその方法の一つとして、 AutoLayout を用いた解決策を示します。
解決策
final class CustomTableViewCell: UITableViewCell { @IBOutlet private weak var mainImageView: UIImageView! private var aspectConstraint: NSLayoutConstraint? { didSet { if let value = oldValue { mainImageView.removeConstraint(value) } if let constraint = aspectConstraint { mainImageView.addConstraint(constraint) } } } func prepare(image: UIImage) { setImage(image) } private func setImage(_ image: UIImage) { let aspect = image.size.width / image.size.height let constraint = NSLayoutConstraint( item: mainImageView as Any, attribute: .width, relatedBy: .equal, toItem: mainImageView, attribute: .height, multiplier: aspect, constant: 0.0 ) constraint.priority = UILayoutPriority(rawValue: 999) aspectConstraint = constraint mainImageView.image = image } }
aspectConstraint: NSLayoutConstraint?ではセルの再利用を考慮して制約の削除も行っています。また、セルに対して複数回画像がセットされる可能性がある場合(セルのイニシャライズが行われてから破棄されるまでの間に複数回setImage()が呼ばれる場合)にも対応します。実際に制約を作成して UIImageView に適応するのが
setImage()になります。priorityに関しては、構築している UI に応じて臨機応変に設定してください。参考文献

























