20200811のSwiftに関する記事は6件です。

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で続きを読む

今更だけど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で続きを読む

[macOS] AppleEventサポート

macOSにおけるAppleEventサポート方法

アプリケーションから、他のアプリケーションを制御するために、Scripting Supportが準備されているのですが、ここ数年資料の更新が行われていません。本稿では最近のmacOS(10.15)でのサポート状況、方法についてまとめていきます。

なお、本稿で説明するのは、AppleEventの送出側の実装です。受信側については、別記事
[macOS] Cocoa Scripting Supportをご覧下さい。

資料作成日: 2020.08.11

実行環境

下記を想定しています:

  • OS: macOS 10.15
  • Xcode: 11.6
  • 言語: Swift

AppleEvent

自アプリから他のアプリへのイベントの送信/受診に際して、イベントの実行はNSAppleEventDescriptorクラスで実装します。

Sandbox対応

Sandbox下のアプケーションにて、AppleEventの送受信をサポートするためには、次の設定が必要です。下記の例では、TextEdit.appを制御する場合について記述しています。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        .....
        <key>com.apple.security.temporary-exception.apple-events</key>
        <array>
                <string>com.apple.TextEdit</string>
        </array>
        <key>com.apple.security.scripting-targets</key>
        <dict>
                <key>com.apple.TextEdit</key>
                <array>
                        <string>com.apple.TextEdit</string>
                </array>
        </dict>
</dict>
</plist>

特に後者の項目(com.apple.security.scripting-targets)については、正式なドキュメントは見つからず、次の記事を参考にしています: Hardened Runtime and Sandboxing

アプリケーションソフトウェアのbundle identifierの取得

macOS付属のlsappinfoにて取得可能。
対象となるアプリが起動した状態で実行する必要がある。また、「テキストエディット」については、TextEditでは指定できない。

% lsappinfo info -only bundleid テキストエディット
"CFBundleIdentifier"="com.apple.TextEdit"

参考文献

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む