20200811のiOSに関する記事は8件です。

1分で分かる Cloud Firestore 概要

Cloud Firestore とは?

高速でサーバレスなクラウド NoSQL ドキュメントデータベースです。Firebase Realtime Database と同様にスキーマレスかつリアルタイムにデータを監視することができます。

image.png

公式ドキュメント

メリットは?

一番大きなメリットとしては開発コストの大幅な削減です。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 全体としてのスケーラビリティの高さだったり、他のサービスとうまく組み合わせることで開発スピードは飛躍的に向上すると考えられる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MediaPipeのハンドトラッキング(iOS)をXcodeプロジェクトでビルドしてみた

概要

  1. 環境構築(MediaPipe)
  2. ターミナルでビルド
  3. 環境構築(Tulsi)
  4. Xcodeプロジェクトでビルド

成果物

開発環境

  • 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.7

2. Command Line Tools のインストール

  • https://developer.apple.com/download/moreより、Xcodeのバージョンに合ったCommand Line Toolsをダウンロード ※Apple Developer Programへのサインインが必要
  • ダウンロードした.dmgをクリックしてインストール
  • 以下のコマンドを打ってライセンスが表示されたら完了
$ sudo xcodebuild -license

3. 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.5

4. sixライブラリのインストール

Python 2 と Python 3 の間の違いを吸収するために、”six” ライブラリをインストールする

$ pip install –user future six

5. MediaPipeリポジトリのクローン

$ git clone https://github.com/google/mediapipe.git

6. Bazelのインストール

  • Bazelをインストール
$ brew install bazel
  • 終わったら以下のコマンドを打ってバージョンが表示されたら完了
$ bazel --version
# bazel 3.3.0

7. OpenCVとFFmpegのインストール

$ brew install opencv@3

8. numpyのインストール

$ pip install numpy

9. 動作確認

  • 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_idProvisioning Profileで設定したBundle Identifierに変更する。

mediapipe/examples/ios/handtrackinggpu/BUILD:36
bundle_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.git

2. パッチの適用

$ cd tulsi
$ git fetch origin pull/99/head:xcodefix
$ git checkout xcodefix

3. ビルドスクリプトの実行

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:6
tag = "0.17.2",
         ↓
tag = "0.18.0",

4. Tulsi.appでMediaPipe.tulsiprojを開き、Xcodeプロジェクトを生成

  • Tulsi.appを起動して、mediapipe/mediapipe/配下にあるMediapipe.tulsiprojを開く

Tulsi_Start.png

  • ConfigsタブにあるGenerateボタンを押下し、Xcodeプロジェクトの保存場所を指定すると生成が開始される

Tulsi_Generate.png

Xcodeプロジェクトでビルド

さあ、macにiPhoneを繋げよう。そして、生成したXcodeプロジェクトを開き、ビルドを開始する。首を長くして待つと、ビルドが完了し、実機へインストールされる。

Xcode_handtrackinggpu.png

デフォルトのカメラがフロントカメラなので、リアカメラを使用するように設定

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上でリアルタイムなハンドトラッキングを高精度で実装することが出来た。

今後は、この技術を利用して視覚障害者向けの学習アプリケーションの開発に取り組んでいこうと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更だけどUINavigationControllerについて学びなおそう!

はじめに

UINavigationController は今まで作ったほぼ全てのアプリで使っていましたが、わりとふわっと使っていたので改めて使い方を学びなおそう!ということでまとめました。
SwiftUI が出てきましたがまだまだ UINavigationController も現役だと思います。

UINavigationControllerの構成

UINavigationControllerUIViewController を継承した ViewController をスタックで管理するコンテナクラスです。(この説明でいいかわからんけど。。。)push した ViewController は UINavigationController の childeViewController として設定されています。(ContainerView みたいな感じで)

class UINavigationController : UIViewController

構成は下記のようになっています。

navigationcontroller

引用:UINavigationControllerドキュメント

画面上部に表示する navigationBar、画面下部に表示する toolbar (デフォルト非表示)、push した ViewController 一覧(viewControllers)と ViewController の表示時などに通知を受け取る delegate で構成されています。

単純に UINavigationController を利用する場合は特に toolbardelegate を意識する必要はないのですが今回はこのあたりも軽く触れようと思います。(あんま使ったことないので軽めに)

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
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: UIPanGestureRecognizer

navigationBar

ちょっとややこしいのが navigationBar 。。。構成は下記のようになっています。

navigationBar

引用:UINavigationBarドキュメント

leftBarButtonItem (backBarButtonItem)、rightBarButtonItemtitle (titleView)、prompt の4つで構成されています。

この4つは ViewController ごとに設定でき ViewControllernavigationItem からアクセスできます。UINavigationControllernavigationBardelegatepopItempushItem を自動で設定してくれるので UINavigationController を利用する場合はそれぞれの ViewControllernavigationItem を設定しておけば popItempushItem を気にする必要はありません。(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 の設定は UINavigationControllernavigationBar に設定するので VC を push しようが pop しようが変わらないということ!(すべてのナビゲーションバーで設定を変えたい場合は AppDelegate などで UINavigationBar.appearance() に設定してやるといけます。MFMailComposeViewControllerUIActivityViewController の表示がおかしくなる場合があるので appearance の扱いには注意が必要です!)

barStyle は default, black の2パターンあり、isTranslucent と組み合わせると下記のようになります。

default black default(透過なし) black (透過なし)
bar_default bar_black bar_default2 bar_black2
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]

