- 投稿日:2020-12-18T23:38:28+09:00
[Swift5]githubにファイルをコミットしたいが、数が多すぎる時の対処法
開発環境
Swift5 xcode12.2
EngineerApp という名前のプロジェクトを作成しているときに起きた問題です。エラー
このように、gitにファイルをコミットしようと思った際に、ファイルの数が多過ぎると、コミットができないです。
the working copy “EngineerApp” failed to commit files.
といったメッセージが出てきて、コミットに失敗します。原因
プロジェクト内に画像などのファイルを追加する際に誤って他のファイルも一緒に入れてしまった。自分の場合は、Macの中のダウンロードフォルダのデータが全て入ってしまってました。
対処方法
ターミナルで下記を実行してターミナルを閉じ、プロジェクトを再起動し、再びコミット
cd EngineerApp
/Applications/Xcode.app/Contents/Developer/usr/bin/git reset
参考
- 投稿日:2020-12-18T23:03:07+09:00
【Swift】現在時刻を数値で取得
どういうことか
例えば 12時34分 という時刻から
12
と34
という数値を取得したい。
Swiftの日時取得は初めてでDateFormatter
を使うというのはたくさん見つけたんだけど、文字列型で取得されて、それをうまくキャストすることが(ぼくには)できなかった。
なんとか見つけた方法でうまくいった。Calendar
let now = Date() let time = Calendar.current.dateComponents([.hour, .minute], from: now) print(time.hour!) print(time.minute!)PHPで型に甘えてたのかなぁとぼんやり思ってます。
それにしてもこんな単純なことでこんなに手間かかるもんなんでしょうか。簡単な方法あったらぜひ教えていただきたいです。
- 投稿日:2020-12-18T19:37:47+09:00
[Swift] ByteArrayとHexStringを相互変換する
実装
String+.swiftextension String { func toBytes() -> [UInt8]? { let length = count if length & 1 != 0 { return nil } var bytes = [UInt8]() bytes.reserveCapacity(length / 2) var index = startIndex for _ in 0..<length / 2 { let nextIndex = self.index(index, offsetBy: 2) if let b = UInt8(self[index..<nextIndex], radix: 16) { bytes.append(b) } else { return nil } index = nextIndex } return bytes } }Data+.swiftextension Data { func toHexString() -> String { var hexString = "" for index in 0..<count { hexString += String(format: "%02X", self[index]) } return hexString } }動作確認
Test.swiftfunc testBytesHexConversion() { let bytes: [UInt8] = [0, 1, 254, 255] XCTAssertEqual("0001FEFF", Data(bytes).toHexString()) XCTAssertEqual(bytes, "0001FEFF".toBytes()) }参考
- 投稿日:2020-12-18T19:33:50+09:00
NotificationCenterを用いて、iOSアプリのライフサイクルイベントを検知する
はじめに
iOSアプリがフォアグラウンド、ハックグラウンドに入った等のライフサイクルイベントを、NotificationCenterを用いて検知する方法について書いていきます。このNotificationCenterの使用例として、アプリがバックグランドに入って30分以上経過した状態でフォアグラウンドに戻った時に、API通信を行う方法をRxSwift、ViewModelを使用して説明します。
環境
- Swift:5.3
- Xcode:12.1
iOSアプリのライフサイクル
下記がiOSアプリのライフサイクルの図です。
Managing Your App's Life Cycleより引用。
状態 内容 Unattached アプリが未起動の状態。 Background アプリがバックグラウンドで実行中。 Foreground Inactive アプリがフォアグラウンドで実行中だが、イベントは受信していない。別の状態に切り替わる時に、一瞬この状態になる。 Foreground Active アプリがフォアグラウンドで実行中。アプリを使用している時は基本的にこの状態。 Suspended アプリがバックグラウンドで未実行。 状態遷移時に呼ばれるメソッド
メソッド 呼ばれるタイミング func application(_:willFinishLaunchingWithOptions) アプリ起動後 func application(_:didFinishLaunchingWithOptions:) アプリが画面を表示する直前 func sceneWillEnterForeground(UIScene) フォアグラウンドで実行を開始しようとする時 func sceneDidBecomeActive(UIScene) アクティブになり、イベントを受け取れる状態になった時 func sceneWillResignActive(UIScene) アクティブ状態から離れ、イベントの受信を止めようとしている時 func sceneDidEnterBackground(UIScene) バックグラウンドに入った時 func applicationWillTerminate(UIApplication) アプリが終了する時 アプリ全体で状態遷移のイベントを検知し、何らかのメソッドを実行したい場合はAppDelegate、SceneDelegateにある上記の各メソッド内に記述すれば問題ありません。ただ、場合によってはViewControllerで各状態遷移のイベントを検知したいことがあると思いますので、その方法について記述していきます。
ViewControllerで状態遷移イベントを検知する
状態遷移イベントを検知するNotificationCenterが用意されているので、それを使用します。
他にもありますが、主に下記4つがあります。
Notification 通知するタイミング willEnterForegroundNotification フォアグラウンドで実行を開始しようとする時 didBecomeActiveNotification アクティブになり、イベントを受け取れる状態になった時 willResignActiveNotification アクティブ状態から離れ、イベントの受信を止めようとしている didEnterBackgroundNotification バックグラウンドに入った時 NotificationCenterが通知するタイミング
アプリ起動時
- willEnterForegroundNotification
- didBecomeActiveNotification
アプリをバックグラウンドへ
- willResignActiveNotification
- didEnterBackgroundNotification
コントロールセンターを表示して閉じる
- 表示
- willResignActiveNotification
- 閉じる
- didBecomeActiveNotification
通知センターを表示して閉じる
- 表示
- willResignActiveNotification
- didBecomeActiveNotification(←なぜか呼ばれる)
- willResignActiveNotification(←2度目が呼ばれる)
- 閉じる
- didBecomeActiveNotification
使用例
今回はアプリがバックグランドに入って30分以上経過した状態でフォアグラウンドに戻った時にAPI通信を行うようにします。RxSwiftを使用して、ViewModelとバインディングします。
ViewController.swiftimport UIKit import RxSwift import RxCocoa final class ViewController: UIViewController { private let viewModel = ViewModel() private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() bindOutput() bindIntput() } private func bindOutput() { NotificationCenter.default.rx.notification(UIApplication.willResignActiveNotification) .subscribe(onNext: { [unowned self] _ in // アプリがアクティブではなくなる時の時間を取得 self.viewModel.inputs.willResignActiveTime.onNext(Date()) }) .disposed(by: disposeBag) NotificationCenter.default.rx.notification(UIApplication.didBecomeActiveNotification) .subscribe(onNext: { [unowned self] _ in // アプリがアクティブになった時の時間を取得 self.viewModel.inputs.didBecomeActiveTime.onNext(Date()) }) .disposed(by: disposeBag) } private func bindIntput() { viewModel.outputs.refreshData .subscribe(onNext: { [unowned self] in // API通信する処理を記載 }) .disposed(by: disposeBag) } }Viewmodel.swiftimport RxCocoa import RxSwift protocol ViewModelInput { var willResignActiveTime: PublishSubject<Date> { get } var didBecomeActiveTime: PublishSubject<Date> { get } } protocol ViewModelOutput { var refreshData: PublishSubject<Void> { get } } protocol ViewModelType { var inputs: ViewModelInput { get } var outputs: ViewModelOutput { get } } final class ViewModel: ViewModelType, ViewModelInput, ViewModelOutput { var inputs: ViewModelInput { return self } var outputs: ViewModelOutput { return self } private let disposeBag = DisposeBag() // Inputs var willResignActiveTime = PublishSubject<Date>() var didBecomeActiveTime = PublishSubject<Date>() // Outputs public var refreshData = PublishSubject<Void>() init() { setBind() } private func setBind() { let thirtyMinutesPassed = didBecomeActiveTime .withLatestFrom(willResignActiveTime) { [unowned self] didBecomeActiveTime, willResignActiveTime in self.checkThirtyMinutesPassed(didBecomeActiveTime, willResignActiveTime) } thirtyMinutesPassed .filter { $0 } .subscribe(onNext: { [unowned self] _ in // バックグラウンドに入って30分以上経過した場合更新する self.outputs.refreshData.onNext(()) }) .disposed(by: disposeBag) } // アプリがバックグラウンドに入ってから30分経過したか判定 // バックグラウンドに入った時間とアプリがアクティブになった時間の差が30分(30*60秒=1800秒)以上の時はtrueを返す private func checkThirtyMinutesPassed(_ didBecomeActiveTime: Date, _ willResignActiveTime: Date) -> Bool { let convertedDidEnterBackgroundTime = Int(willResignActiveTime.timeIntervalSince1970) let convertedDidBecomeActiveTime = Int(didBecomeActiveTime.timeIntervalSince1970) return convertedDidBecomeActiveTime - convertedDidEnterBackgroundTime >= 1800 } }今回はアプリがバックグランドに入った時、フォアグラウンドに戻った時の時刻をそれぞれNotificationCenterを使用して取得し、その差が30分以上の場合にAPI通信をするように実装しました。
アプリがフォアグラウンドになった状態検知に
didBecomeActiveNotification
を使用しました。アプリがフォアグラウンドになる時にはwillEnterForegroundNotification
でも通知されますが、まだアプリが完全にアクティブな状態でない時に通知されるため、正常にメソッドが実行されない懸念があります(今回で言うとAPI通信する処理)。そのため、確実にアクティブな状態になった時に通知されるdidBecomeActiveNotification
を使用するのが良いです。アプリがバックグラウンドに入った状態検知には
willResignActiveNotification
を使用しました。didEnterBackgroundNotification
でもバックグラウンドに入ったことを検知できますが、コントロールセンター/通知センターを使用する時に問題が発生します。
その理由は、コントロールセンター/通知センターを表示する際にはdidEnterBackgroundNotification
は通知されませんが、閉じた際にはdidBecomeActiveNotification
が通知されるからです。例えば下記のようなことが発生してしまいます。12:00 バックグラウンドに入る(didEnterBackgroundNotification) 12:31 バックグランドから復帰(didBecomeActiveNotification) 12:32 通知センター/コントロールセンターを開く -> didEnterBackgroundNotificationは呼ばれず、バックグラウンドに入った時刻は12:00のまま 12:33 通知センター/コントロールセンターを閉じる -> didBecomeActiveNotificationが呼ばれ、復帰した時刻は12:33 -> 12:00と12:33で差が30分以上のため、通知センター/コントロールセンターを表示してすぐ閉じた場合でもメソッドが実行される今回はAPI通信処理を実行するので、通知センター/コントロールセンターを表示してすぐ閉じた場合にもメソッドが実行されてしまうと、無駄な通信が発生することになってしまいます。このような事を避けるため、通知センター/コントロールセンターを表示した場合にも通知される
willResignActiveNotification
を使用しました。これで通知センター/コントロールセンターを表示してすぐ閉じた場合にメソッドが実行されるということはなくなります。参考文献
- 投稿日:2020-12-18T17:30:11+09:00
iOS14 で始める CarPlay
はじめに
みなさん、CarPlay をご存知でしょうか?
実はiOS7以降のiOSには CarPlay という機能が搭載されており、対応車種であればカーナビに iPhone を繋いで、カーナビ上で CarPlay 対応された iPhone アプリを操作できるようになっています。
対応車種はこちら
本稿ではその CarPlay 対応アプリのサンプルとしてナビゲーションアプリを取り上げ、地図を表示させるところまで説明します。
準備
実際にカーナビにつなげる CarPlay のアプリの開発を始めるに当たってXcodeを開く前に色々と準備をする必要があります。
1. CarPlay の開発申請
2. Provisioning Profile の設定なお、とりあえず Simulator でいいから作ってみたい、という人はここの項目を飛ばしてもらってCarPlay アプリを実装するの項目に行ってもらって結構です。
CarPlay の開発申請
まず、iOS アプリ開発の基本として実機デバッグするには Apple Developer Program (以下ADP) に試験用端末の登録と Provisioning Profile の登録が必要なのですが CarPlay の場合はその Provisioning Profile に CarPlay 用の設定を行わないといけません。
ですが ADP の Provisioning Profile の設定画面みてもありません。その設定を行うにはまず Apple に申請を ここ から行う必要があります(要 ADP アカウント)。
その際、開発する CarPlay 向けアプリがどのようなものか App Type のセレクトボックスから以下のカテゴリから選びます
1. Audio - 音楽アプリ
2. Automaker - 車両組み込み機能アプリ
3. Communication - チャットアプリ
4. EV Charging - 充電スポット検索アプリ
5. Navigation - カーナビアプリ
5. Parking - 駐車場検索アプリ
6. Quick Food Ordering - レストラン検索アプリ上記カテゴリ以外のアプリは開発は許されておらず、また、2つ以上を組み合わせるアプリを作ることも許されてはいません。
この中で特殊なのが 2 の Automaker です。エアコンの操作やラジオと言った車両に組み込まれてる機能を操作するためのアプリです。
もちろん車両側からそのAPIが提供されている必要があるため実際には開発する際は車両 or 周辺機器メーカーと協業する必要がでるでしょう。なお、申請を行えるのは ADP アカウント保持者のみです。 InHouseバイナリを配布できる法人向けアカウント Apple Developer Enterprise Program(以下 ADEP )アカウントで申請しようとすると、この画面ではなく以下のような権限がないことを伝える画面が表示されます。
実はこれは結構厄介で、
現状では CarPlay 対応させると InHouse ビルドができなくなることを意味しています。業務で iOS アプリを開発してる人のほとんどが社内テスト用に InHouse ビルドしてると思うのですが、
CarPlay 対応の開発をする際はビルドコンフィルグ等をいじって InHouse ビルド時は コンパイル範囲から CarPlay を外すと言った処理が必要になってくるでしょう。(もしくは TestFlight を使うか)Provisioning Profile の設定
CarPlay の申請が通ると Provisioning Profile 作成画面に新たに Entitlement を設定する項目が追加されます
そこの項目に App Type で設定したカテゴリを選択します。
以上で CarPlay を実機デバッグするための事前準備は終了です。次に実際に CarPlay を実装していくために必要な設定及びコードを説明していきます。CarPlay 向けアプリを実装する
では実際にiOSアプリを CarPlay 対応させていきましょう。
CarPlay 対応アプリと書いてしまうと tvOS や watchOS アプリのように専用の Bundle Identifier を用意して
ターゲットを新たに作るというイメージを持つ人がいるかもしれませんが実はそうではなく、すでにある iOS アプリのUIをカーナビ向けに最適化させる作業になります。
どちらかというと iPhone アプリの iPad 対応の方が感覚的に近いです。では、はじめに、で述べたようにナビゲーションアプリを作っていきます
Simulator について
前段で実機デバッグするための準備について書きましたが、ずっと車両やカーナビに繋いで開発していくのも難しいため Simulator を使うことになると思います。
CarPlay の Simulator は実は iPhone Simulator に組み込まれており以下の図の様に I/O → External Display → CarPlay を選択して表示できます
Entitlements ファイルを作成
開発するアプリが CarPlay 対応アプリであるということを設定する必要があります。
Entitlements ファイルを作り Provisioning Profile に設定されたカテゴリIDを有効化させます。
今回作るナビゲーションアプリの場合、カテゴリIDはcom.apple.developer.carplay-maps
になります<dict> <key>com.apple.developer.carplay-maps</key> <true/> </dict>CarPlay 用 SceneDelegate を作る
実は Entitlements ファイルを作成した時点で CarPlay からアプリを呼ぶことが可能になります。
が、この時点で CarPlay 側から呼びだすと CarPlay 用の SceneDelegate が設定されていないためクラッシュします。
そのため、まず以下の様な CarPlay 用の SceneDelegate を用意します。CarPlaySceneDelegate.swiftclass CarPlaySceneDelegate: UIResponder { var interfaceController: CPInterfaceController? var window: CPWindow? } extension CarPlaySceneDelegate: CPTemplateApplicationSceneDelegate { func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController, to window: CPWindow) { self.interfaceController = interfaceController } }上記が CarPlay 用の SceneDelegate の最小限実装です。
CarPlay から呼ばれるためにCPTemplateApplicationSceneDelagate
を実装します。
CarPlay 上でアプリが呼ばれると上記のtemplateApplicationScene(_ templateApplicationScene:didConnect interfaceController:to window:)
メソッドが呼ばれるのでその中で CarPlay 用の画面を作っていくことになります。CarPlay 用の SceneDelegate を呼び出せるようにする
CarPlay 用の SceneDelegate を作ったら今度は CarPlay から呼び出せるように設定をします。
方法としては、Info.plist でやる方法と AppDelegate でやる方法の2つがあります呼び分け方法その1 - Info.plist でやる方法
Info.plist に
Application Scene Manifest
という項目があります。
XML のキーとしてはUIApplicationSceneManifest
になります。
その中に項目としてUISceneConfigurations
を追加し、iPhone用のSceneDelegate
クラス、 CarPlay 用のSceneDelegate
クラスをそれぞれ以下のように追記します<key>UIApplicationSceneManifest</key> <dict> <key>UISceneConfigurations</key> <dict> <key>UIWindowSceneSessionRoleApplication</key> <!-- "iPhone用のSceneDelegate設定"--> <array> <dict> <key>UISceneConfigurationName</key> <string>iPhone</string> <key>UISceneDelegateClassName</key> <string>$(PRODUCT_MODULE_NAME).IPhoneAppSceneDelegate</string> </dict> </array> <!-- "CarPlay用のSceneDelegate設定"--> <key>CPTemplateApplicationSceneSessionRoleApplication</key> <array> <dict> <key>UISceneConfigurationName</key> <string>CarPlay</string> <key>UISceneDelegateClassName</key> <string>$(PRODUCT_MODULE_NAME).CarPlaySceneDelegate</string> </dict> </array> </dict> </dict>これを記述することで自動的に呼び出し元が iPhone 画面なのか CarPlay 画面なのかが判定されそれに合わせた
SceneDelegate
が呼ばれるようになります。呼び分け方法その2 - AppDelegate でやる方法
Info.plist だけでなく AppDelegate 側にコードを記述して出しわけの制御を書くこともできます。
iOS13から AppDelegate に画面情報(UIScene
)を受け取るデリゲートメソッドを定義することができるようになりました。AppDelegate.swift@available(iOS 13.0, *) func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { switch connectingSceneSession.role { case .carTemplateApplication: // CarPlay 用のアプリとして起動された let config = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) config.delegateClass = CarPlaySceneDelegate.self return config default: // それ以外 let config = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) config.delegateClass = IPhoneAppSceneDelegate.self return config } }渡ってくる
UISceneSession
のrole
プロパティをみるとアプリがどのシーンから起動されたのかがわかるので、
そこで出しわけをすることになります。
Info.plist
で出しわけをする方がいいのかAppDelegate
で出しわけをする方がいいのか正直悩ましいです。
アプリのデプロイ戦略に応じて変わると考えています。ちなみに、両方書いた場合はどちらかが無視される、というわけではなくどうやら結果が合成されるようです。
これ以上はUIScene
の概念の説明となり本稿の主題からずれるので説明しませんが、omochimetaru さんの記事が詳しいので
以下を読んでいただけるといいかもしれません。
https://qiita.com/omochimetaru/items/31df103ef98a9d84ae6bCarPlay 向けの画面を用意する
では準備が全て終わったので CarPlay アプリを作っていきましょう。
以下のコードが CarPlay から呼び出された際に画面を表示する為の最小限のコードです。
ナビゲーションアプリを作るための UI テンプレートであるCPMapTemplate
をセットし、起動時にそのテンプレートが表示されるようにしています。CarPlaySceneDelegate.Swiftclass CarPlaySceneDelegate: UIResponder { var interfaceController: CPInterfaceController? var window: CPWindow? } extension CarPlaySceneDelegate: CPTemplateApplicationSceneDelegate { func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController, to window: CPWindow) { self.interfaceController = interfaceController self.window = window let mapTemplate = CPMapTemplate() interfaceController.setRootTemplate(mapTemplate, animated: true) } }CPTemplate とは
ViewController
を起点として画面を構築していく iOS や Mac アプリと違い、CarPlay 対応には基本的にViewController
は出てきません。
代わりにCPTemplateApplicationSceneDelegate
のデリゲートメソッドで渡ってくるCPInterfaceController
にCPTemplate
を継承したクラスをセットして画面表示を行います。
CPTemplate
系クラスそのものには画面遷移を行うメソッドはないです。したがって、画面遷移を行う場合は
templateApplicationScene
で渡ってくるCPInterfaceController
を持ち回して実装することになります。地図を表示する
さて、起動時にナビゲーション用のテンプレートが表示できるようになった、と言ってもテンプレートに何もセットしていないため
起動しても以下の様なただ黒い画面が表示されるだけです。
なので簡単に地図を表示してみます。
前項で CarPlay 対応には基本的にViewController
は出てきません、と書いたのですが例外があります。
ナビーゲーションアプリの開発に使われるCPMapTemplate
は地図表示部分に関してのみViewController
を使うことになります。
具体的にはCPWindow
のrootViewController
プロパティに地図表示用のViewController
をセットすると
地図が表示されるようになります。
(MapViewController
自体のコードは非常に単純なものなので割愛)CarPlaySceneDelegate.Swiftfunc templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController, to window: CPWindow) { self.interfaceController = interfaceController self.window = window let mapTemplate = CPMapTemplate() interfaceController.setRootTemplate(mapTemplate, animated: true) { _,_ in // MapViewController は内部的に MKMapView を保持した ViewController window.rootViewController = MapViewController() } }すると以下のように Apple の地図が表示されるようになります。
よくみると上に黒いバーがあるのがわかると思うのですが、ここにボタンを置いて行って様々なインタラクションを行えるようにしていくことになります。
地図のパンニングやルート表示についてをここで説明すると長くなりすぎるため、興味があるようでしたら
参考文献のGuideとサンプルコードを参照することをお勧めします実装する上での留意点
以上が CarPlay のナビゲーションアプリで地図を表示するまでの実装になります。
様々なテンプレートが用意されているため実装量がすくなく結構簡単に作れることがわかったと思います。
(それまでの Apple とのやりとりの方がコストが高い)ですが、実装していく上で何点か留意すべき点があったため記述します。
タップ範囲が決められてる
CPMapTemplate
はほかのテンプレートと違いViewController
を表示できるのでデザインの自由度は高いように思えます。
(実際、地図以外も表示させることは可能です)
ですが、実はViewController
にタップイベントを伝搬させることはできません。
CPMapTemplate
におかれたボタン経由でしかイベントを伝搬させることしかできず、地図を直接スワイプさせたりすることはできません。
そしてCPMapTemplate
のみならず他のテンプレートもボタンの表示数、領域等に制限があります。
結果的にそれらの条件を考慮する必要があり、自由度の高いデザインができず、どのアプリも UI は似たものになります。カスタムテンプレートを作れない
CPMapTemplate
の基底クラスであるCPTemplate
は他のテンプレートクラスでも使われており画面遷移機能を持たないので
UIView
に近い印象を受けるのですが開発者側のほうで継承させてカスタムテンプレートを作る、ということはできません。Swift の言語仕様上、継承自体はできるのですが
CPInterfaceController
を使って画面に表示させようとすると
以下のエラーを吐いてクラッシュします*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unsupported object <View.Test: 0x60000047e920> <identifier: F6B164D2-E640-4644-8C06-AAFB57AD7192, userInfo: (null), tabTitle: (null), tabImage: (null), showsTabBadge: 0> passed to setRootTemplate:animated:completion:. Allowed classes: {( CPContactTemplate, CPPointOfInterestTemplate, CPGridTemplate, CPMapTemplate, CPTabBarTemplate, CPInformationTemplate, CPListTemplate, CPNowPlayingTemplate, CPSearchTemplate )}'標準の組み込まれたテンプレートでなければいけない、ということらしいです。
カテゴリによっては使えないテンプレートがある
Apple に申請を出した際、CarPlay 向けにどのような機能を提供するかカテゴリを決める必要があることは申請の項目で説明しましたが、
その選んだカテゴリによって表示できる UI 、つまりCPTemplate
系のクラスが決まります。
対応表は以下のようになっていますこの表をみるとナビゲーションアプリは表示できる UI が多いのですが Now Playing 用と Point of Interest 用のテンプレートである
CPNowPlayingTemplate
とCPPointOfInterestTemplate
が表示できないことになっています。
実際、表示しようとするとTerminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unsupported objectというエラーを吐いて落ちます。
テンプレートによっては画面遷移の仕方に制限がある(し、公式ドキュメントにずれがある)
基本的に
CPInterfaceController
のpresentTemplate
及びpushTemplate
というメソッドを使って画面遷移をさせていくのですが
presentTemplate
に関してはCarPlay can only present one modal template at a time. templateToPresent must be one of CPActionSheetTemplate, CPAlertTemplate, or CPVoiceControlTemplate.
と制限があり
pushTemplate
に関してはuse pushTemplate with a supported CPTemplate class such as CPGridTemplate, CPListTemplate, CPSearchTemplate, or
CPVoiceControlTemplate.とあり指定されたクラスしか使えないような記述がされています。
が、実はCPMapTemplate
をpushTemplate
を使って表示させることができたりします。
また、All apps are limited to pushing up to 5 templates in depth, including the root template. Quick food ordering
apps are limited to 2 templates in depth.とあり、
pushTemplate
を使って画面遷移したとしても5階層以上いけないようになっている様です。(未検証)CarPlay 対応アプリを呼び出すには
カーナビ上で操作を完結させるために CarPlay 対応アプリから URL スキーム等つかって他の CarPlay アプリを呼び出したい時があると思います(ex. Calendar, music, etc...)
通常の iOS アプリではUIApplication.shared.open(url:options:complicationHandler)
を使ってたと思うのですが、
CarPlay 上でそれを使うと CarPlay 側ではなにも起きずカーナビに繋いでる iPhone 側の方で別アプリが呼び出されます。CarPlay 側でアプリ呼び出しをしたい場合は
CPTemplateApplicationSceneDelegate
のデリゲートメソッドで渡されるtemplateApplicationScene
のopen(url:options:complicationHandler)
の方をつかう必要があります。CarPlaySceneDelegate.swiftfunc templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController, to window: CPWindow) { // こちらを使うと iPhone 側でアプリが起動する UIApplication.shared.open(URL(string: "なんらかの URL スキーマ")!, options: nil, complicationHandler: nil)] // こちらを使うと CarPlay 側のアプリが起動する templateApplicationScene.open(URL(string: "なんらかの URL スキーマ")!, options: nil, complicationHandler: nil) }個人的には iPhone 側の方でも
UIApplication.shared.open
を使うのは基本的にやめてUIScene.open
を使うべきな気はしています。まとめ
以上、CarPlay 対応のナビゲーションアプリの作り方と注意点を紹介してみました。
読んでいただいてわかると思うのですがカーナビという安全に直結するアプリケーションの UI であるため
非常に縛りが多く、自由なレイアウトというのはできない設計になります。とはいえ、主観ではありますが従来の広く販売されているカーナビと比べるとやはりだいぶ使いやすいです。
また、ナビゲーションアプリに関しては ViewController を使って地図を表示してるので
その気になれば動画や音楽を流せる気がしています(未検証)。Apple の審査に落とされそうな気がしてますが審査に出さず個人でやる分には問題ないはずなので
どこかのタイミングで実験して記事にしたいなと考えています。参考文献
- CarPlay App Programing Guide
- 2020年7月更新。CarPlay の概念については一番わかりやすい
- CarPlay Navigation App Programing Guid
- 2018年更新、説明のベースが iOS12 になっておりちょっと古い
- Integrating CarPlay with Your Navigation App
- ナビゲーションアプリの作り方のサンプルコード
- https://qiita.com/koogawa/items/71710027a4ee7af267bc
- koogawa さんの CarPlay の記事
- https://koogawa.hateblo.jp/entry/2019/01/15/003501
- koogawa さんの CarPlay でAudioアプリを作ってみた際の記事
- 投稿日:2020-12-18T17:16:16+09:00
【Swift】プロトコルの概念と定義方法
プロトコルとは、型のインターフェースを定義するものです。
インターフェースとは、型がどのようなプロパティやメソッドを持つか示したものになります。型のインターフェースを定義する方法
プロトコルは、型が特定の性質や機能を持つために必要なインターフェースを定義するものです。
また、プロトコルが要求するインターフェースを型が満たすことを準拠と言います。私の記事でたまに登場する、
「○○型は△△プロトコルに準拠しているので、このメソッドを使うことができます。」
というフレーズはそれを意味しています。プロトコルを利用することで、複数の型で共通となる性質を抽象化できます。
例えば、2つの値が同値であるかどうかを同値性と言い、
同値性が検出可能であるという性質は、
標準ライブラリのEquatableプロトコルで表現されています。同値性、すなわち==演算子は、Equatableプロトコルに定義されています。
そして、Equatableプロトコルに準拠する方は==演算子に対する実装を定義する必要があります。プロトコルのおかげで様々な恩恵を受けるのですが、
その中の一つで、プロトコルに準拠している型のみを扱うことが可能になります。次のサンプルコードでは、
関数の引数を2つ指定し値が同じかどうか出力していますが、
渡せる引数の型をEquatableプロトコルに準拠している型のみに制限しています。制限している箇所は、
func equate<T: Equatable>(・・・)
の部分になります。
T型はEquatableプロトコル準拠している型ですよ。と制限をかけています。
(TではなくAでもBでも実行可能です。Tを使う傾向があるそうです・・・。)今回、引数に渡している値はInt型やString型になり、
これらの型はEquatableプロトコルに準拠しているので実行できます。
(_ value1: T, _ value2: T)
とあるように、
第一引数と第二引数で引数の型をT型で統一しているので、
異なる型を引数に渡すとコンパイルエラーが発生します。func equate<T: Equatable>(_ value1: T, _ value2: T) { if value1 == value2 { print("値は同じです。") } else { print("値が異なります。") } } equate("abc", "abc") equate(1, 1) equate(1, 2) // equate(1, "1") // コンパイルエラー 実行結果 値は同じです。 値は同じです。 値が異なります。プロトコルの基本
プロトコルの定義方法や準拠方法、利用方法について説明します。
定義方法
プロトコルは
protocolキーワード
を使用して宣言し、
{ }内にプロパティやメソッドなどの構成要素を定義していきます。構成要素については後ほど記載します。
protocol プロトコル名 { プロトコルの定義 }準拠方法
型はプロトコルに準拠することにより、
プロトコルで定義されたインターフェースを通じて扱うことが可能になります。型をプロトコルに準拠されるためには下記のように記述します。
(今回は構造体をプロトコルに準拠させています。)また、
,
で区切ることにより複数のプロトコルに準拠させることが可能です。struct 構造体名: プロトコル名1, プロトコル名2 ・・・ { 構造体の定義 }プロトコルに準拠するには、
プロトコルが要求している全てのインターフェースに対する実装を用意する必要があります。次のサンプルコードのように、プロトコルでメソッドを定義した場合は、
そのプロトコルに準拠している方は同じメソッドを型内に定義する必要があります。定義がされていない場合はコンパイルエラーになります。
protocol SampleProtocol { func sampleMethod() } struct Sample: SampleProtocol { func sampleMethod() { } } struct Sample2: SampleProtocol { } // コンパイルエラーエラー内容:
Type 'Sample2' does not conform to protocol 'SampleProtocol'
和訳:タイプ「Sample2」はプロトコル「SampleProtocol」に準拠していませんクラス継承時の準拠方法
構造体だけでなくクラスでもプロトコルに準拠することが可能ですが、
先ほどの定義方法だとスーパークラスなのかプロトコルなのかわかりません。実は決まりがあり、継承とプロトコルへの準拠を同時に行う場合は、
最初に継承するスーパークラス名を書き、次にプロトコルを書く必要があります。class クラス名: スーパークラス, プロトコル名1, プロトコル名2 ・・・ { クラスの定義 }エクステンションによる準拠方法
プロトコルへの準拠はエクステンションで行うことも可能です。
エクステンションで準拠対象のプロトコルを追加する方法は下記になります。extension エクステンションを定義する型: プロトコル名 { プロトコルが要求する定義 }1つのエクステンションに対して複数のプロトコルを準拠させることも可能です。
これは人によって別れると思いますが可読性を高めるためには1つずつがいいと思います。というのも、一度に複数のプロトコルに準拠させると、
どのプロトコルに対する定義なのかが分かりづらくなるからです。コード量は増えますが、一度のエクステンションで一つのプロトコルに準拠させれば、
どのプロトコルで宣言されている内容なのかが分かりやすくなります。自分以外の人もメンテナンスする可能性があるので、
可読性を高くするためにも分けた方がいいかな?と私は思います。実際に分けると下記のような記述になります。
// 1つ目のプロトコル protocol SampleProtocol1 { func sampleMethod1() } // 2つ目のプロトコル protocol SampleProtocol2 { func sampleMethod2() } //構造体 struct Sample { let a = 1 } // エクスションション1回目 extension Sample: SampleProtocol1 { func sampleMethod1() { } } // エクスションション2回目 extension Sample: SampleProtocol2 { func sampleMethod2() { } }コンパイラによる準拠チェック
あるプロトコルに準拠した型がそのプロトコルの要件を満たしているかどうかは
コンパイラによってチェックされ、一つでも欠けている場合はコンパイルエラーとなります。次のサンプルコードでは、
SampleProtocol
に準拠した型であるSample型
がありますが、
プロトコルで定義されているsampleMethod()
を構造体で定義していません。この場合、要件を満たしていないのでコンパイルエラーが発生します。
protocol SampleProtocol { func sampleMethod() } struct Sample: SampleProtocol{ }エラー内容:
Type 'Sample' does not conform to protocol 'SampleProtocol'
和訳:タイプ「Sample」はプロトコル「SampleProtocol」に準拠していません利用方法
プロトコルは、構造体・クラス・列挙型・クロージャと同様に、
変数・定数・引数の型として利用することができます。プロトコルに準拠している型はプロトコルにアップキャストすることが可能です。
なので、次のサンプルコードような処理が可能です。
func sample(x: SampleProtocol)
のように引数の型にプロトコルを指定した場合、
引数に渡すことができる型はSampleProtocolに準拠した型のみになります。
x.value
のように、
プロトコルに定義されているプロパティやメソッドを使えるのも特徴です。なお、プロトコルに準拠していない型を引数に渡した場合は
コンパイルエラーが発生します。protocol SampleProtocol { var value: Int { get } } struct Sample: SampleProtocol{ var int: Int var value: Int { return int * 10 } } // 引数の型にプロトコルを指定 func sample(x: SampleProtocol) -> Int { // 引数xのプロパティやメソッドの内 // SampleProtocolで定義されているものが使える x.value } let a = 1 // Int型 let b = Sample(int: 2) // Sample型 sample(x: a) // コンパイルエラー sample(x: b) // 20エラー内容:
Argument type 'Int' does not conform to expected type 'SampleProtocol'
和訳:引数タイプ「Int」が予期されるタイプ「SampleProtocol」に準拠していませんなお、連想型を持つプロトコルは変数や定数、引数の型として使用できず、
ジェネリクスの型引数の型制約の記述のみに利用可能です。
(連想型やジェネリクスについては別記事で説明します。)プロトコルコンポジション
プロトコルコンポジションとは、複数のプロトコルに準拠した型を表現する仕組みです。
使用方法は、複数のプロトコル名を&
で区切って記述していきます。次のサンプルコードでは、
Protocol1とProtocol2の両方に準拠している型を引数に指定しています。なので、
x.value1 + x.value2
のように、
各プロトコルで定義されているプロパティを使うことができます。protocol Protocol1 { var value1: Int { get } } protocol Protocol2 { var value2: Int { get } } struct Sample: Protocol1, Protocol2 { var value1: Int var value2: Int } func plus(x: Protocol1 & Protocol2) -> Int { x.value1 + x.value2 } let sample = Sample(value1: 10, value2: 5) plus(x: sample) // 15複数のプロトコルに準拠させる場合は、
特に型を統一する必要はなく次のようなコードを記述することも可能です。protocol Protocol1 { var value: Int { get } } protocol Protocol2 { var name: String { get } } struct Sample: Protocol1, Protocol2 { var value: Int var name: String } func plus(x: Protocol1 & Protocol2) { print("\(x.name)さんが\(x.value)円くれました。") } let sample = Sample(value: 1000, name: "近藤") plus(x: sample) 実行結果 近藤さんが1000円くれました。以上がプロトコルの概念と定義方法、利用方法になります。
少し長くなってしまい申し訳ないです。プロトコルは出現頻度も高く必修科目かと思うのでぜひ使えるようになってください!
この記事の他にもプロトコルについて説明している記事がありますので、
お時間ありましたらそちらもご覧ください。・【Swift】プロトコルを構成する要素
・【Swift】プロトコルエクステンションについて最後までご覧いただきありがとうございました!
- 投稿日:2020-12-18T15:28:00+09:00
【Swift】型の種類〜列挙型〜
列挙型 とは
列挙方は値型の一種で、複数の識別子をまとめる型になります。
曜日を例にしますと、月火水木金土日の7種類がありますが、
列挙型ではこれら7つの識別子をまとめて一つの型として扱えます。列挙型の一つ一つの識別子をケースと言います。
また、火曜日であると同時に木曜日であることがありえないように、
ケースどうしは排他的になっています。標準ライブラリでも、一部の型は列挙型として扱われています。
例えばよく登場するものでいうと、Optional<Wrapped>型が列挙型になります。定義方法
列挙型は
enumキーワード
とcaseキーワード
を使用し定義します。enum 列挙型名 { case ケース名1 case ケース名2 ・・・ そのほかの列挙型の定義 }列挙型のインスタンス化は構造体やクラスと少し違い、
列挙型名.ケース名
のようにケース名を指定してインスタンス化します。次のサンプルコードでは、列挙型Weekdayに各曜日を定義し、
.mondayと.fridayをインスタンス化しています。enum Weekday { case sunday case monday case tuesday case wednesday case thursday case friday case saturdar } let monday = Weekday.monday let friday = Weekday.fridayまた、列挙型もイニシャライザを定義することができます。
イニシャライザを追加し、引数に渡された文字列に応じて各ケースを代入します。enum Weekday { case sunday case monday case tuesday case wednesday case thursday case friday case saturdar init?(japanese: String) { switch japanese { case "日": self = .sunday case "月": self = .monday case "火": self = .tuesday case "水": self = .wednesday case "木": self = .thursday case "金": self = .friday case "土": self = .saturdar default: return nil } } } let sunday = Weekday(japanese: "日") // Optional<Weekday.sunday> let monday = Weekday(japanese: "月") // Optional<Weekday.monday>列挙型では、イニシャライザの他にもメソッドやプロパティを持つことはできますが、
プロパティには制限があり、ストアドプロパティを持つことができません。つまり、列挙型はコンピューテッドプロパティしか持つことができないのです。
raw value(ローバリュー)
列挙型は、それぞれのケースに対応する値を設定することができます。
この値のことをraw value(ローバリュー)と言います。全てのrawValueの型は同じである必要があり、
指定できる型は、Int型、Double型、String型、Character型などになります。rawValueの定義方法は、
クラスでスーパークラスを定義する時のように、
enum 列挙型名: rawValueの型 { }
と定義しますenum 列挙型名: rawValueの型 { case ケース名1 = rawValue1 case ケース名2 = rawValue2 }Int型のrawValueを設定した列挙型は次のようになります。
enum Sample: Int { case a = 1 case b = 2 case c = 3 }rawValueを定義されている列挙型では、
rawValueと列挙型の相互変換を行うためにの機能が暗黙的に追加されます。追加される機能というのが、失敗可能イニシャライザ
init(rawValue:)
と
プロパティrawValue
の2つになります。失敗可能イニシャライザは、rawValueと同じ型の値を引数にとります。
rawValueが一致するケースが存在すればそれに該当するケースを返し、
rawValueに一致するケースが存在しなければnilを返します。また、ケースに対応するrawValueを取得するには、
変数名.rawValueで暗黙的に宣言されたrawValueプロパティにアクセスします。enum Sample: Int { case a = 1 case b = 2 case c = 3 } let a = Sample(rawValue: 1) // a let d = Sample(rawValue: 4) // nil print(a?.rawValue) 実行結果 Optional(1)rawValueのデフォルト値
Int型やString型のrawValueにはデフォルト値が存在し、
値を指定しない場合はデフォルト値が適用されます。Int型のrawValueのデフォルト値は、
最初のケースが0でそれ以降はインクリメントされた値になります。String型のrawValueのデフォルト値は、
ケース名をそのまま文字列にした値がデフォルト値になります。
case other = 100
でrawValueを100にしているので、
それ以降のデフォルト値は100+1の値になります。// Int型のrawValue enum SampleInt: Int { case none case one case other = 100 case otherPlusOne } SampleInt.none.rawValue // 0 SampleInt.one.rawValue // 1 SampleInt.other.rawValue // 100 SampleInt.otherPlusOne.rawValue // 101 // String型のrawValue enum SampleString: String { case home case shcool case park } SampleString.home.rawValue // home SampleString.shcool.rawValue // shcool SampleString.park.rawValue // park連想値
列挙型のインスタンスは、ケースの情報に加えて、
連想値という付加情報を付与することができます。連想値に指定できる型に制限はありません。
色の代表的な数値表現にRGB(Red,Green,Blue)が存在します。
次のサンプルコードのように
表現方法(rgb)をケース、数値(rgbそれぞれの値)を連想値として表現すれば、
RGBを列挙型として表現することができます。RGBのように連想値の型が全て同じ型でもいいですし、
ケースhumanのようにString, Int, Stringといった異なる型でも大丈夫です。列挙型Associatedを定義し、その中に二つのケースを定義しました。
その後、定数colorと定数humanでインスタンス化し、
Associated型を格納する配列arrayに各インスタンスを格納しています。for文で配列の中の値を順に取り出し、switch文で条件分岐しています。
この時に、列挙型から連想値を取り出すための手法として、
バリューバインディングパターンを使っています。enum Associated { case rgb(Float, Float, Float) case human(String, Int, String) } let color = Associated.rgb(0.0, 0.33, 0.66) let human = Associated.human("Saitou", 20, "釣り") let array: [Associated] = [color, human] for item in array { switch item { case .rgb(let r, let g, let b): print("Red: \(r), Green: \(g), Blue: \(b)") case .human(let name, let age, let hobby): print("名前: \(name), 年齢: \(age), 趣味: \(hobby)") } } 実行結果 Red: 0.0, Green: 0.33, Blue: 0.66 名前: Saitou, 年齢: 20, 趣味: 釣りバリューバインディングパターンとは、値を変数にや定数に代入する手法になります。
具体的にはcase .rgb(let r, let g, let b)
の部分を指します。どのような処理かというと、
let r
= 第一引数、let g
= 第二引数、let b
= 第三引数 となります。
なので、case以降は、定数r, 定数g, 定数b を使用することができます。Caselterableプロトコル
列挙型を使用していると、全てのケースを配列として取得したい場合が出てきます。
例えばですが、都道府県を列挙型で表現する場合、
選択肢を表示するためには全てのケースの配列が必要になります。CaseIterableプロトコルはその要件を満たすプロトコルになります。
CaseIterableプロトコルに準拠した列挙型には、
自動的にallCasesプロパティが追加されます。このallCasesプロパティが全てのケースを返すプロパティになります。
次のサンプルコードでは、
列挙型Japanを定義しいくつか県を書いてみました。その後、
Japan.allCases
を実行したところ、
全てのケースを取得することができました。allCasesプロパティは、
enum Japan: CaseIterable { }
の部分で
CaseIterableへの準拠を宣言したので、コンパイラによって自動的に作られております。enum Japan: CaseIterable { case 群馬, 栃木, 東京, 神奈川 case 青森, 岩手, 宮城, 秋田 } Japan.allCases // [群馬, 栃木, 東京, 神奈川, 青森, 岩手, 宮城, 秋田]allCasesは、コンパイラによって自動生成された実装を使わずに、
自分でallCasesを実装することもできます。allCasesが自動生成されない条件
列挙型が連想値を持つ場合はallCasesが自動生成されなくなります。
つまり、連想値を持つ列挙型で全てのケースを列挙したい場合は、
プログラマがallCasesプロパティを自ら実装する必要があります。enum Japan: CaseIterable { case 東京(String, String, String, String) case 神奈川(String, String, String, String) static var allCases: [Japan] { return [ .東京("新宿", "渋谷", "中目黒", "江戸川"), .神奈川("横浜", "鎌倉", "小田原", "江ノ島") ] } } Japan.allCases // {東京 "新宿", "渋谷", "中目黒", "江戸川"}, {神奈川 "横浜", "鎌倉", "小田原", "江ノ島"}各要素にアクセスするには先程のバリューバインディングパターンを使ったりします。
enum Japan: CaseIterable { case 東京(String, String, String, String) case 神奈川(String, String, String, String) static var allCases: [Japan] { return [ .東京("新宿", "渋谷", "中目黒", "江戸川"), .神奈川("横浜", "鎌倉", "小田原", "江ノ島") ] } } let japan = Japan.allCases for item in japan { switch item { case .東京(let a, let b, let c, let d): print("東京:\(a),\(b),\(c),\(d)") case .神奈川(let a, let b, let c, let d): print("神奈川:\(a),\(b),\(c),\(d)") } } 実行結果 東京:新宿,渋谷,中目黒,江戸川 神奈川:横浜,鎌倉,小田原,江ノ島列挙型の説明については以上になります。
構造体、クラス、列挙型と3つの型について紹介しましたが、
どの場面ではこの型を使えばいい!というところが正直まだ理解できていません。個人的には、すでに決まっている情報で型を定義する時は列挙型
それ以外は基本的に構造体を使い、
参照型や継承として持つ機能を使いたい時にはクラスを使用すればいいのかな?
という印象です。これについては、もっと歴を重ねて一人前になったら再度共有したいと思います。
この記事の他にも型の種類について記載した記事がありますのでぜひご覧ください!
・【Swift】型の種類〜基礎知識〜
・【Swift】型の種類〜構造体〜
・【Swift】型の種類〜クラス前編〜最後までご覧いただきありがとうございました。
- 投稿日:2020-12-18T15:00:47+09:00
SwiftUIで作るプログレス
はじめに
この記事はDiverse Advent Calendar 2020の18日目の記事です。
業務でいくつかプログレスを作ったので紹介します。
同じようなデザインのプログレスを作る際はぜひご活用ください!GitHubにこの記事のサンプルを上げてます。
↓実際に動きを見たい方はこちら
https://github.com/Masataka-n/SwiftUIProgressRing
struct RingProgressView: View { var value: CGFloat var lineWidth: CGFloat = 6.0 var outerRingColor: Color = Color.black.opacity(0.08) var innerRingColor: Color = Color.orange var body: some View { ZStack { Circle() .stroke(lineWidth: self.lineWidth) .foregroundColor(self.outerRingColor) Circle() .trim(from: 0.0, to: CGFloat(min(self.value, 1.0))) .stroke( style: StrokeStyle( lineWidth: self.lineWidth, lineCap: .square, // プログレスの角を丸くしたい場合は.round lineJoin: .round ) ) .foregroundColor(self.innerRingColor) .rotationEffect(.degrees(-90.0)) } .padding(.all, self.lineWidth / 2) } }@State var value: CGFloat = 0.0 var body: some View { RingProgressView(value: value) .frame(width: 150, height: 150) .onAppear { withAnimation(.linear(duration: 5)) { self.value = 1.0 } } }Pie
Shapeを継承してPieShapeというものを作っています。
Shapeを自作した場合、アニメーションが適用されないのでanimatableData
をoverrideする必要があります。struct PieProgressView: View { var value: CGFloat var body: some View { ZStack { Circle() .fill(Color.black.opacity(0.08)) PieShape(progress: value) .fill(Color.orange) .rotationEffect(.degrees(-90)) } } } struct PieShape: Shape { var value: CGFloat var animatableData: CGFloat { get { value } set { value = newValue } } func path(in rect: CGRect) -> Path { Path { path in let center = CGPoint(x: rect.midX, y: rect.midY) path.move(to: center) path.addArc( center: center, radius: rect.width / 2, startAngle: .degrees(0), endAngle: .degrees(Double(360 * value)), clockwise: false ) path.closeSubpath() } } }@State var value: CGFloat = 0.0 var body: some View { PieProgressView(value: value) .frame(width: 150, height: 150) .onAppear { withAnimation(.linear(duration: 5)) { self.value = 1.0 } } }Square
Rectangleを2つ重ねて上のRectangleのwidthをvalueに応じて変えることで実現してます。
Squareと名付けてますがcornerRadiusを指定して角丸にすることもできます。struct SquareProgressView: View { var value: CGFloat var baseColor: Color = Color.black.opacity(0.08) var progressColor: Color = Color.orange var body: some View { GeometryReader { geometry in VStack(alignment: .trailing) { ZStack(alignment: .leading) { Rectangle() .fill(self.baseColor) Rectangle() .fill(Color.progressColor) .frame(minWidth: 0, idealWidth:self.getProgressBarWidth(geometry: geometry), maxWidth: self.getProgressBarWidth(geometry: geometry)) } } } } func getProgressBarWidth(geometry:GeometryProxy) -> CGFloat { let frame = geometry.frame(in: .global) return frame.size.width * value } }@State var value: CGFloat = 0.0 var body: some View { SquareProgressView(value: value) .frame(height: 20) //.cornerRadius(10) 角丸も可 .onAppear { withAnimation(.linear(duration: 5)) { self.value = 1.0 } } }さいごに
今回は作成したプログレスを3つ紹介しました。
基本的にコピペで使えると思いますのでぜひご活用ください。
- 投稿日:2020-12-18T14:56:53+09:00
【Swift】UIImageViewを使わずに背景に画像をセットする方法。
はじめに
背景に画像を使用する際にStoryboradにImageViewをセットすると思っていたのですが、backgroundcolorのセットと同じようにできることがわかったので備忘録をかねて簡単にまとめました。
準備
・Xcodoに新規プロジェクトを作成
・背景に使用する画像を準備
実装
viewDidLoadself.view.backgroundColor = UIColor(patternImage: UIImage(named: "backgoroundImage.jpg")!)完成
参考
まとめ
簡単に背景に画像をセットする事ができました。
今回の記事は自分の備忘録的な要素が強くなってしまったのですが、背景に画像をセットする方法を探している人の参考に少しでもなれたら嬉しいです。
- 投稿日:2020-12-18T14:56:53+09:00
【Swift】背景に画像をセットする方法。UIImageViewを使わない?
はじめに
背景に画像を使用する際にStoryboradにImageViewをセットすると思っていたのですが、backgroundcolorのセットと同じようにできることがわかったので備忘録をかねて簡単にまとめました。
準備
・Xcodoに新規プロジェクトを作成
・背景に使用する画像を準備
実装
viewDidLoadself.view.backgroundColor = UIColor(patternImage: UIImage(named: "backgoroundImage.jpg")!)完成
参考
まとめ
簡単に背景に画像をセットする事ができました。
今回の記事は自分の備忘録的な要素が強くなってしまったのですが、背景に画像をセットする方法を探している人の参考に少しでもなれたら嬉しいです。
- 投稿日:2020-12-18T13:45:36+09:00
[Swift5]'PKHUD'を用いた待機画面の実装方法
PKHUDとは
PKHUDとはHUD(Head-Up Display)を表示するためのライブラリです。
HUDを使用して、何かしらの処理が実行中であることや、完了したことをユーザーに伝えることができる。PKHUDはCocoaPods、Carthageのいずれかを使用してインストールできる。
この記事ではCocoaPodsを用いて紹介します。ライブラリのインストール
//Podfileに以下のpodを記述して'pod install'をターミナルで実行 pod 'PKHUD' //PKHUDを使いたいファイルにimport import PKHUDコードの紹介
================================================================== ①基本的な使い方 //ローディングの表示 HUD.show(.progress) //ローディングの非表示 HUD.hide(animation: true) ================================================================== ================================================================== ②成功/失敗の表示 //成功を表示 HUD.flash(.success) //失敗の表示 HUD.flash(.error) ================================================================== ================================================================== ③ローディング・成功・失敗の表示(テキストあり) //テキストを指定してローディングの表示 HUD.show(.labelProgress(title: "ここにテキストを記述", subtitle: nil)) //テキストを指定して成功を表示 HUD.flash(.labeledSuccess(title: "Success", subtitle: nil)) //テキストを指定して失敗を表示 HUD.flash(.labeledError(title: "Error", subtitle: nil)) ================================================================== ================================================================== ④テキストのみで表示 HUD.show(.label("ここにテキストを記述")) ==================================================================マスクの無効化
デフォルト設定では、HUDの背景ビューは暗くなり、ユーザーはその間アプリの操作ができません。
しかし、ユーザーが操作できるようにしておきたい状況もあると思います。そういった場合の実装方法も消化します。//HUDの背景ビューを操作できるようにする HUD.allowsInteraction = true //HUDの背景ビューを暗くしない HUD.dimsBackground = false参考にしてください!
- 投稿日:2020-12-18T11:09:43+09:00
iOSでmethod swizzlingして、共通の処理を実行する
はじめに
- iOSで共通の処理を実装するときにいろいろなパターンを考えてみましたのでまとめます。今回は
method swizzling
を中心に考えてみます。環境
Xcode 12.3 Build version 12C33 Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28) Target: x86_64-apple-darwin19.6.0method swizzlingとは
- method swizzlingはすでに存在するメソッドの処理を入れ替える機能です
- Objective-C の全てのクラスに対して使用できる。
- 共通の処理を継承せずに実装できる
クラスでの継承との違い
- クラスでの継承とmethod swizzlingでの実装の違いについてまとめました。
⭕️ ❌ method swizzling 入れ替えたクラスに処理が反映される 影響範囲が見えづらいので不具合が起きた時に対処しずらい クラスの継承 継承したクラスのみで処理が実行されるのでコントロールしやすい。 実装が必要なクラスをすべて書き換える必要があるので
- 実装方法には一長一短あることがわかります。使い所よく考える必要があります。
活用例
kickstater
- ViewModelのbindingやstyleを反映させる処理などに活用されています
- https://github.com/kickstarter/ios-oss/blob/master/Library/UIViewController-Preparation.swift
画面のトラッキング
viewWillAppear
の実装を入れ替えて、Firebaseなどを使えば、画面表示のイベントを送る処理が楽に実装できますimport ObjectiveC import UIKit private func swizzle(_ vc: UIViewController.Type) { [ (#selector(vc.viewWillAppear(_:)), #selector(vc.hoge_viewWillAppear(_:))) ] .forEach { original, swizzled in guard let originalMethod = class_getInstanceMethod(vc, original), let swizzledMethod = class_getInstanceMethod(vc, swizzled) else { return } let didAddViewDidLoadMethod = class_addMethod( vc, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod) ) if didAddViewDidLoadMethod { class_replaceMethod( vc, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod) ) } else { method_exchangeImplementations(originalMethod, swizzledMethod) } } } private var hasSwizzled = false extension UIViewController { public final class func doBadSwizzleStuff() { guard !hasSwizzled else { return } hasSwizzled = true swizzle(self) } @objc internal func hoge_viewWillAppear(_ animated: Bool) { self.hoge_viewWillAppear(animated) let instanceType = type(of: self) let name = String(reflecting: instanceType) print(name) } }まとめ
method swizzling
は便利ではあるが使い方を見極めて、使うことが必要参考リンク
- 投稿日:2020-12-18T11:09:43+09:00
iOSでmethod swizzling使って、共通の処理を実装する
はじめに
- iOSで共通の処理を実装するときにいろいろなパターンを考えてみましたのでまとめます。今回は
method swizzling
を中心に考えてみます。環境
Xcode 12.3 Build version 12C33 Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28) Target: x86_64-apple-darwin19.6.0method swizzlingとは
- method swizzlingはすでに存在するメソッドの処理を入れ替える機能です
- Objective-C の全てのクラスに対して使用できる。
- 共通の処理を継承せずに実装できる
クラスでの継承との違い
- クラスでの継承とmethod swizzlingでの実装の違いについてまとめました。
⭕️ ❌ method swizzling 入れ替えたクラスに処理が反映される 影響範囲が見えづらいので不具合が起きた時に対処しずらい クラスの継承 継承したクラスのみで処理が実行されるのでコントロールしやすい。 実装が必要なクラスをすべて書き換える必要があるので
- 実装方法には一長一短あることがわかります。使い所よく考える必要があります。
活用例
kickstater
- ViewModelのbindingやstyleを反映させる処理などに活用されています
- https://github.com/kickstarter/ios-oss/blob/master/Library/UIViewController-Preparation.swift
画面のトラッキング
viewWillAppear
の実装を入れ替えて、Firebaseなどを使えば、画面表示のイベントを送る処理が楽に実装できますimport ObjectiveC import UIKit private func swizzle(_ vc: UIViewController.Type) { [ (#selector(vc.viewWillAppear(_:)), #selector(vc.hoge_viewWillAppear(_:))) ] .forEach { original, swizzled in guard let originalMethod = class_getInstanceMethod(vc, original), let swizzledMethod = class_getInstanceMethod(vc, swizzled) else { return } let didAddViewDidLoadMethod = class_addMethod( vc, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod) ) if didAddViewDidLoadMethod { class_replaceMethod( vc, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod) ) } else { method_exchangeImplementations(originalMethod, swizzledMethod) } } } private var hasSwizzled = false extension UIViewController { public final class func doBadSwizzleStuff() { guard !hasSwizzled else { return } hasSwizzled = true swizzle(self) } @objc internal func hoge_viewWillAppear(_ animated: Bool) { self.hoge_viewWillAppear(animated) let instanceType = type(of: self) let name = String(reflecting: instanceType) print(name) } }まとめ
method swizzling
は便利ではあるが使い方を見極めて、使うことが必要参考リンク
- 投稿日:2020-12-18T10:22:10+09:00
【Xcode&Swift】ちょっと手こずるエラーまとめ
はじめに
こちらは、大学生限定プログラミングコミュニティGeekSalonアドベントカレンダー2020に投稿予定の記事です!
この記事では、私が解決に時間がかかってしまったエラー(主にコードエラー以外)を紹介しようと思います!シミュレーターが出てこない、、!
シミュレーターが選択できない原因は二つあります。
1つ目:自分のプロジェクトファイルの場合
解決法
再起動しましょう。Xcodeを一旦終了して、Mac自体も再起動すればおそらく解決です。
それでもダメな場合は、command+shift+K(クリーン)
とcommand+B(ビルド)
を試してみてください。2つ目:他人のプロジェクトファイルの場合
自分で開発しているXcodeプロジェクトファイルでは、シミュレーターが起動するのに
人からもらったXcodeプロジェクトファイルでは、シミュレーターが起動しない?
そういう現象の場合は以下を試してみてください。解決法
プロジェクト設定のInfoタブの
Deployment Target
で、上限を超えている数値を設定しているか、そもそも選択されていない場合だと思います。指定された範囲で設定しなおせば解決です!Unable to boot the Simulator エラー
シミュレーターを立ち上げたが以下のようなエラーが出た時がありました。
解決法
ターミナルで
sudo chmod -R 777 /private/tmp
コマンドを実行してみると直ります!Command PhaseScriptExecution failed with a nonzero exit code エラー
ずっと開いていないファイルの時や、github上からクローンしたファイルを開いた時に出てきたエラーです。
これはFinder情報を持つ拡張属性がよくない状態の時に出るエラーです。
解決法
ターミナルで
sudo xattr -rc .
このコマンドを打つと拡張属性がきれいになり、
その後Xcode でClean + Build
すれば解決すると思います!duplicate symbols for architecture x86_64 エラー
解決方法は2つあります!
1つ目:podsというワードがエラー文に含まれる場合
duplicate symbols for architecture x86_64
の上に大量のエラー文が記載されていると思います。もしこのエラー文中にpods
と書いてあったら、pod回りでduplicate(重複)がみられることによるエラーです。解決法
①
pod update
を試す
②上記でうまくいかないときは、podfileを削除し入れ直してください2つ目:podsというワードがない
解決法
AppDelegate.swiftに
@UIApplicationMain
というワードが抜けている可能性があります。(バージョンによっては、@main
)
抜けていたら追記してください!ビルドエラー
私がビルド(AppStoreConnectへのアップロード)をしている時に、出会ったエラーについて書いておきます!
①transparentと書いてあるエラー
解決法
これは、エラー文通り定例のものですが、アイコンを透過させなければ大丈夫です!
②success表示されるが、アップロードされない(AppStoreConnectのビルド欄で選択ができない)
これは、info.plistに空白がある場合に起きていました!
例えば、Privacy - Photo Library Usage Description
を追加しているのに、その説明書きを忘れている場合などです!解決法
info.plistで、そのような箇所がないか確認&修正しましょう。
もし見つからない場合は、そもそもデフォルト部分を間違って書き換えたり消してしまった可能性があるので、既存ファイルと見比べてみてください。③それ以外
解決法
だいたい、こちらの記事で解決しました!?
p.s.ビルド後の作業
Xcodeのキャッシュ削除は必ずやってください!
これをやらないとpcの容量が大変なことになってしまいます〜〜?(60MBになってpcが死んだことがありました笑)おわりに
エラーって、解決したそのときは「これだけ時間かかったしもう覚えたから次からはもう大丈夫!」と思うのですが、意外と頻繁に出会わないエラーは解決法を忘れがちです。。
なので忘れないように備忘録として残しておくと良いと思います!もしこの記事を読んでいる大学生の方がいましたら、ぜひこちらGeekSalonのHPに立ち寄ってください〜〜!?
- 投稿日:2020-12-18T09:04:56+09:00
ビットボードの紹介~少しのオセロ実装を添えて~
こんにちは。Qiita初投稿となります。お気軽にご覧いただければと思います。
Bitboardとは
みなさんはBitboardというデータ構造について聞きいたことはありますでしょうか?私はプロコンに関わるまで知りませんでした(笑)
名前から察しがついている方も多いとは思いますが、Bitboardは盤面をBit、つまり0・1の2進数で表現することで処理の高速化を図るデータ構造です。
以降では具体的な例を交えながらご紹介していこうと思います。2進数で盤面を表現するには
本記事の題にもある通り、本記事全体を通してオセロを例にして説明してこうと思います。
通常オセロのプログラムを書く際、どのように盤面を表現する事が多いでしょうか?
ぱっと思いつくのだと、こんな感じで盤面の大きさ分の2次元配列に黒石・白石・置いてないという状態を数字などに置き換えて代入して表現するものが多いと思います。Bitboardでは石が置いてある場所が1、置いていない場所が0と考えて2進数で表現します。盤面の大きさは8×8、つまり64マスあるので64bitで表現できることがわかると思います。
また、2進数では白石と黒石の区別をすることができないので、白石の盤面の状態と黒石の盤面の状態の2種類を保持します。図にするとこんな感じです。単純と言えば単純なのですが、プログラムに落とすととってもわかりずらくなりますので気をつけてください。
盤面の操作について
では今度は実際に盤面を操作してみましょう。
Bitboardでは配列とは違い、盤面が2進数で表現されているのでANDやORなどの論理演算やbitをずらすビットシフトをすることができます。
Bitboardではこの論理演算やビットシフトを駆使することで盤面の操作を行います。盤面の座標は0スタートとして考えてください。
ここからはちょっと図を作るのが大変なので文字で盤面を表現します笑石を置く
「石を置く」ための手順は至って単純です。(5, 3)の場所に置いてみましょう。因みに表現としては、(x, y)になります。
まず上位1bitが1で、残りの63bitが0の変数を用意します。1000000000000000...0000000これを8bitごとに区切って改行して表示してみます。
10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000こんな感じになりますね。これが先ほどまで図で表現していた盤面になります。
では、これを置きたいマスまでビットシフトし、1を移動させます。
この時シフトさせる数はy×8+x
で計算する事ができます。今回の例で言えば3×8+5=29
回ビットシフトさせます。すると以下のような状態になります。正しく場所が移動できていることがわかると思います。
00000000 00000000 00000000 00000100 00000000 00000000 00000000 00000000あとはこの置きたい場所にビットが立っている盤面を置きたい色の盤面にOR演算することで追加することができます。
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000100 OR 00010000 = 00010100 00000000 00001000 00001000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000置ける場所を列挙する
オセロにおいて重要な処理についてです。少し処理が複雑になります。
初期状態の置ける場所を列挙する処理を例にとってみます。具体的な処理を説明する前に、盤面の端を示すマスクを紹介しようと思います。
horizontal mask vertical mask allside mask 01111110 00000000 00000000 01111110 11111111 01111110 01111110 11111111 01111110 01111110 11111111 01111110 01111110 11111111 01111110 01111110 11111111 01111110 01111110 11111111 01111110 01111110 00000000 00000000盤面はあくまで2進数のため、プログラム側から見るとどこが盤面の端なのかわかりません。(本記事では2進数を盤面の大きさで改行してわかりやすくしています)
そこで、盤面の端を示す上記のマスクを使うことでその問題を解消しています。では、具体的な処理の説明に入っていきます。今回の例では初期状態で置ける場所を一部列挙してみます。
まず初期状態の盤面を用意します。player board enemy board 00000000 00000000 00000000 00000000 00000000 00000000 00010000 00001000 00001000 00010000 00000000 00000000 00000000 00000000 00000000 00000000次に
enemy board
に対し、horizontal mask
をANDして、マスクを適用します。enemy board horizontal mask masked board 00000000 01111110 00000000 00000000 01111110 00000000 00000000 01111110 00000000 00001000 AND 01111110 = 00001000 00010000 01111110 00010000 00000000 01111110 00000000 00000000 01111110 00000000 00000000 01111110 00000000そしてひっくり返せる石があるか見るために、
player board
を左へビットシフトさせ、masked board
とANDします。player board masked board tmp 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00100000 AND 00001000 = 00000000 00010000 00010000 00010000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000この上記処理をすることでplayerの石に隣接している敵の石の場所にのみbitが立っている状態になります。つまりひっくり返すことができる可能性のある石の場所にのみbitが立っている状態です。
次からは
tmp
をビットシフトしてmasked board
とANDを行い、ビットシフト前のtmp
にORをして更新していきます。
この処理はひっくり返すことができる最大の数である6回行います(端と端に置いた場合が最大)。tmp masked board tmp 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 AND 00001000 OR= 00000000 00100000 00010000 00010000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000最終的には以下のようになります。
この状態ではplayerの石の左側にあるひっくり返すことができる可能性のある石が列挙されているのみになります。tmp 00000000 00000000 00000000 00000000 00010000 00000000 00000000 00000000最後に置ける場所のみにbitを立ってる状態にしていきます。
まず、player board
とenemy board
をORをしてどちらの石も置いてある盤面を用意します。player board enemy board current board 00000000 00000000 00000000 00000000 00000000 00000000 00000000 OR 00000000 = 00000000 00010000 00001000 00011000 00001000 00010000 00011000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000次に
current board
をNOTして現状の盤面で何も石が置かれいていない場所のみにbitが立っているempty board
を作成します。current board empty board 00000000 11111111 00000000 11111111 00000000 NOT= 11111111 00011000 11100111 00011000 11100111 00000000 11111111 00000000 11111111 00000000 11111111
tmp
を左にビットシフトしてひっくり返すことができる石の左側に移動させます。これが挟める可能性のある場所にのみbitが立っている状態です。
そして、ビットシフトさせた盤面に対して、empty board
をANDすることで石を置ける場所のみにbitが立っている状態が完成します。tmp empty board legal board 00000000 11111111 00000000 00000000 11111111 00000000 00000000 11111111 00000000 00000000 AND 11100111 = 00000000 00100000 11100111 00100000 00000000 11111111 00000000 00000000 11111111 00000000 00000000 11111111 00000000これを8方向全てに行うことで列挙が完了します。
左右であればhorizontal mask
、上下であればvertical mask
、斜めであればallside mask
を使って処理をしてください。ひっくり返す処理
次はひっくり返す処理についてです。
まず、石を置きたい場所にのみbitが立っている盤面を用意します。例では(5,3)に置く想定とします。
puted mask 00000000 00000000 00000000 00000100 00000000 00000000 00000000 00000000次に左へ1ビットシフトさせ、
horizontal mask
とANDをします。puted mask horizontal mask tmp 00000000 01111110 00000000 00000000 01111110 00000000 00000000 01111110 00000000 00001000 AND 01111110 = 00001000 00000000 01111110 00000000 00000000 01111110 00000000 00000000 01111110 00000000 00000000 01111110 00000000
tmp
に対して敵の盤面をANDします。tmp empty board reverse tmp 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000 AND 00001000 = 00001000 00000000 00010000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000これ以降は
tmp
をさらにビットシフトさせempty board
とANDし、結果をreverse tmp
にORして更新する処理をtmp AND empty board
が0になるまで繰り返します。tmp AND empty board
が0になるということは連続した敵の石が無くなったことを意味します。最後に繰り返し処理を抜けたタイミングで、
player board
とtmp
をANDします。player board tmp reverse tmp 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00010000 AND 00010000 = 00010000 00001000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000もしこの結果が0以外であれば敵の石を自分の石で挟んでいるということなので最終的なひっくり返す石として確定させます。今回は0以外なので
reverse
にORをして更新して確定させます。reverse
はひっくり返せる場所にのみビットが立っている変数です。reverse tmp reverse 00000000 00000000 00000000 00000000 00000000 00000000 00010000 OR= 00010000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 000000001方向が終了したら他の方向にも行います。
この
reverse
の部分ができてしまえばあとは下記の論理演算のみでひっくり返すことができます。^
はNOT、|
はORという意味です。
player board ^= puted mask | reverse
enemy board ^= reverse
説明は以上となります。これ以外にもパスの処理・置けるかどうかのチェックなどがありますが、今までにご紹介した処理を応用すれば実装できます。
実践
ここからは実際にプログラムに落とし込んだ物をご紹介します。
実装自体は私が比較的慣れているSwiftで書いていますが、速さを求めるならAVXを使うことで高速化ができるC++で書くのが最適だと思います。実際に競プロで使おうとした際は、C++で書いていました。石を置く
func put(x: String, y: String) { guard let xInt = Int(x) else { return } guard let yInt = Int(y) else { return } var putedMask: UInt64 = 0x8000000000000000 putedMask = putedMask >> xInt putedMask = putedMask >> (yInt * 8) if isCanPut(putedMask: putedMask) { doReverse(putedMask: putedMask) nextTurn() if shouldPass { print("置けるマスがないためパスされます") nextTurn() } } else { print("(" + x + ", " + y + ")には置けません") } }置ける場所を列挙する
// 8方向分の置ける場所を列挙して返す func makeLegalBoard(playerBoard: UInt64, enemyBoard: UInt64) -> UInt64 { var legalBoard = getCanPutPoint(mask: verticalMask, playerBoard: playerBoard, enemyBoard: enemyBoard, direction: .TOP) legalBoard |= getCanPutPoint(mask: allSideMask, playerBoard: playerBoard, enemyBoard: enemyBoard, direction: .TOP_RIGHT) legalBoard |= getCanPutPoint(mask: verticalMask, playerBoard: playerBoard, enemyBoard: enemyBoard, direction: .RIGHT) legalBoard |= getCanPutPoint(mask: allSideMask, playerBoard: playerBoard, enemyBoard: enemyBoard, direction: .BOTTOM_RIGHT) legalBoard |= getCanPutPoint(mask: verticalMask, playerBoard: playerBoard, enemyBoard: enemyBoard, direction: .BOTTOM) legalBoard |= getCanPutPoint(mask: allSideMask, playerBoard: playerBoard, enemyBoard: enemyBoard, direction: .BOTTOM_LEFT) legalBoard |= getCanPutPoint(mask: horizontalMask, playerBoard: playerBoard, enemyBoard: enemyBoard, direction: .LEFT) legalBoard |= getCanPutPoint(mask: allSideMask, playerBoard: playerBoard, enemyBoard: enemyBoard, direction: .TOP_LEFT) return legalBoard } // direction方向の置ける場所にビットが立っている盤面を返す func getCanPutPoint(mask: UInt64, playerBoard: UInt64, enemyBoard: UInt64, direction: DIRECTION) -> UInt64 { let masked = mask & enemyBoard var tmp: UInt64 = masked & getStridedBoard(target: playerBoard, direction: direction) for _ in 0..<5 { tmp |= masked & getStridedBoard(target: tmp, direction: direction) } return ~(playerBoard | enemyBoard) & getStridedBoard(target: tmp, direction: direction) } // targetをdirectionの方向にビットシフトさせる func getStridedBoard(target: UInt64, direction: DIRECTION) -> UInt64 { return direction.stride.isNegativeNumber ? target << direction.stride.absValue : target >> direction.stride.absValue }ひっくり返す処理
// 実際にひっくり返す処理 func doReverse(putedMask: UInt64) { var reversed: UInt64 = 0 for i in 0..<8 { var revTmp: UInt64 = 0 var tmp: UInt64 = moveTo(target: putedMask, direction: DIRECTION(rawValue: i)!) while (tmp != 0) && ((tmp & enemyCell) != 0) { revTmp |= tmp tmp = moveTo(target: tmp, direction: DIRECTION(rawValue: i)!) } if (tmp & playerCell) != 0 { reversed |= revTmp } } playerCell ^= putedMask | reversed enemyCell ^= reversed } // targetをDIRECTION方向に移動させた上でマスクする func moveTo(target: UInt64, direction: DIRECTION) -> UInt64 { let stridedBoard = getStridedBoard(target: target, direction: direction) return stridedBoard & direction.sideMask.rawValue }終わりに
いかがだったでしょうか?
今回は通常の実装との性能比較は行っていませんでしたが、なんだか速くなりそうな気がしますよね笑Bitを使うことによる利点は並列で求めることができる点だと思っています。
オセロでいうのであれば、置ける場所を列挙するという処理はBitboardを使うと全ての石に対して同時に求めることができますが、配列ではそうもいきません。実際にプロコンでプログラムを書いていたときは、陣取りゲームだったのですがBitboardで表現するようにしたところ100倍くらい処理速度が速くなりました笑(C++でAVXで最適化した上での処理速度です)
そのときはちょっと感動してしまったのを覚えています。今回はその感動を少し思い出したので書いてみました。
Bitboardの発展系としてMagic Bitboardなるものが存在しています。調べてみると面白いかもしれません。以上となります。ご閲覧ありがとうございました。
- 投稿日:2020-12-18T02:34:02+09:00
脱Auto Layout モーダル遷移編
はじめに
最近コードでレイアウトを組んでいるのでその備忘録。ネット上にあまり情報がなかったので残しときます。改善点などがあればコメントして欲しいです。?♂️なお、外部ライブラリの
SnapKit
を使っています。完成するアプリの動きとしては
こんな感じになります。
一つ目の画面
中央にボタンを配置し、
present
でモーダル遷移するようにaddTarget
の中に入れています。
final
はこれ以上継承させない時に用いる修飾子です。最近覚えたので使っています。(笑)パフォーマンスが上がるとか上がらないとか。
button
をlazy
で遅延処理させているのはbuttonTapped
を読み込んでからインスタンス化させているからです。let
にするとエラーになるので一度試してみるといいかもしれません。import UIKit import SnapKit final class ViewController: UIViewController { lazy var button: UIButton = { let button = UIButton() button.setTitle("移動する", for: .normal) button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) return button }() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .blue navigationItem.title = "Left" self.view.addSubview(button) button.snp.makeConstraints { make in make.size.equalTo(100) make.center.equalToSuperview() } } @objc func buttonTapped() { print("ボタンが押された") let leftSecondVC = SecondViewController() self.present(leftSecondVC, animated: true, completion: nil) } }二つ目の画面
import UIKit class SecondViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .purple } }特に何もしてないです。が、
backgroundColor
で背景色を指定しないと挙動がわかりにくくなるので注意が必要です。SceneDelegate
コードが全部載っていると読みやすいと思うので全部載せます。(笑)
import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let _ = (scene as? UIWindowScene) else { return } if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = ViewController() self.window = window window.makeKeyAndVisible() } } //ここから変えてない func sceneDidDisconnect(_ scene: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). } func sceneDidBecomeActive(_ scene: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } func sceneWillResignActive(_ scene: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). } func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. } func sceneDidEnterBackground(_ scene: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. } }
- 投稿日:2020-12-18T02:27:14+09:00
脱Auto Layout TabViewController編
はじめに
最近コードでレイアウトを組んでいるのでその備忘録。ネット上にあまり情報がなかったので残しときます。改善点などがあればコメントして欲しいです。?♂️なお、外部ライブラリの
SnapKit
を使っています。完成するアプリの動きとしては
こんな感じになります。
一つ目の画面
つまづいたポイントとしては
tableView.register
を忘れていた。dequeueReusableCell
を使っていなかった。
regesiter
はAutoLayoutだと記述しないので忘れがち。画面遷移しないと思ったらここが原因かも。dequeueReusableCell
はセルを再利用して処理が重くなるのを防ぎます。カクカクしていたらここを見直すのもありかも。import UIKit final class TableViewController: UITableViewController { var numbers: [String] = ["リンゴ","ビスケット","砂糖"] override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .red navigationItem.title = "Center" tableView.dataSource = self tableView.delegate = self tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return numbers.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = numbers[indexPath.row] return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { print("selected") let centerSecondViewController = CenterSecondViewController() self.navigationController?.pushViewController(centerSecondViewController, animated: true) } }二つ目の画面
特に何もしていません。が、
view.backgroundColor = .green
がないと画面がカクカクした画面遷移になってしまいます。import UIKit class CenterSecondViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .green } }SceneDelegate
これはUINavigationViewCOntroller編のものとほとんど同じです。
cellをタップしても画面遷移しない時などはここでUINavigationViewControllerのrootViewに設定していないことが原因かもしれません。
import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var navigationVC: UINavigationController!//加えた func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let _ = (scene as? UIWindowScene) else { return } guard let _ = (scene as? UIWindowScene) else { return } if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) let tableVC = TableViewController() navigationVC = UINavigationController(rootViewController: tableVC) window.rootViewController = navigationVC self.window = window window.makeKeyAndVisible() } } //ここからしたは変えてない func sceneDidDisconnect(_ scene: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). } func sceneDidBecomeActive(_ scene: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } func sceneWillResignActive(_ scene: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). } func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. } func sceneDidEnterBackground(_ scene: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. } }
- 投稿日:2020-12-18T01:13:01+09:00
【iOS】CIFilterを駆使して、ネガフィルムの写真をネガポジ変換する
まだまだ続いているフィルムカメラブーム。デジタルカメラと違い、現像後のデータ化費用が嵩んだり、撮影後のネガフィルムも整理が間に合わず溜まっていくことが多かったり...
専用の機械なども多くありますが、お手軽にiPhoneでなんとか...と考える人は多いはず。実際AppStoreにはいくつかのフィルムスキャナーアプリがリリースされています。
有名どころだとKodak Mobile Film Scannerなどがあります。ただ、これらのアプリってどのようにiOS上でネガポジ変換を実現してるのかと聞かれると、開発を行っている人が少なく実現方法が分かりません。
なので、今回は同様のネガポジ変換を実現してみたいと思います。陰画から陽画を取得する
ネガフィルムは、撮影時の被写体の色が反転して画像が作られます。
白い被写体であれば、ネガフィルム上には真っ黒の被写体となります。
これをネガティブ画像と呼びます。
ネガティブ画像をプリント、データ化するときに色の再反転を行い、撮影時の被写体の色を復元します。
ここで得られる画像をポジティブ画像と呼びます。昔は、データ化などなくフィルムから印画紙にプリントしていました。印画紙にもネガフィルム同様色を反転する感光剤が塗布されているので、ネガフィルムに写っているコマを引き伸ばし機などを用いて印画紙に投影し、印画紙にネガティブ画像を感光させることで色を再反転し、撮影時の被写体の色を復元していました。
デジタル化では、ネガティブ画像を画像処理で色の再反転を行うことで撮影時の被写体の色を復元するということを行っているだけになります。シンプルに考えると、色の再反転を行うことでネガフィルムからポジティブ画像を取得することができるようになるということです。
トーンカーブを反転させる
ネガティブ状態の画像からポジティブ画像を取得するには、色の再反転を行う必要ですが、トーンカーブのを上下反転させることで実現することが可能です。
Swiftでは、CIFilterに用意されたToneCurveFilterを利用します。
CIFilterとは?
CoreImageFrameworkで提供される画像処理フィルターがCIFilterです。
CoreImageFrameworkもiOS5から提供されている歴史あるFrameworkで、現在では100種類以上のフィルターが提供されています。
CIFilterを駆使することができるようになると、リッチな画像編集アプリなどを作ることが可能です。CIFilterの種類は、こちらのドキュメントで確認できます。是非試してみてください。
Core Image Filter ReferenceToneCurveFilterを利用する
サンプルコードは下記のようになります。
class ToneReverse { func demo() { guard // AssetsCatalogに「nega」という名称のネガ画像がある想定で読み込む let negaImage = UIImage(named: "nege"), // CIFilterで処理するため、UIImageからCIImageへ変換する let ciNegaImage = CIImage(image: negaImage), // toneCurve反転を行うメソッドを介して反転後の画像を取得する let toneReversedImage = reverse(with: ciNegaImage) else { return } // CIImageからUIImageへ再変換 let posiImage = UIImage(ciImage: toneReversedImage) } /// Tone Reverse /// - Parameter image: base image /// - Returns: filtered image func reverse(with image: CIImage) -> CIImage? { return image.applyingFilter( "CIToneCurve", parameters: [ "inputPoint0": CIVector(x: 0.0, y: 1.0), "inputPoint1": CIVector(x: 0.25, y: 0.75), "inputPoint2": CIVector(x: 0.5, y: 0.5), "inputPoint3": CIVector(x: 0.75, y: 0.25), "inputPoint4": CIVector(x: 1.0, y: 0.0) ]) } }CIFilterにはinputPoint0からinputPoint4までのToneCurveの座標を設定することができます。
何も触らない状態のトーンカーブは、下記の画像のようになります。
ToneCurveで色反転を行うにはToneCurveの上下反転すれば良いので、Point0からPoint4までのY座標を反転させることで実現できます。
上記図のトーンカーブをCIFilterで行うと下記のような実装になります。
/// Tone Reverse /// - Parameter image: base image /// - Returns: filtered image func reverse(with image: CIImage) -> CIImage? { return image.applyingFilter( "CIToneCurve", parameters: [ "inputPoint0": CIVector(x: 0.0, y: 1.0), "inputPoint1": CIVector(x: 0.25, y: 0.75), "inputPoint2": CIVector(x: 0.5, y: 0.5), "inputPoint3": CIVector(x: 0.75, y: 0.25), "inputPoint4": CIVector(x: 1.0, y: 0.0) ]) }これでネガティブ画像からポジティブ画像を取得することが可能です。
ただ、ポジティブ画像になってはいるものの青味が強すぎてしまい撮影時の被写体の色を再現できていません。
被写体の色を再現するにはここからさらに色調補正を行う必要があります。
※Debug用にカメラ入力をトーン反転のみフィルター適用してライブ表示した場合が上記みたいになります。色調補正を行う
一応ポジティブ画像を取得することはできましたが、このままでは見るに耐えられません。
なので色調補正を行うのですが、CoreImageに用意されているToneCurveFilterはRGB値の合成チャンネルでしかToneCurveを弄ることしか出来ません。
被写体の色を再現するにはRed、Green、Blueの各チャンネルごとの色調量を調整してバランスを保つ必要があります。今回で言うと青みが強いので、青の色調量を下げて相対的なバランスを作っていくことが必要です。ただ、前述の通りCIFilterに用意されたToneCurveは合成チャンネルでしか触ることは出来ません。
ではどうするか...
CIKernelを用いてカスタムフィルターを作ります。
現在CIKernelを利用する際は、Metalを利用します。Metalとは
Metalは、2014年に登場したAppleプラットフォーム向けのグラフィックAPIです。
記述言語は、C++11ベースのMetal Shading Languageで記述します。Metal利用時の設定
Metalを利用する際には、Build SettingsのOther Metal Compiler FlagsとOther Metal Linker Flagsに-fcikernelの指定が必要です。
ここで落とし穴ですが、下記のAppleの公式ドキュメントでは、MELLINKER_FLAGSをuser-definedに追加し-cikernelを設定しろという記述がありますがXcode12.3の環境下ではWarningが表示されます。上記の設定通りにすることが求められるので注意が必要です。
Apple - Type Method kernelWithFunctionName:fromMetalLibraryData:error:Metal側の実装
今回行いたいことは、RGBごとのチャンネルをToneCurveで弄り、各チャンネルの結果を合成して1枚の画像として取得することです。
ToneCurveの操作自体は、CIFliterで各チャンネル向けにCIFilterで行い、得られたCIImageからR、G、Bの各要素だけを抽出し、各要素を掛け合わせた1枚の画像を生成することでRGBチャンネルごとにいじれるToneCurve機能を実現します。
実現にあたり、ページを参考にさせていただきました。
HSL color adjustment filter in an iOS 8.0+ app using CoreImage#include <metal_stdlib> using namespace metal; #include <CoreImage/CoreImage.h> extern "C" { namespace coreimage { float4 rgbChannelCompositing(sample_t red, sample_t green, sample_t blue) { return float4(red.r, green.g, blue.b, 1.0); } } }上記の実装は、任意の名前 + .metalの名称のファイルに記述しておきます。
Swift側の実装
先に全体のコードです。
ToneCurvePointModelのXYに入っている値は、デモ用に調整した値です。
微調整することで寒暖色などに調整することが可能です。class RgbCompositing { private let redToneCurveModel = ToneCurvePointModel(zeroX: 0, zeroY: 0, oneX: 0.38, oneY: 0.07, twoX: 0.63, twoY: 0.24, threeX: 0.78, threeY: 0.49, fourX: 1.0, fourY: 1.0) private let greenToneCurveModel = ToneCurvePointModel(zeroX: 0, zeroY: 0, oneX: 0.67, oneY: 0.07, twoX: 0.85, twoY: 0.19, threeX: 0.92, threeY: 0.39, fourX: 1.0, fourY: 0.90) private let blueToneCurveModel = ToneCurvePointModel(zeroX: 0, zeroY: 0, oneX: 0.69, oneY: 0.06, twoX: 0.90, twoY: 0.20, threeX: 0.96, threeY: 0.44, fourX: 1, fourY: 0.92) func rgbCompositing(with image: CIImage) -> CIImage? { let redImage = image.applyingFilter( "CIToneCurve", parameters: [ "inputPoint0": redToneCurveModel.pointZero, "inputPoint1": redToneCurveModel.pointOne, "inputPoint2": redToneCurveModel.pointTwo, "inputPoint3": redToneCurveModel.pointThree, "inputPoint4": redToneCurveModel.pointFour ]) let greenImage = image.applyingFilter( "CIToneCurve", parameters: [ "inputPoint0": greenToneCurveModel.pointZero, "inputPoint1": greenToneCurveModel.pointOne, "inputPoint2": greenToneCurveModel.pointTwo, "inputPoint3": greenToneCurveModel.pointThree, "inputPoint4": greenToneCurveModel.pointFour ]) let blueImage = image.applyingFilter( "CIToneCurve", parameters: [ "inputPoint0": blueToneCurveModel.pointZero, "inputPoint1": blueToneCurveModel.pointOne, "inputPoint2": blueToneCurveModel.pointTwo, "inputPoint3": blueToneCurveModel.pointThree, "inputPoint4": blueToneCurveModel.pointFour ]) guard let url = Bundle.main.url(forResource: "default", withExtension: "metallib"), let data = try? Data(contentsOf: url), let rgbChannelCompositingKernel = try? CIColorKernel(functionName: "rgbChannelCompositing", fromMetalLibraryData: data) else { return nil } let extent = redImage.extent.union(greenImage.extent.union(blueImage.extent)) let arguments = [redImage, greenImage, blueImage] guard let ciImage = rgbChannelCompositingKernel.apply(extent: extent, arguments: arguments) else { return nil } return ciImage } }少しづつ分解してみていきましょう。
func rgbCompositing(with image: CIImage) -> CIImage? { let redImage = image.applyingFilter( "CIToneCurve", parameters: [ "inputPoint0": redToneCurveModel.pointZero, "inputPoint1": redToneCurveModel.pointOne, "inputPoint2": redToneCurveModel.pointTwo, "inputPoint3": redToneCurveModel.pointThree, "inputPoint4": redToneCurveModel.pointFour ]) let greenImage = image.applyingFilter( "CIToneCurve", parameters: [ "inputPoint0": greenToneCurveModel.pointZero, "inputPoint1": greenToneCurveModel.pointOne, "inputPoint2": greenToneCurveModel.pointTwo, "inputPoint3": greenToneCurveModel.pointThree, "inputPoint4": greenToneCurveModel.pointFour ]) let blueImage = image.applyingFilter( "CIToneCurve", parameters: [ "inputPoint0": blueToneCurveModel.pointZero, "inputPoint1": blueToneCurveModel.pointOne, "inputPoint2": blueToneCurveModel.pointTwo, "inputPoint3": blueToneCurveModel.pointThree, "inputPoint4": blueToneCurveModel.pointFour ])上記の部分では、受け取ったCIImageに対してRed、Green、Blueの各チャンネルの弄りたいToneCurveの値を適応しています。
この時点では、redImage
などに入っているCIImageは、合成チャンネルでトーンカーブを弄った画像になるので理想の色調補正はまだ行われていません。guard let url = Bundle.main.url(forResource: "default", withExtension: "metallib"), let data = try? Data(contentsOf: url), let rgbChannelCompositingKernel = try? CIColorKernel(functionName: "rgbChannelCompositing", fromMetalLibraryData: data) else { return nil }上記部分では、CIColorKernelを生成しています。
Metalファイルは、コンパイル後metallib
形式でrootディレクトリ直下に生成されます。特段設定しなければリソース名はdefault
で書き出される模様です。
Bundle.main.url(forResource:, withExtension:)
で、default.metallib
のパスを取得し、Data(contentsOf:)
でData型としてファイルを取得しています。
最後にCIColorKernel(functionName:, fromMetalLibraryData:)
に対してMetalファイルで定義した画像処理メソッドのメソッド名と取得したdefault.metallib
を引数で渡すと、Metal側で実装したrgbChannelCompositing
が利用可能になります。let extent = redImage.extent.union(greenImage.extent.union(blueImage.extent)) let arguments = [redImage, greenImage, blueImage] guard let ciImage = rgbChannelCompositingKernel.apply(extent: extent, arguments: arguments) else { return nil } return ciImage }続いてCIKernelを用いて、新しい画像を取得します。
extent
は画像の出力範囲です。union
を利用して3つの画像を含むサイズを取得します。今回で言うと元画像が同一であるため基本サイズがずれることはないはずです。
arguments
は、CIKernelに対して渡す元画像の配列です。並び順はrgbです。
extent
とarguments
を引数にし、apply(extent:arguments :)
を実行すると、RGB各チャンネルのToneCurveを調整したCIImageを取得することができます。成果物
こんな感じの画像にネガポジ変換して、色調補正をかけた画像を取得することができます。
Framework化しました
一連の処理を整理してFramework化までしてます。
サンプルコードも一緒にしてあるので気になる方は下記からどうぞ。
(Cocoapods、Carthage対応は追って行います。)
NegaDeveloping - Masami Yamate最後に
探せば世界のどこかでやってる人はいてOSSのFrameworkがすでにあるかもしれませんが、自分で仕組みを辿ってみると楽しいですね。
個人的に使うアプリとして開発していますが、冬休みの課題で間に合えばApp Storeでも現像アプリとして公開するかもしれないので期待せずに待っていただけると嬉しいです。
- 投稿日:2020-12-18T00:56:05+09:00
[GraphQL] Apollo-iOS を使ってみて感じたイケてるところ。イケてないところ。
この記事は iOS #2 Advent Calendar 2020 の18日目の記事です。
概要
今業務でApollo-iOSを導入してます。
ネット上のいろんなサイトを参考し実装することができましたが、思い返すとApollo-iOSの使い方について言及されている記事は多いのですが、実際使ってみての感想はあまり見たことがないので、今回はApollo-iOSのイケてると思った所とイケてないなと思った所について、主観で好き勝手書いてみました。これからGraphQL導入しようか迷っているという方に流し目に読んでいただけると幸いです.
捉え方は要件や環境によると思うので、当てはまらない場合もあると思います。
また、私の知識が及んでいないゆえの間違いなど気づいた点があればTwitter等でコメントいただけると嬉しいです。GraphQLとは
簡単に言うとサーバーとクライアントとの通信をする際のプロトコルのようなもので、特定のライブラリやツールを指すものではありません。
詳しくはすでに様々なサイトで紹介されているので、参考になるであろうリンクを記載しておきます。
GraphQL.org
世のフロントエンドエンジニアにApollo Clientを布教したい
Web API初心者と学ぶGraphQLApollo-iOSとは
iOSアプリからGraphQLでサーバーとやり取りするためのライブラリです。
こちらも参考となるリンクを貼っておきます。
Apollo-iOS
Apollo-iOS Docイケてる所
サーバーエンジニアとの認識齟齬がなくなる
これはApollo-iOSというよりもGraphQLについてですが、GraphiQLやPlayGroundなどのツールを導入することで、ブラウザ上でリクエストを簡単に試したり、付属するDoc機能でAPIの仕様を簡単に把握することができます。
初めてのGraphQLという本でも紹介されていた サンプルでPlayGroundを実際に触ることができます。こうしたGraphQLから提供されたツールを利用することで、API仕様についてのレビューをより効率的に行えるようになり、
先にインターフェースの仕様をかっちりと決めて、その後にサーバーとクライアントがそれぞれ実装に入るというスキーマ駆動がしやすい点がイケてました。また、ページネーションや認証などのよくある処理について、実現パターンがGraphQLコミュニティで策定されており、これを参照することで実装方法や仕様の一貫性を保つことができる点もいいなと思いました。
Interceptorによって通信処理がカスタマイズしやすい
こちらはApollo-iOSについてです。
Apollo-iOSでは、通信、パース、キャッシュの操作などの処理をInterceptorとして個別に定義し、それらを配列にして登録することで順番に処理されるようになります。デフォルトではLegacyInterceptorProviderが定義されていて、この中にInterceptorの配列が定義されています。
Interceptorプロトコルに準拠した構造体を独自の
InterceptorProvider
に組み込むことで、通信前後に任意の処理を行うことができます。例えば、リクエストヘッダーにアクセストークンを付与することも簡単に行なえます。
イケてない所
エラーはスキーマで定義されない
GraphQLでは、エラーはHTTPステータス200として以下のようなJSON形式で返ってきます。
message
やlocations
の値はライブラリによって自動的に格納されます。
extensions
にはサーバーエンジニアが任意で設定した情報が格納されます。{ "errors": [ { "message": "Cannot query field \"n\" on type \"Lift\".", "locations": [ { "line": 3, "column": 5 } ], "extensions": { "code": “HogeError” “message”: “HogeMessage” } } ] }エラーの情報は正常系とは違いスキーマ等には明示されません。
また、GraphQLの仕様としてかっちり仕様がきまっているわけではないようです。認証エラーやその他ビジネスロジックに関わる独自のエラーは
extensions
に格納されて返ってくるはずですが
何をどのタイミングで返すのか、どんな構造なのかはサーバーエンジニアと認識を合わせる必要があります。Apollo-iOSで
extensions
内のcode
にアクセスするには以下のようにします。response?.parsedResponse?.errors?[0].extensions?[“code”] == “HogeError”ここでもし、
extensions
内の構造が想定と違ったり、HogeError
をタイポしたりするとエラーハンドリングに失敗してしまうので、個人的な思いとしてはエラーも可能な限りスキーマで管理できたり、型セーフに扱えたら良かったのにと思った次第です。また、このような200系で返ってくるエラーは
Result.failure
ではなくResult.success
に包まれてハンドラーに返ってくる点にも注意が必要です。各Interceptorのエラー型についてApollo-iOSの内部実装を確認する必要がある
Interceptorにそれぞれ独自のエラーが定義されており( 例: MaxRetryInterceptor)、それぞれの内部実装を確認しながら、Errorをそれぞれの具体的な型にキャストして適したハンドリングをする必要があります。
構造さえ知れば特に不便なこともないんですが、普通にライブラリ内のコードリーディングが求められているので割とハードル高めだなと思った次第です。DDD的な思想とGraphQLの特長がマッチしない部分がある
GraphQLの思想として「(画面ごとに)必要な情報だけ取得できる」という特長がありますが、
DDD的な思想を元にアーキテクチャを組んでいる場合は以下の点でGraphQLの思想とバッティングするんじゃないかと思います。
- 画面ごと構造体を作る == 画面駆動になってしまう。
- 自動生成されたコードをそのままViewで使いたくない。本来データ層だけをApollo-iOSに依存させたい。
- 自動生成されたコードのモックのデータが用意が難しくてドメイン層のテストがしづらい。
- クエリのプロパティをフラグメントで共通化するとコードの構造も変わる。。API都合の変更でView側に修正を加えたくない。
ということで結局は、アプリ内部で使いやすいドメインオブジェクトにマッピングすることになります。その時は複数の画面でも使用できるようにオプショナルな値を持つことになると思います。そしてマッピングのためには扱いづらい自動生成されたコードと向き合う必要があります。
また、マッピングのテストをする場合自動生成された構造体のモックを作る必要があります。
モックをつくるには、以下のようなイニシャライザに渡す引数を用意する必要があります。public init(unsafeResultMap: [String: Any?]) { self.resultMap = unsafeResultMap }引数を作る手順は以下です。
自動生成されたコードに以下のプロパティがあるので、
public private(set) var resultMap: [String: Any?]
実行中にデバッグコンソールでpo print(resultMap)
で値を表示する。1.の出力はそのままSwiftのコードとしても機能するはずなので、
[String: Any]?
の変数として定義すればコンパイルが通るはずです。(膨大な量のコードになるので、型を明示しないとコンパイルが通らないかもしれません。)Interceptorの単体テストは難しい
Interceptorの処理は以下のメソッド内部に書くので、テストするためには引数のモックを用意する必要があります。引数は4つとも独自の型で構成されており、正しいモックを作るには内部のコードを知る必要があり、かなりハードルが高いなと思いました。
func interceptAsync<Operation: GraphQLOperation>( chain: RequestChain, request: HTTPRequest<Operation>, response: HTTPResponse<Operation>?, completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void)ちなみに、Assertionについては、RequestChainを継承したモックを作って引数に渡し、
chain.proceedAsync()
やchain.handleErrorAsync()
などの実行をトラップするのがいいのかなと思います。QueryとMutationで実行するメソッドが違う
GraphQLでは特性に応じて主に3種類のオペレーションが存在します。
その内のQueryはサーバーから値を取得するとき、Mutationはサーバーの値を変える時に使用されます。Apollo-iOSではQueryのリクエストは
fetch
、Mutationのリクエストはperform
を使用する必要があるのですが、
fetch
とperform
を単純に両方使用すると同じような処理を2つ実装する必要がでてくるので、共通化するためにちょっと複雑なコードを書く必要がありました。自動生成したコード内で強制アンラップ祭り
これ原因でクラッシュしたことはありません。ただ、できれば強制アンラップはしてほしくないなって思いました。
- 投稿日:2020-12-18T00:51:36+09:00
脱Auto Layout NavigationView編
はじめに
最近コードでレイアウトを組んでいるのでその備忘録。ネット上にあまり情報がなかったので残しときます。改善点などがあればコメントして欲しいです。?♂️
完成するアプリの動きとしては
![]()
こんな感じ。一つ目のViewController
viewDidLoad()
の中でbarButton
を加えています。これを書いても追加されない時については後述しています。
pushViewController
で遷移しています。モーダル遷移の時はpresent()
を使います。
import UIKit class FirstViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .blue navigationItem.title = "画面1" let barButton = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(addButtonTapped)) navigationItem.rightBarButtonItem = barButton } @objc func addButtonTapped() { print("addButtonTapped") let secondVC = SecondViewController() self.navigationController?.pushViewController(secondVC, animated: true) } }二つ目のViewController
遷移先の画面ですが、特に何もしていません。
view.background
で色を設定しないと遷移の時にカクカクになります。
import UIKit class SecondViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "画面2" view.backgroundColor = .green } }
SceneViewDelegate
UITabBarButtonが出ない!といったトラブルの原因はUINavigationControllerのrootViewControllerにUIBarButtonを加えたいViewControllerを設定することで解決すると思います。
import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var navigationController: UINavigationController?//加えた func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { //ここから guard let _ = (scene as? UIWindowScene) else { return } if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) let firstVC = FirstViewController() navigationController = UINavigationController(rootViewController: firstVC) window.rootViewController = navigationController self.window = window window.makeKeyAndVisible() } //ここまで加えた } //以下は同じ func sceneDidDisconnect(_ scene: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). } func sceneDidBecomeActive(_ scene: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } func sceneWillResignActive(_ scene: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). } func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. } func sceneDidEnterBackground(_ scene: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. } }
- 投稿日:2020-12-18T00:07:47+09:00
[SwiftUI] Qiitaのタグみたいに表示されるTextFieldを作る
SwiftUIのTextFieldはカスタマイズ性に乏しいのですが、見かけだけでもそれっぽくしてみました。
実装
struct TestView: View { @State private var text: String = "" var body: some View { ZStack{ TextField("入力", text: $text) .foregroundColor(.clear) HStack(spacing: 0){ let strings = text.split(separator: " ", omittingEmptySubsequences: false) ForEach(strings.indices, id: \.self){i in Text(strings[i]) .background( Color(.sRGB, red: 0.847, green: 0.917, blue: 0.992) .cornerRadius(5) .overlay( RoundedRectangle(cornerRadius: 5) .stroke(Color(.sRGB, red: 0.745, green: 0.866, blue: 0.988)) ) ) .padding(.horizontal, 2) } Spacer() } } } }ポイント
TextField
は制約が大きすぎるので色をclear
にして見えなくし、その上から好きなように表示します。制約
上からテキストを被せている都合上、カーソルが見えなくなります。これを防ぐためには例えばテキストの背景のカラーを透明色にすればいいのですが、色を合わせるのが大変だったので断念しました。