- 投稿日:2020-08-11T23:15:34+09:00
1分で分かる Cloud Firestore 概要
Cloud Firestore とは?
高速でサーバレスなクラウド NoSQL ドキュメントデータベースです。Firebase Realtime Database と同様にスキーマレスかつリアルタイムにデータを監視することができます。
公式ドキュメント
メリットは?
一番大きなメリットとしては開発コストの大幅な削減です。Firebase Realmtime Database もそうですが、
Cloud Firestore は基本的にサーバレス DB なので、自前でサーバを構築する必要がなくなり。維持コストを削減
することができます。また Cloud Firestore はスキーマレスなのでデータの構造などを柔軟に変えることができ、
開発スピードの向上も見込めます。また、ある程度サービスの規模が小さいまたはテストプロトタイプとして利用する場合は
ほぼ無料で使うことができます。具体的な料金についてはこちらを参照してください。デメリットは?
デメリットとしては、Firebase Realtime Database もそうですが、スキーマレスであるが故に柔軟性がありますが、
その分設計やセキュリティルール
などの設定は難しくなります。また、Cloud Firestore は Firebase Realmtime Database とドキュメントデータベース
になったことで複合クエリができたりして、検索性は大幅に向上していますが、それでも十分とは言えないので、
検索APIの,Algoliaなどの使用も検討しておくと良さそうです。また、
Firebase Realmtime Database はツリー構造のデータベースなので Json による一括インポートなどが可能でした
が、Cloud Firestore はドキュメント型のデータベースなので、そのように小回りのきく操作は難しいそうです。まとめ
Firebase Realtime Database と迷った時は基本的には Cloud Firestore を選択して問題なさそう。ただ、DB の以降で使用したり、ノードの子コレクションに対しても明示的に変更通知を受け取りたい場合などは Firebase Realtime Database の方が良さそう。また、スキーマレスが故に設計やルールの設定などは複雑化したり、検索性が十分ではないことなどのデメリットも存在するが、Firebase 全体としてのスケーラビリティの高さだったり、他のサービスとうまく組み合わせることで開発スピードは飛躍的に向上すると考えられる。
- 投稿日:2020-08-11T23:02:43+09:00
MediaPipeのハンドトラッキング(iOS)をXcodeプロジェクトでビルドしてみた
概要
- 環境構築(MediaPipe)
- ターミナルでビルド
- 環境構築(Tulsi)
- Xcodeプロジェクトでビルド
成果物
Hand Tracking on iOS?#MediaPipe pic.twitter.com/mgHPfUIHwK
— MIWA Tetsushi (@WWWPONTE) August 11, 2020開発環境
- MacBook Pro: Catalina 10.15.4
- Xcode: 11.6
- iPhone SE(第2世代): iOS 13.5.1
環境構築(MediaPipe)
1. Homebrewのインストール
- https://brew.shに表示されているコマンドをコピー
- ターミナルにペーストして実行
- 終わったら以下のコマンドを打ってバージョンが表示されたら完了
$ brew -v # Homebrew 2.4.72. Command Line Tools のインストール
- https://developer.apple.com/download/moreより、Xcodeのバージョンに合ったCommand Line Toolsをダウンロード ※Apple Developer Programへのサインインが必要
- ダウンロードした
.dmg
をクリックしてインストール- 以下のコマンドを打ってライセンスが表示されたら完了
$ sudo xcodebuild -license3. Pythonのバージョン確認
mac には、デフォルトで Python がプリインストールされている。Python のバージョンによって、ビルドが通らないことがあるので、Python のバージョンを確認しておく必要がある。私は、
Python 3.7.5
でビルドが通ったことを確認しているのでこのバージョンの Pythonでビルドを行うことを推奨する。その際に、pyenv
という Python のバージョンを切り替えられるバージョンマネージャーがあるので、それを利用した方法を説明する。
- pyenvをクローン
$ git clone https://github.com/pyenv/pyenv.git ~/.pyenv
- パスを通す
シェルが
zsh
の場合$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zsh_profile $ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zsh_profile $ echo 'eval "$(pyenv init -)"' >> ~/.zsh_profile
Python 3.7.5
をインストール$ pyenv install 3.7.5~/.pyenv/versions/ 配下にインストールした Python が配置される
shim
のリフレッシュ$ pyenv rehash
- 使用するPythonの指定
グローバルで指定する場合
$ pyenv global 3.7.5ローカルで指定する場合
$ pyenv local 3.7.54. sixライブラリのインストール
Python 2 と Python 3 の間の違いを吸収するために、”six” ライブラリをインストールする
$ pip install –user future six5. MediaPipeリポジトリのクローン
$ git clone https://github.com/google/mediapipe.git6. Bazelのインストール
Bazel
をインストール$ brew install bazel
- 終わったら以下のコマンドを打ってバージョンが表示されたら完了
$ bazel --version # bazel 3.3.07. OpenCVとFFmpegのインストール
$ brew install opencv@38. numpyのインストール
$ pip install numpy9. 動作確認
Hello World desktop example
を実行$ export GLOG_logtostderr=1 $ bazel run --define MEDIAPIPE_DISABLE_GPU=1 \ mediapipe/examples/desktop/hello_world:hello_world # しばらくして以下が表示されたら動作確認完了 # Hello World! # Hello World! # Hello World! # Hello World! # Hello World! # Hello World! # Hello World! # Hello World! # Hello World! # Hello World!ターミナルでビルド
まず、ターミナルでハンドトラッキング(iOS)をビルドして、ビルドに必要な準備をする
1. Provisioning Profileの準備
iOS アプリを実機で実行するために、
Provisioning Profile
と呼ばれるiOSデバイスやアプリを識別するためのファイルが必要である。Apple Developer Program 加入者は、https://developer.apple.com/jpより作成してダウンロード可能である。ダウンロードしたファイルをprovisioning_profile.mobileprovision
にリネームして、mediapipe/mediapipe/
に配置する。2. Bundle Identifierの変更
次に、
mediapipe/mediapipe/examples/ios/handtrackinggpu/
の中にあるBUILD
を修正する。bundle_id
をProvisioning Profile
で設定したBundle Identifier
に変更する。mediapipe/examples/ios/handtrackinggpu/BUILD:36bundle_id = BUNDLE_ID_PREFIX + ".HandTrackingGpu", ↓ bundle_id = "(Bundle Identifier)",3. ビルド
mediapipe/
ディレクトリに移動してから以下のコードをターミナルに入力することでビルドが始まる$ bazel build -c opt –config=ios_arm64 mediapipe/example/ios/handtrackinggpu:HandTrackingGpuApp下記ディレクトリにipaファイルができる
bazel-bin/mediapipe/examples/ios/handtrackinggpu/環境構築(Tulsi)
Tulsiを使ってXcodeプロジェクトを生成する
1. Tulsiリポジトリのクローン
$ git clone https://github.com/bazelbuild/tulsi.git2. パッチの適用
$ cd tulsi $ git fetch origin pull/99/head:xcodefix $ git checkout xcodefix3. ビルドスクリプトの実行
sh build_and_run.sh実行してみたら、以下のエラーが発生した
ERROR: /Users/miwa/tulsi/BUILD:62:18: Linking of rule '//:tulsi.__internal__.apple_binary' failed (Exit 1) wrapped_clang failed: error executing command external/local_config_cc/wrapped_clang -Xlinker -objc_abi_version -Xlinker 2 -fobjc-link-runtime -ObjC -arch x86_64 -filelist ... (remaining 26 argument(s) skipped)こちらの記事を参考にしたところ、実行することが出来た
tulsi/WORKSPACE:6tag = "0.17.2", ↓ tag = "0.18.0",4. Tulsi.appでMediaPipe.tulsiprojを開き、Xcodeプロジェクトを生成
- Tulsi.appを起動して、
mediapipe/mediapipe/
配下にあるMediapipe.tulsiproj
を開く
- ConfigsタブにあるGenerateボタンを押下し、Xcodeプロジェクトの保存場所を指定すると生成が開始される
Xcodeプロジェクトでビルド
さあ、macにiPhoneを繋げよう。そして、生成したXcodeプロジェクトを開き、ビルドを開始する。首を長くして待つと、ビルドが完了し、実機へインストールされる。
デフォルトのカメラがフロントカメラなので、リアカメラを使用するように設定
mediapipe/examples/ios/handtrackinggpu/ViewController.mm:107_cameraSource.cameraPosition = AVCaptureDevicePositionFront; ↓ _cameraSource.cameraPosition = AVCaptureDevicePositionBack;左右が反転してしまったので、Mirroredの値を変更
mediapipe/examples/ios/handtrackinggpu/ViewController.mm:111_cameraSource.videoMirrored = YES; ↓ _cameraSource.videoMirrored = NO;おわりに
iPhone上でリアルタイムなハンドトラッキングを高精度で実装することが出来た。
今後は、この技術を利用して視覚障害者向けの学習アプリケーションの開発に取り組んでいこうと思う。
- 投稿日:2020-08-11T18:55:13+09:00
今更だけどUINavigationControllerについて学びなおそう!
はじめに
UINavigationController
は今まで作ったほぼ全てのアプリで使っていましたが、わりとふわっと使っていたので改めて使い方を学びなおそう!ということでまとめました。
SwiftUI が出てきましたがまだまだUINavigationController
も現役だと思います。UINavigationControllerの構成
UINavigationController
はUIViewController
を継承した ViewController をスタックで管理するコンテナクラスです。(この説明でいいかわからんけど。。。)push した ViewController はUINavigationController
の childeViewController として設定されています。(ContainerView みたいな感じで)class UINavigationController : UIViewController構成は下記のようになっています。
引用:UINavigationControllerドキュメント
画面上部に表示する
navigationBar
、画面下部に表示するtoolbar
(デフォルト非表示)、push した ViewController 一覧(viewControllers
)と ViewController の表示時などに通知を受け取るdelegate
で構成されています。単純に
UINavigationController
を利用する場合は特にtoolbar
、delegate
を意識する必要はないのですが今回はこのあたりも軽く触れようと思います。(あんま使ったことないので軽めに)UINavigationControllerのプロパティやメソッド
UINavigationController
には様々なプロパティやメソッドが用意されています。遷移系
// NavigationスタックのトップのVC var topViewController: UIViewController? // 表示しているトップのVC(モーダルがあればモーダルのやつ) var visibleViewController: UIViewController? // NavigationスタックのVC一覧 var viewControllers: [UIViewController] // Navigationスタックを設定して遷移する func setViewControllers([UIViewController], animated: Bool) // プッシュして遷移する func pushViewController(UIViewController, animated: Bool) // ポップして戻る func popViewController(animated: Bool) -> UIViewController? // ルート以外ポップして遷移する func popToRootViewController(animated: Bool) -> [UIViewController]? // 指定のVCまでポップして指定のVCに遷移する func popToViewController(UIViewController, animated: Bool) -> [UIViewController]? // スワイプで戻るジェスチャ var interactivePopGestureRecognizer: UIGestureRecognizer?使い方
First Second Third 上記のような3画面構成の場合コードで遷移しようと思うとそれぞれ下記のようになります。
// MARK:- FirstViewController // First -> Secondの遷移 @IBAction private func pushToSecond(_ sender: Any) { let vc = storyboard?.instantiateViewController(identifier: "Second") as! SecondViewController navigationController?.pushViewController(vc, animated: true) } // First -> Thirdの遷移 @IBAction private func pushToThird(_ sender: Any) { let second = storyboard?.instantiateViewController(identifier: "Second") as! SecondViewController let third = storyboard?.instantiateViewController(identifier: "Third") as! ThirdViewController navigationController?.setViewControllers([self, second, third], animated: true) } // MARK:- SecondViewController // Second -> Thirdの遷移 @IBAction private func pushToThird(_ sender: Any) { let vc = storyboard?.instantiateViewController(identifier: "Third") as! ThirdViewController navigationController?.pushViewController(vc, animated: true) } // Second -> Firstの遷移 @IBAction private func popToFirst(_ sender: Any) { navigationController?.popViewController(animated: true) } // MARK:- ThirdViewController // Third -> Firstの遷移 @IBAction private func popToFirst(_ sender: Any) { // FirstはrootViewControllerなのでこれでもいける // navigationController?.popToRootViewController(animated: true) navigationController?.popToViewController(navigationController!.viewControllers.first!, animated: true) } // Third -> Secondの遷移 @IBAction private func popToSecond(_ sender: Any) { navigationController?.popViewController(animated: true) }Third 表示時の各プロパティは下記のようになります
navigationController?.viewControllers // [FirstViewController, SecondViewController, ThirdViewController] navigationController?.topViewController // ThirdViewController navigationController?.visibleViewController // ThirdViewControllerナビゲーションバー・ツールバー系
知らなかったけどバーを非表示にする色々なフラグがあるようです。
// ナビゲーションバー var navigationBar: UINavigationBar // ナビゲーションバーの表示・非表示を切り替える func setNavigationBarHidden(Bool, animated: Bool) // ツールバー var toolbar: UIToolbar! // ツールバーのの表示・非表示を切り替える func setToolbarHidden(Bool, animated: Bool) // ツールバーのの表示・非表示を切り替える var isToolbarHidden: Bool // setNavigationBarHidden/setToolbarHiddenのアニメーション時間 class let hideShowBarDuration: CGFloat // タップでバーを非表示にするかどうか var hidesBarsOnTap: Bool // スワイプでバーを非表示にするかどうか var hidesBarsOnSwipe: Bool // 垂直方向にコンパクトな環境でナビゲーションコントローラーがバーを非表示にするかどうかを示すブール値。 var hidesBarsWhenVerticallyCompact: Bool // キーボード表示時にバーを非表示にするかどうか var hidesBarsWhenKeyboardAppears: Bool // ナビゲーションバーが非表示かどうか var isNavigationBarHidden: Bool // バー非表示のタップジェスチャ var barHideOnTapGestureRecognizer: UITapGestureRecognizer // バー非表示のスワイプジェスチャ var barHideOnSwipeGestureRecognizer: UIPanGestureRecognizernavigationBar
ちょっとややこしいのが
navigationBar
。。。構成は下記のようになっています。
leftBarButtonItem
(backBarButtonItem
)、rightBarButtonItem
、title
(titleView
)、prompt
の4つで構成されています。この4つは
ViewController
ごとに設定できViewController
のnavigationItem
からアクセスできます。UINavigationController
はnavigationBar
のdelegate
、popItem
、pushItem
を自動で設定してくれるのでUINavigationController
を利用する場合はそれぞれのViewController
でnavigationItem
を設定しておけばpopItem
、pushItem
を気にする必要はありません。(ViewController
を push/pop したときに item も push/pop してくれるはず)// ナビゲーションバーのスタイル // default: 白 // black: 黒 var barStyle: UIBarStyle // ナビゲーションバーが半透明かどうか var isTranslucent: Bool // タイトルを大きく表示するかどうか var prefersLargeTitles: Bool // 標準の高さのナビゲーションバーのAppearance var standardAppearance: UINavigationBarAppearance // iPhone横向き??のときの高さのナビゲーションバーのAppearance var compactAppearance: UINavigationBarAppearance? // largeTitle??のときの高さのナビゲーションバーのAppearance var scrollEdgeAppearance: UINavigationBarAppearance? // 戻るボタンの「<」部分の画像 // backIndicatorTransitionMaskImageも設定しないといけない var backIndicatorImage: UIImage? // push/pop時の画像 var backIndicatorTransitionMaskImage: UIImage? // ナビゲーションバーの影画像 // setBackgroundImage(_:for:)で画像を設定しないと反映されない var shadowImage: UIImage? // ナビゲーションバーの背景色 var barTintColor: UIColor? // ナビゲーションバーのタイトルのアトリビュート var titleTextAttributes: [NSAttributedString.Key : Any]? // ナビゲーションバーのラージタイトルのアトリビュート var largeTitleTextAttributes: [NSAttributedString.Key : Any]? // メトリックがよくわからない。。。(たぶんcompactがiPhone横向きの場合だと思う) // 指定されたバーメトリックの背景画像を返す func backgroundImage(for: UIBarMetrics) -> UIImage? // 指定されたバーメトリックの背景画像を設定する func setBackgroundImage(UIImage?, for: UIBarMetrics) // 指定された位置とバーメトリックの背景画像を返す func backgroundImage(for: UIBarPosition, barMetrics: UIBarMetrics) -> UIImage? // 指定された位置とバーメトリックの背景画像を設定する func setBackgroundImage(UIImage?, for: UIBarPosition, barMetrics: UIBarMetrics) // 指定されたバーメトリックのタイトルの垂直位置調整を返す func titleVerticalPositionAdjustment(for: UIBarMetrics) -> CGFloat // 特定のバーメトリックのタイトルの垂直位置調整を設定する func setTitleVerticalPositionAdjustment(CGFloat, for: UIBarMetrics)気をつけないといけないのが
navigationItem
の設定はViewController
ごとですがnavigationBar
の設定はUINavigationController
のnavigationBar
に設定するので VC を push しようが pop しようが変わらないということ!(すべてのナビゲーションバーで設定を変えたい場合はAppDelegate
などでUINavigationBar.appearance()
に設定してやるといけます。MFMailComposeViewController
やUIActivityViewController
の表示がおかしくなる場合があるのでappearance
の扱いには注意が必要です!)barStyle は default, black の2パターンあり、
isTranslucent
と組み合わせると下記のようになります。
default black default(透過なし) black (透過なし) navigationController?.navigationBar.barTintColor = .systemOrange navigationController?.navigationBar.tintColor = .systemTeal navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.systemPurple] title = "Title" navigationItem.prompt = "Prompt" let left1 = UIBarButtonItem(title: "left1", style: .done, target: nil, action: nil) let left2 = UIBarButtonItem(title: "left2", style: .done, target: nil, action: nil) left2.tintColor = .systemGreen navigationItem.leftBarButtonItems = [left1, left2] let right1 = UIBarButtonItem(title: "right1", style: .done, target: nil, action: nil) let right2 = UIBarButtonItem(title: "right1", style: .done, target: nil, action: nil) right2.tintColor = .systemPink navigationItem.rightBarButtonItems = [right1, right2]上記のように設定するとこんな感じになります
prompt の色を設定するプロパティはなさそう。。。
navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.systemPurple] navigationController?.navigationBar.prefersLargeTitles = true上記のようにして largeTitle 表示に変更するとこんな感じになります
largeTitle の表示は
navigationItem
の下記プロパティで制御できます// automatic/always/neverの3パターン var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayModeUIBarbuttonItem と titleView は View を設定できるので下記のようにすると
navigationController?.navigationBar.barTintColor = .systemOrange navigationController?.navigationBar.tintColor = .systemTeal navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.systemPurple] // largeTitleの場合 // navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.systemPurple] // navigationController?.navigationBar.prefersLargeTitles = true title = "Title" navigationItem.prompt = "Prompt" let leftSegment = UISegmentedControl(items: ["first", "second"]) leftSegment.selectedSegmentIndex = 0 navigationItem.leftBarButtonItem = UIBarButtonItem(customView: leftSegment) let rightSegment = UISegmentedControl(items: ["first", "second"]) rightSegment.selectedSegmentIndex = 1 navigationItem.rightBarButtonItem = UIBarButtonItem(customView: rightSegment) navigationItem.titleView = UISearchBar()こんな感じの表示もできます!
デフォルト largeTitle 背景画像
shadowImage
に関しては下記のようにしてみるとわかりやすいかも// 背景画像非表示 navigationController?.navigationBar.setBackgroundImage(.init(), for: .default) // 影画像非表示 navigationController?.navigationBar.shadowImage = .init()
デフォルト 背景画像非表示 背景画像と影画像非表示 背景画像のいい感じの大きさはわからないですが、スライスとかストレッチでいい感じにこっちでしないといけないのかも。。。
backBarButtonItem
UINavigationController
で扱いが難しいのが戻るボタン。。。標準だと1つ前の VC のタイトルが表示され、長い場合は「戻る」表示になる便利なやつだけどカスタムしようと思うと色々難しかったりします。
デフォルト タイトルが長い場合 戻るボタンをカスタムする場合は戻るボタンを表示する VC の前の VC で設定する必要があります。
// Second の戻るボタンをカスタムする場合は First で設定する // 1.文字を非表示にしたい場合 navigationItem.backBarButtonItem = .init(title: "", style: .plain, target: nil, action: nil) // 2.タイトル以外の文字を表示したい場合 navigationItem.backBarButtonItem = .init(title: "Hoge", style: .plain, target: nil, action: nil) // 3.画像を設定する場合 navigationItem.backBarButtonItem = .init(image: UIImage(systemName: "trash"), style: .plain, target: nil, action: nil)
1.文字を非表示 2.タイトル以外の文字表示 3.画像を表示 backBarButtonItemドキュメントに下記のようにあるので
backBarButtonItem
にカスタム View を設定しても無視されるらしいです。When configuring your bar button item, do not assign a custom view to it; the navigation item ignores custom views in the back bar button anyway.
戻るボタン押下時にアラートを表示したいなどイベントを取得したい場合はおとなしく戻るボタンは使わずに
leftBarButtonItem
を設定する方が無難かと思います。leftBarButtonItem
を設定する場合はスワイプで戻るジェスチャも無効になります。そんなパターンがあるのかわかりませんがleftBarButtonItem
を設定したけどスワイプで戻るは有効にしたい場合は下記のようにするとたぶん動きます(やったことはないです。。。)navigationController?.interactivePopGestureRecognizer?.delegate = self詳細は下記参考
UINavigationControllerのスワイプで戻るを有効・無効にする方法「<」の画像を変更したい場合は下記のように設定する。
navigationController?.navigationBar.backIndicatorImage = UIImage(named: "back") navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "back")画像は16*14で作成すると下記のようになりました。
縦向き 横向き largeTitle UINavigationBarAppearance
今までは直接
navigationController?.navigationBar.barTintColor = .systemOrange
のようにプロパティ設定をしていましたが、iOS 13 からはUINavigationBarAppearance
の設定でも変更できるようになっています。
ナビゲーションバーには下記の3つの表示状態があり、それぞれに対応するUINavigationBarAppearance
のプロパティ (standardAppearance
,compactAppearance
,scrollEdgeAppearance
) がUINavigationBar
には用意されているので別々に外観を設定することができます。
standardAppearance compactAppearance scrollEdgeAppearance ちなみに下記のように設定するとスクロール View がある場合、トップでは largeTitle 表示でスクロールするとデフォルト表示に切り替わるようになります。
navigationController?.navigationBar.prefersLargeTitles = true navigationItem.largeTitleDisplayMode = .automaticそれぞれの
UINavigationBarAppearance
を下記のように設定してやるとlet standard = UINavigationBarAppearance() standard.configureWithDefaultBackground() standard.titleTextAttributes = [.foregroundColor: UIColor.systemRed] standard.backgroundColor = .systemOrange let compact = UINavigationBarAppearance() compact.configureWithDefaultBackground() compact.titleTextAttributes = [.foregroundColor: UIColor.systemPurple] compact.backgroundColor = .systemYellow let scrollEdge = UINavigationBarAppearance() scrollEdge.configureWithDefaultBackground() scrollEdge.titleTextAttributes = [.foregroundColor: UIColor.systemTeal] scrollEdge.backgroundColor = .systemGreen navigationController?.navigationBar.standardAppearance = standard navigationController?.navigationBar.compactAppearance = compact navigationController?.navigationBar.scrollEdgeAppearance = scrollEdgeこんな感じになります
UINavigationBarAppearance
には下記のようにボタンの Appearance のプロパティがあるのでそれぞれのボタンの見た目も設定できそうです。var buttonAppearance: UIBarButtonItemAppearance var doneButtonAppearance: UIBarButtonItemAppearance var backButtonAppearance: UIBarButtonItemAppearancetoolbar
navigationItem
同様ViewController
の下記のプロパティやメソッドで VC ごとに設定ができます。var hidesBottomBarWhenPushed: Bool var toolbarItems: [UIBarButtonItem]? func setToolbarItems(_ toolbarItems: [UIBarButtonItem]?, animated: Bool)navigationController?.isToolbarHidden = false navigationController?.toolbar.barTintColor = .systemOrange navigationController?.toolbar.tintColor = .red let left = UIBarButtonItem(title: "left", style: .plain, target: nil, action: nil) let right = UIBarButtonItem(title: "right", style: .plain, target: nil, action: nil) let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) setToolbarItems([left, space, right], animated: false) // こっちでも可 // toolbarItems = [left, space, right]上記のように設定するとこんな感じになります
UINavigationControllerDelegate
UINavigationController
にはUINavigationControllerDelegate
というデリゲートがあります。(使ったことないけど。。。)内容は下記。// VCが表示される前に呼ばれる func navigationController(UINavigationController, willShow: UIViewController, animated: Bool) // VCが表示された後に呼ばれる func navigationController(UINavigationController, didShow: UIViewController, animated: Bool) // アニメーション設定?? func navigationController(UINavigationController, animationControllerFor: UINavigationController.Operation, from: UIViewController, to: UIViewController) -> UIViewControllerAnimatedTransitioning? func navigationController(UINavigationController, interactionControllerFor: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? // 画面の向きの優先度?? func navigationControllerPreferredInterfaceOrientationForPresentation(UINavigationController) -> UIInterfaceOrientation // ナビゲーションコントローラーでサポートされている画面の向き?? func navigationControllerSupportedInterfaceOrientations(UINavigationController) -> UIInterfaceOrientationMask気をつけないといけないのが First, Second など各 VC でデリゲートを設定してしまうと Second 表示時は First のデリゲート設定が解除され First の
viewWillAppear
などで再びデリゲート設定しないと First のデリゲート設定は解除されたままになってしまうということ。UINavigationController
のデリゲートは各 VC で設定するのではなくカスタムのUINavigationController
クラスを作ってそのクラスでデリゲート設定をするか別オブジェクトに設定するもしくは rootViewController でのみ設定するなどの工夫がいると思います。(使ったことないですが。。。)おわりに
やっぱり調べてみると知らないことがわりとありました。。。toolbar とかデフォルトであったんだとか prompt とか largeTitle とかも使ってなかったから忘れてたなどなど。。。
SwiftUI の登場で今後使う機会が減っていくかもしれませんが、まだまだ
UINavigationController
は現役だと思うのでこの記事がどこかで役に立つことを願います参考
- 投稿日:2020-08-11T18:25:31+09:00
nilを保持できる「オプショナル型」について。【 ?, ! 】
オプショナル型とは
「nil」を保持できる、『データ型』
nil = 空の(値が無い)状態オプショナル型とは、「変数にnilの代入を許容するデータ型」。
変数の宣言時に使用。
「nilを保持することができる変数」とも言える。オプショナルの宣言
「?」を使う。
var text: String?※ 変数を作る = 「変数を宣言する」
なぜ、オプショナルが必要なのか。?
Swiftで「nil」の値を参照しようとすると、他のプログラミング言語同様、
アプリケーションが落ちてしまう。nilを許容する「オプショナル型」を使うことで、そのような問題を解決。
また、変数にnilを許容するか明示的になり、変数のnilチェックを省くことができて効率的。
つまり、Appleの「安全のための設計」の一つ。
そもそも、なぜ「nil」が必要なのか。?
なぜアプリケーションが落ちる危険があるにも関わらずnilを扱うのでしょうか。
Facebookではユーザーの名前は必須の項目ですが、画像の設定は任意です。
var name: String var image: UIImage?画像は任意なので、存在しない場合があります。
そのようなユーザーを考えると、変数imageは「nil」でなくてはいけません。通常の型(=非オプショナル型)との違い
- 非オプショナル型の変数
値を代入しないと、使うことができない。
nil cannot be assigned to type // nilは代入できないので、エラー。- オプショナル型の変数
値を入れないときには、自動でnilが代入され、実行できる。
nilOptionalの構造を、見てみた。?
因みに
: xxx?
は、: Optional<xxx>
の簡易verらしい。var optionalString : String? // 簡単な方法ver。「?」 付けるだけ。 var optional : Optional<String> // これでもOK実行すると、こんな感じ。(もちろん、変数名は自由)
「?」のヤツと、同じ扱い。var optionalString : Optional<String> // Optionをクリック print(optionalString) // ---> nilOptionalを、Commandと同時にクリックして、定義を見てみた。?
@frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral { /// The absence of a value. /// /// In code, the absence of a value is typically written using the `nil` /// literal rather than the explicit `.none` enumeration case. case none /// The presence of a value, stored as `Wrapped`. case some(Wrapped) // 以下、省略。
case
は2つ。
- none(=何もない)。つまり、nil
- some(=何かある)。
ん、Wrapped..?
ラップされている状態...?
そもそも、Optional(値)とはどのような状態??
オプショナル型は、Optionalというラップ1枚で、値を包み込んでいるイメージです。
var optionalString : String? // オプショナル型を使用 (nil、OK状態) optionalString = "Apple" // "Apple" という値を、変数に代入? print(optionalString) // ターミナルにprintすると...ターミナルにprintすると、こんな感じ。
Optional("Apple")裸の
Apple
ではありません。
Optionalというラップで包まれたAppleです。値を代入しなくても、ラップだけは存在。
「nil」でも包み紙だけは存在するため、とりあえず扱うことができます。?
オプショナル型の値が「nil」でも使えるのは、この包み紙が存在しているためです。
アンラップ(Unwrap)
しかし、この便利な包み紙があるために値はラッピングされ、直接扱うことができません。
扱うためにはラップを取り除き、中身の値を取り出さなくてはいけません。この包み紙を取り除き、値を取り出すことを
アンラップ
と言います。オプショナル型の、アンラップ
// IntとInt?は、別の型 var num1: Int = 1 var num2: Int? = 2 print(num1 + 1); // 2 print(num2 + 1); // エラー // ※ 計算せずに、num2をprintするだけなら、一応できます。num2は、Int型として扱えません。
オプショナル型にはWrapがかかっていて、別の型として扱われるためです。オプショナル型の変数の値がnilでないことを明示するため、
アンラップ
する必要があります。アンラップ方法
いくつかあるかもですが、今回は2つ。
1. 『!』
『!』 を最後に付ける。
var num2: Int? = 2 // ---> nilだと、エラー。 print(num2!) // 22. Optional binding
オプショナル型は、値がnilのときは
FALSE
、それ以外はTRUE
を返します。?//オプショナル型の宣言 var num2: Int? // num2 = xxx (代入は、今回しません) //Binding if let number = num2 { print("number") } else { print("nilだよ") }実行すると、
nilだよ // num2 = xxx をしていないから。binding
オプショナル型を用いて比較することを、バインディング(Binding)と呼びます。
注意⚠️
2を代入せず、nilで『!』のアンラップをすると、エラー起きます。
- 『!』 値が指定されていないと、エラー。
- Optional binding 値が指定されていなくても、大丈夫。
終わりに
暗黙的なオプショナル型とかは、省略。
気になる方は、おググリなさって下さい。参考サイト
- 投稿日:2020-08-11T12:44:33+09:00
EfficientnetをCore MLに変換する【変換済みモデルあり】
TensorFlow Hubのモデルコレクションからダウンロードしてモデルを構築。
m = tf.keras.Sequential([ hub.KerasLayer("https://tfhub.dev/tensorflow/efficientnet/b0/classification/1") ]) m.build([1, 224, 224, 3]) m.summary()クラスラベルを読み込んでおきます。
(今回はImageNetの1000クラスでそのまま使用。TensorFlowHubには転移学習のできるバージョンも公開されています。)
import urllib label_url = 'https://storage.googleapis.com/download.tensorflow.org/data/ImageNetLabels.txt' class_labels = urllib.request.urlopen(label_url).read().splitlines() class_labels = class_labels[1:] # remove the first class which is background assert len(class_labels) == 1000 # make sure entries of class_labels are strings for i, label in enumerate(class_labels): if isinstance(label, bytes): class_labels[i] = label.decode("utf8")CoreMLTools4.0で変換します。
import coremltools as ct image_input = ct.ImageType(shape=(1, 224, 224, 3,), scale=1/255) mlmodel = ct.convert(m, inputs=[image_input], classifier_config=classifier_config, ) mlmodel.save('./efficientnet.mlmodel')Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-08-11T09:20:13+09:00
bundler: failed to load command: pod が出た場合の対応
Xcodeのバージョンを新しくした後にcocoapodsを更新しようとターミナルで
bundle exec pod installを叩いた時にエラーが発生
$ bundle exec pod install bundler: failed to load command: pod (/Users/tamappe/Documents/workspace/iOS/App/vendor/bundle/ruby/2.3.0/bin/pod) RuntimeError: Failed to extract git version from `git –version` (“xcrun: error: active developer path (\”/Applications/Xcode_11.app/Contents/Developer\”) does not exist\nUse `sudo xcode-select –switch path/to/Xcode.app` to specify the Xcode that you wish to use for command line developer tools, or use `xcode-select –install` to install the standalone command line developer tools.\nSee `man xcode-select` for more details.\n”) /Users/tamappe/Documents/workspace/iOS/App/vendor/bundle/ruby/2.3.0/gems/cocoapods-1.6.1/lib/cocoapods/command.rb:118:in `git_version’ /Users/tamappe/Documents/workspace/iOS/App/vendor/bundle/ruby/2.3.0/gems/cocoapods-1.6.1/lib/cocoapods/command.rb:130:in `verify_minimum_git_version!’ /Users/tamappe/Documents/workspace/iOS/App/vendor/bundle/ruby/2.3.0/gems/cocoapods-1.6.1/lib/cocoapods/command.rb:49:in `run’ /Users/tamappe/Documents/workspace/iOS/App/vendor/bundle/ruby/2.3.0/gems/cocoapods-1.6.1/bin/pod:55:in `<top (required)>’ /Users/tamappe/Documents/workspace/iOS/App/vendor/bundle/ruby/2.3.0/bin/pod:23:in `load’ /Users/tamappe/Documents/workspace/iOS/App/vendor/bundle/ruby/2.3.0/bin/pod:23:in `<top (required)>’に関するエラー対応
たまにちょいちょい起こることがあります。
着目すべきは
(\"/Applications/Xcode_11.app/Contents/Developer\") does not exist\nUse `sudo xcode-select --switch path/to/Xcode.app` to specify the Xcode that you wish to use for command line developer toolsです。
新しいXcodeをダウンロードしたり複数のXcodeをApplicationフォルダに入れておくと起こるエラーなので、使うXcodeのpathを指定して
$ sudo xcode-select --switch path/to/Xcode.appを叩くと直りました。
てっきり前半の
failed to load command: pod
でpodやrubyのバージョンによるエラーだと勘違いしてしまったので注意が必要です。
宣伝
個人ブログを新しくリニューアルしました。
- 投稿日:2020-08-11T09:17:03+09:00
PyTorchのモデルをiOSで利用する - LibTorchをiOSプロジェクトに組み込む手順
PyTorchで作成した.ptモデルをiOSで直接(Core MLモデルに変換せずに)使う方法。
MetalやNeural Engineに最適化されることが期待されるので基本的にはCore MLに変換してから使ったほうが良いのだが、
- PyTorchモデルをCore ML Toolsで変換するにはいったんONNXフォーマットに変換するといった煩雑さがある1
- PyTorchモデルをAndroidと共通で使いたい
こういった場合にそのまま直接組み込むという線も出てくる。
PyTorchモデルを扱うC++ライブラリがCocoaPods対応してるので、自分のアプリへの導入はめちゃくちゃ簡単。
以下その手順。
1. LibTorchのインストール
Podfile
に以下を追記して、pod 'LibTorch', '~>1.5.0'
pod install
を実行。2. プロジェクト設定の変更
- ビットコードを無効化("Build Settings"の"Enable Bitcode"で
NO
を指定)- その他プロジェクトに応じて設定変更が必要な箇所も
3. PyTorchモデルを追加する
.ptファイルをXcodeプロジェクトに追加する。
4. ブリッジ実装を書く
LibTorchとモデルを使って推論処理を行うラッパーをObjective-C++で実装する。ここはモデルによって実装が変わってくる。
たとえば公式サンプルのHelloWorldに入っている
TorchModule.h/mm
の実装はこんな感じ:TorchModule.h#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface TorchModule : NSObject - (nullable instancetype)initWithFileAtPath:(NSString*)filePath NS_SWIFT_NAME(init(fileAtPath:))NS_DESIGNATED_INITIALIZER; + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (nullable NSArray<NSNumber*>*)predictImage:(void*)imageBuffer NS_SWIFT_NAME(predict(image:)); @end NS_ASSUME_NONNULL_ENDTorchModule.mm#import "TorchModule.h" #import <LibTorch/LibTorch.h> @implementation TorchModule { @protected torch::jit::script::Module _impl; } - (nullable instancetype)initWithFileAtPath:(NSString*)filePath { self = [super init]; if (self) { try { _impl = torch::jit::load(filePath.UTF8String); _impl.eval(); } catch (const std::exception& exception) { NSLog(@"%s", exception.what()); return nil; } } return self; } - (NSArray<NSNumber*>*)predictImage:(void*)imageBuffer { try { at::Tensor tensor = torch::from_blob(imageBuffer, {1, 3, 224, 224}, at::kFloat); torch::autograd::AutoGradMode guard(false); at::AutoNonVariableTypeMode non_var_type_mode(true); auto outputTensor = _impl.forward({tensor}).toTensor(); float* floatBuffer = outputTensor.data_ptr<float>(); if (!floatBuffer) { return nil; } NSMutableArray* results = [[NSMutableArray alloc] init]; for (int i = 0; i < 1000; i++) { [results addObject:@(floatBuffer[i])]; } return [results copy]; } catch (const std::exception& exception) { NSLog(@"%s", exception.what()); } return nil; } @end5. Swiftから呼ぶ
4で実装したクラスをSwiftから使って推論処理を行う。
たとえばHelloWorldサンプルでは次のようにモデルファイル(
model.pt
)のパスを渡してTorchModule
クラスを初期化している:private lazy var module: TorchModule = { if let filePath = Bundle.main.path(forResource: "model", ofType: "pt"), let module = TorchModule(fileAtPath: filePath) { return module } else { fatalError("Can't find the model file!") } }()推論処理の実行:
let resizedImage = image.resized(to: CGSize(width: 224, height: 224)) guard var pixelBuffer = resizedImage.normalized() else { return } guard let outputs = module.predict(image: UnsafeMutableRawPointer(&pixelBuffer)) else { return }ちなみに・・・このサンプルの実装でいうとリサイズやノーマライズといったピクセルデータにアクセスする(=GPU向き)前処理をCPUで行っていて、やっぱり基本的には(PyTorch Mobile/LibTorchを使うのではなく)Core MLを利用して前処理〜推論処理まで一貫してGPU(Metal)およびNeural Engineで行うようにしたほうが良いように思う。
Neural Engineについては以下の記事を参照:
coremltools 4.0から直接Core MLモデルに変換できるようになったが、まだベータなのと、ちょっと使ってみた感じでは生成されるモデルがiOS 14以上でしか使用できない ↩
- 投稿日:2020-08-11T08:43:52+09:00
AnimeGANv2をCore MLに変換してiOSでつかう【変換済みモデルあり】
変換済みモデル(Hayao,Paprika)GitHubリンク
8.6MBの軽量モデル。
グラフをpbtxt形式で保存します。
test.pytf.train.write_graph(sess.graph_def, './', 'animegan.pbtxt')アウトプットノードの名前を調べます。
test.pygraph = sess.graph print([node.name for node in graph.as_graph_def().node])凍結グラフを作ります。
from tensorflow.python.tools.freeze_graph import freeze_graph import tfcoreml graph_def_file = 'animegan.pbtxt' checkpoint_file = 'checkpoint/generator_Hayao_weight/Hayao-64.ckpt' frozen_model_file = './frozen_model.pb' output_node_names = 'generator/G_MODEL/out_layer/Tanh' freeze_graph(input_graph=graph_def_file, input_saver="", input_binary=False, input_checkpoint=checkpoint_file, output_node_names=output_node_names, restore_op_name="save/restore_all", filename_tensor_name="save/Const:0", output_graph=frozen_model_file, clear_devices=True, initializer_nodes="")変換します。
input_tensor_shapes = {'test:0':[1, 256, 256, 3]} # batch size is 1 # Output CoreML model path coreml_model_file = './animegan.mlmodel' output_tensor_names = ['generator/G_MODEL/out_layer/Tanh:0'] # Call the converter coreml_model = tfcoreml.convert( tf_model_path='frozen_model.pb', mlmodel_path=coreml_model_file, input_name_shape_dict=input_tensor_shapes, output_feature_names=output_tensor_names, image_input_names='test:0', red_bias=-1, green_bias=-1, blue_bias=-1, image_scale=2/255, minimum_ios_deployment_target='12' )Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。