上記のように設定するとこんな感じになります

custom

prompt の色を設定するプロパティはなさそう。。。

navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.systemPurple]
navigationController?.navigationBar.prefersLargeTitles = true

上記のようにして largeTitle 表示に変更するとこんな感じになります

largeTitle

largeTitle の表示は navigationItem の下記プロパティで制御できます

// automatic/always/neverの3パターン
var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode

UIBarbuttonItem と 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
customView customView_large

背景画像

shadowImage に関しては下記のようにしてみるとわかりやすいかも

// 背景画像非表示
navigationController?.navigationBar.setBackgroundImage(.init(), for: .default)
// 影画像非表示
navigationController?.navigationBar.shadowImage = .init()
デフォルト 背景画像非表示 背景画像と影画像非表示
default none_image none_shadowimage

背景画像のいい感じの大きさはわからないですが、スライスとかストレッチでいい感じにこっちでしないといけないのかも。。。

backBarButtonItem

UINavigationController で扱いが難しいのが戻るボタン。。。標準だと1つ前の VC のタイトルが表示され、長い場合は「戻る」表示になる便利なやつだけどカスタムしようと思うと色々難しかったりします。

デフォルト タイトルが長い場合
back_1 back_2

戻るボタンをカスタムする場合は戻るボタンを表示する 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.画像を表示
back_title_none back_title back_image

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
backindicator_default backindicator_landscape backindicator_large

UINavigationBarAppearance

今までは直接 navigationController?.navigationBar.barTintColor = .systemOrange のようにプロパティ設定をしていましたが、iOS 13 からは UINavigationBarAppearance の設定でも変更できるようになっています。
ナビゲーションバーには下記の3つの表示状態があり、それぞれに対応する UINavigationBarAppearance のプロパティ (standardAppearance, compactAppearance, scrollEdgeAppearance) が UINavigationBar には用意されているので別々に外観を設定することができます。

standardAppearance compactAppearance scrollEdgeAppearance
standard compact scrollEdge

ちなみに下記のように設定するとスクロール 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

こんな感じになります

appearance

UINavigationBarAppearance には下記のようにボタンの Appearance のプロパティがあるのでそれぞれのボタンの見た目も設定できそうです。

var buttonAppearance: UIBarButtonItemAppearance
var doneButtonAppearance: UIBarButtonItemAppearance
var backButtonAppearance: UIBarButtonItemAppearance

toolbar

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]

上記のように設定するとこんな感じになります

toolbar

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 は現役だと思うのでこの記事がどこかで役に立つことを願います:sunglasses:

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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が代入され、実行できる。

nil

スクリーンショット 2020-08-11 16.34.47.png

 Optionalの構造を、見てみた。?

因みに: xxx?は、: Optional<xxx>の簡易verらしい。

var optionalString : String? // 簡単な方法ver。「?」 付けるだけ。
var optional : Optional<String> // これでもOK

実行すると、こんな感じ。(もちろん、変数名は自由)
「?」のヤツと、同じ扱い。

var optionalString : Optional<String> // Optionをクリック

print(optionalString) // ---> nil

Optionalを、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!) // 2

 2. 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 値が指定されていなくても、大丈夫。

 終わりに

暗黙的なオプショナル型とかは、省略。
気になる方は、おググリなさって下さい。

 参考サイト

どこよりも分かりやすいSwiftの"?"と"!"

【Swift入門】オプショナル(Optional)型の基本を徹底解説!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EfficientnetをCore MLに変換する【変換済みモデルあり】

スクリーンショット 2020-08-11 12.40.11.png
画像分類の最新モデルをCore ML形式に変換します。

変換済みCoreMLModel(GitHub)

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')

スクリーンショット 2020-08-11 12.40.58.png

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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のバージョンによるエラーだと勘違いしてしまったので注意が必要です。

宣伝

個人ブログを新しくリニューアルしました。

https://tamappe.com/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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. プロジェクト設定の変更

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_END
TorchModule.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;
}

@end

5. 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については以下の記事を参照:


  1. coremltools 4.0から直接Core MLモデルに変換できるようになったが、まだベータなのと、ちょっと使ってみた感じでは生成されるモデルがiOS 14以上でしか使用できない 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AnimeGANv2をCore MLに変換してiOSでつかう【変換済みモデルあり】

変換済みモデル(Hayao,Paprika)GitHubリンクanime.gif

8.6MBの軽量モデル。

グラフをpbtxt形式で保存します。

test.py
tf.train.write_graph(sess.graph_def, './', 'animegan.pbtxt') 

アウトプットノードの名前を調べます。

test.py
graph = 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を使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む