20200807のiOSに関する記事は7件です。

CALayerアニメーションで開始位置を変更した際に迷ったことを整理した

CoreAnimationを使って、吹き出しのアニメーションを作った際に

  • アニメーションがViewの中心から始まってしまう
  • Viewの表示位置がズレてしまう

という点について迷ってしまいましたので、自分なりに整理してみることにしました。

内容を整理するにあたって、日常生活の会話を題材に、以下の4つの点から開始するアニメーションを作成しました。

  • 閃きのアニメーション (画像の下中央からのアニメーション)
  • 吹き出しのアニメーション (画像の左右中央からのアニメーション)
  • 汗マークのアニメーション (画像の上中央からのアニメーション)

アニメーション自体は、Viewのスケールを変えているだけになります。

動かした感じ

アニメーションの開始位置を変更する

アニメーションの開始位置の指定は、CALayerのanchorPoint1を指定することで変更が可能です。

閃きのアニメーション

エナジードリンクを飲む前に、添える一言を思いついた瞬間を表すアニメーションです。

anchorPointで指定する際は、次のような座標をイメージすると対応を進め易いです。
電球.png

電球のアニメーションでは、の位置から始めたいので、座標はxが0.5、yが1.0になります。

デフォルトでは、この値が(0.5,0.5)になっているので、アニメーションがViewの中央から始まります。

電球のviewをlightBulbViewとすると

lightBulbView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0)

と指定することで、の位置からアニメーションが開始されます。

吹き出しのアニメーション

エナジードリンクを飲む直前に添えた一言の吹き出しのアニメーションです。

吹き出し.png

吹き出しのアニメーションは、の位置から開始したいので、xが1.0、yを0.5に指定します。

speechBubbleView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)

汗マークのアニメーション

聞き返された後のアニメーションです。

汗マーク.png

吹き出しのアニメーションは、の位置から開始したいので、xが0.5、yを0.0に指定します。

polkaDotView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)

これで、Viewに追加した画像別に、アニメーションの開始位置を変えることができました。

Viewの表示位置を元に戻す

しかしこのままですと、配置したViewの表示される位置が変わってしまいました。
そこで、 anchorPointで移動させた分を、元に戻して意図した見た目の位置に戻しました。

閃きのアニメーションで試した例
let yMovement = lightBulbView.frame.size.height * lightBulbView.layer.anchorPoint.y
lightBulbView.transform.ty = yMovement
吹き出しのアニメーションで試した例
let xMovement = speechBubbleView.frame.size.width * speechBubbleView.layer.anchorPoint.x
speechBubbleView.transform.tx = xMovement
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftFormat(0.45.0以降)で想定外の整形結果になったので解決方法をまとめました

はじめに

私のチームではソースコードを綺麗な状態で保つために、OSSのSwiftFormatを利用してソースコードの自動整形(フォーマット)を行なっています。
今回、SwiftFormat0.45.0以降にアップデートしたところ、今までは自動整形されていなかった箇所まで変更されてしまう事象が2つ発生したため、解決方法についてまとめました :thumbsup:

実行環境

環境 バージョン
macOS Catalina 10.15.6
Xcode 11.6.0
SwiftFormat 0.45.0以降

発生した事象その1

次のように、修飾子の順番が変更されてしまう事象が発生しました。

- convenience required init?(hoge: Hoge) {
+ required convenience init?(hoge: Hoge) {
- static public func hoge() {
+ public static func hoge() {

原因

CHANGELOG.mdを確認したところ、バージョン0.45.0にて以下のオプション、ルール、構成オプションの名前が変更されていました。1

CHANGELOG.md
Renamed --empty option to --voidtype, which better describes its function
Renamed specifiers rule to modifierOrder for consistency with SwiftLint
Renamed --specifierorder configuration option to --modifierorder

このことが原因で、.swiftformatに設定していたspecifiersの無効指定が0.45.0以降では意味を成しておらず、現象その1が発生していました。

# disable
--disable specifiers

対応方法

CHANGELOG.mdの内容に従って、名前を変更することで無事解決できました :thumbsup:

  # disable
- --disable specifiers
+ --disable modifierOrder

発生した事象その2

次のように、guard-else文で改行を含む複数条件の場合、else { が改行されてしまう事象が発生しました。

  guard let foo = foo,
-     let bar = bar else {
+     let bar = bar
+ else {
      fatalError()
  }

原因

CHANGELOG.mdを確認したところ、バージョン0.45.0にて以下のオプションが追加されていました。

CHANGELOG.md
Added --guardelse option to control wrapping of else clauses in guard statements

--guardelseオプションの説明を確認すると、defaultはautoとなっており、このことが原因で現象その2が発生していました。

elseOnSameLine
Place else, catch or while keyword in accordance with current style (same or next line).

Option Description
--elseposition Placement of else/catch: "same-line" (default) or "next-line"
--guardelse Guard else: "same-line", "next-line" or "auto" (default)

対応方法

--guardelseオプションに指定できる値は、same-line or next-line or autoとなっているため、今までのように何もしないという動作を選択することはできないようです。2
このため、今回は--guardelseオプションに現状のソースコードにより近い値を指定することで、(ほぼ)解決ということにしました。
(もっといい解決方法を知っている方いましたら、ぜひ教えてください :raised_hand:

補足

類似問題として、--guardelsenext-line or auto(default) にした場合に else {のインデントがずれる事象も観測されましたが、こちらの事象は障害のようでした。
すでに以下のIssueとして管理され、fixed in develop ラベルが付与されている状態のため、次のリリース(たぶん、0.45.4だと思われる)で修正される予定のようです。

最後に

今回取り上げた内容以外にも新しい機能が複数追加されているため、上記とは異なる問題が起きている方もいるかもしれません。
そのような場合は、まずCHANGELOG.mdの内容確認をオススメします。

参考情報


  1. ちゃんとCHANGELOG.mdを読んでいなかった自分も悪いと思いつつ、セマンティック バージョニングの考え方からするとマイナーバージョンのアップで互換性が失われるのはどうなんだろう?:thinking: という気もしています。  

  2. --disableで無効化できないか試してみたのですが、上手くいきませんでした。ソースコードを自動整形(フォーマット)して綺麗に保つという趣旨からすると、何もしないという動作は許容したくないのかも?けど、無効化できるオプションもあるしなー:thinking:  

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

ViewController.swiftのデフォルトコード【overrideとか、superとか】

 overrideとか、superとか。

簡単にまとめます。??

 前提

-親クラス(別名: スーパークラス)
-子クラス(別名: サブクラス)

(英 super: 「超〜」以外に、リーダー、監督、管理人、なんて意味もある。)

 override?

overrideは、親クラスのメソッドに何か書き換えて使いたい時に使えます。

継承して出来た子クラス側で親クラスのメソッドを再定義するのがoverrideです。
なので、「継承 =override」ではないです。?

(英 override: 上書き)

 super?

overrideされる前の親のメソッドを、明示的に呼びたい時に使います。

overrideを行った場合にはメソッド名が同じになるので、
そのままでは親クラスのメソッドを呼び出すことができません。

super = "親クラスそのもの"を指す
(super.メソッド名、使い方はこんな感じ。)

 class ViewController: UIViewController {   <----: で継承。

     override func viewDidLoad() { 
         super.viewDidLoad() 
         // 新しく追加したい処理を書く 
     } 
 } 

「親クラス(super)のメソッドですよ」と、
わかりやすく(=明示的) に記述できる。

override func メソッド名super.メソッド名

上記コードでのメソッド名はどちらもviewDidLoad()区別がつかないですが、
superを使うことで、明示的になります。

 overrideできないようにしたい場合?

finalを使う。

-メソッドに使うと、overrideでエラー。(下記)
-親クラス名に使うと、継承自体できなくなる。

 class Paint { 
     final func changeColor() {  <----final
         print("色を変更します") 
     } 
 } 


 class PaintChild: Paint { 
     override func changeColor() {   <----overrideでエラー
         print("色を緑に変更します") 
     } 
 } 

 ViewController.swiftの、デフォルトコード

extendsで継承する言語もあると思いますが、Swiftは「:」 で継承。

 class ViewController: UIViewController {   <----: で継承。

     override func viewDidLoad() { 
         super.viewDidLoad() 
         // 新しく追加したい処理を書く 
     } 
 } 

親クラスviewDidLoadメソッドを呼び出し、そこに追加処理を書く。
という感じ。

おしまい。

 参考サイト

[Swift入門] overrideの意味と使い方
メソッドのオーバーライド

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

iOS14からエンジニアがすべきIDFA対応

はじめに

9月リリースが噂されているiOS14から、
iOS13まで端末単位で設定していたIDFAの取得許可(追跡型広告制限)
iOS13.jpg

が端末兼アプリ単位の取得許可に変更され、
各アプリにて、通知や位置情報許可のように、ユーザに許可を取る必要がでてきます。

IDFAとは?iOS13と比較した詳しい挙動の違いは?は
こちらの素晴らしい記事にまとまっていたので割愛させていただきます。
https://qiita.com/yofuru/items/213b88b85553631204e4

重要なのはiOS14対応していないアプリはIDFAを取得できなくなることです。

本記事では実際にエンジニアがiOS14リリースまでに対応する必要がある実作業をまとめます。

※本記事はbeta4時点の情報です、beta版のスクリーンショットはNDA締結により掲載しておりません

1.frameworkの追加

IDFAの使用用途は、主に広告やトラッキングのためと考えられるため既に導入済みであることが多いかと思いますが、
AdSupport.framework
そして今回追加された
AppTrackingTransparency.framework
を追加します
公式リファレンス: https://developer.apple.com/documentation/apptrackingtransparency

2.info.plistへ説明の追加

Privacy - Tracking Usage Description
をKeyとしてValueにユーザにAlertを出した時に表示される文言を追加します。

ここでの文言は非常に重要になってくると考えられます。
ATTrackingManager.requestTrackingAuthorizationを利用してAlertを表示できるのは1度きりです
通知や位置や写真の時と一緒ですね

AlertのタイトルはApple側が表示するもので、日本語だとbeta4時点でこう表示されてました
「"app"が他社が所有するAppやWebサイトを横断してあなたを追跡する許可を求めます」

???

一般ユーザはなんぞや???
って感じだと思う
誰も許可したくならない…

info.plistの記述はAlertのメッセージ部分に表示されます。
こちらを許可していただければ、関連性の高い広告を配信することができるなど
許可するメリットを伝えることが重要かと思います。

また、info.plistの文言はAppleの審査の目が厳しい箇所でもあるので
強制させるような文言や、用途が何も伝わらない文言はやめましょう。

3.Alertを表示するコードの追加

必要なタイミングで許可情報を取得し、Alertを表示するコードを追加しましょう

ViewController.swift
import UIKit
import AdSupport
import AppTrackingTransparency

class ViewController: UIViewController {

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        switch ATTrackingManager.trackingAuthorizationStatus {
        case .authorized:
            print("Allow Tracking")
            //IDFA取得
            print("IDFA: \(ASIdentifierManager.shared().advertisingIdentifier)")
        case .denied:
            print("?お断り")
        case .restricted:
            print("?制限")
        case .notDetermined:
        //Alert表示
            ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
                switch status {
                case .authorized:
                    print("?")
                case .denied, .restricted, .notDetermined:
                    print("?")
                @unknown default:
                    fatalError()
                }
            })
        @unknown default:
            fatalError()
        }
    }
}

先述のように、ATTrackingManager.requestTrackingAuthorizationを利用してAlertを表示できるのは1度きりです。そのため、表示するタイミングが非常に重要になってきます。

例えばリワード広告を出した後に、「より貴方が好む広告を表示するために情報を利用します」など…
自分も検討中です?

なお、これまでIDFAを取得できるか利用していた
ASIdentifierManager の isAdvertisingTrackingEnabled
はiOS14からDeprecatedになります
https://developer.apple.com/documentation/adsupport/asidentifiermanager

注意点

冒頭で説明した通り、iOS14からは、端末兼アプリ単位の追跡許可設定になります。
端末単位で拒否されていると、requestTrackingAuthorizationしていなくても、trackingAuthorizationStatusがdeniedで返ってきます、Alertを表示することさえ許されません、無慈悲?

端末の設定は
設定 > プライバシー > トラッキング > Appからのトラッキングを許可(ON/OFF)
から設定できます
各アプリの設定もここからできます

また、iOS13までIDFAの設定画面にあった項目は
設定 > プライバシー > Appleの広告 > パーソナライズされた広告(ON/OFF)
に変更されています

試したところ、パーソナライズされた広告がOFFの時でもtrackingAuthorizationStatusが.authorizedで返ってきてIDFAが取れていました
説明を読むとこちらは、App StoreやApple News、株価のターゲティングが無効になるそうです

おわりに

この他に開発者が意識するべきことは、利用しているSDKがIDFAを利用しているかを把握し
各社に対応を伺うことかと思います。

参考にAjust様がものすごく丁重に対応すべき項目をまとめてくださっていたので共有します。
Ajust: iOS14の変更に向けての準備と対策

また、いくつかの広告会社さんからSDKのアップデート予定を告知していただきました?‍♂️

ユーザさんにメリットを伝えつつサービスの目的も達成しうる対応をしていきましょう?

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

iOS14でエンジニアがすべきIDFA対応

はじめに

9月リリースが噂されているiOS14から、
iOS13まで端末単位で設定していたIDFAの取得許可(追跡型広告制限)
iOS13.jpg

が端末兼アプリ単位の取得許可に変更され、
各アプリにて、通知や位置情報許可のように、ユーザに許可を取る必要がでてきます。

IDFAとは?iOS13と比較した詳しい挙動の違いは?は
こちらの素晴らしい記事にまとまっていたので割愛させていただきます。
https://qiita.com/yofuru/items/213b88b85553631204e4

重要なのはiOS14対応していないアプリはIDFAを取得できなくなることです

本記事では実際にエンジニアがiOS14リリースまでに対応する必要がある実作業をまとめます。

※本記事はbeta4時点の情報です、beta版のスクリーンショットはNDA締結により掲載しておりません

1.frameworkの追加

IDFAの使用用途は、主に広告やトラッキングのためと考えられるため既に導入済みであることが多いかと思いますが、
AdSupport.framework
を追加します。
そしてiOS14から追加された、
AppTrackingTransparency.framework
を追加します。

公式リファレンス: https://developer.apple.com/documentation/apptrackingtransparency

2.info.plistへ説明の追加

Privacy - Tracking Usage Description
をKeyとしてValueにユーザにAlertを出した時に表示される文言を追加します。

ここでの文言は非常に重要になってくると考えられます。
ATTrackingManager.requestTrackingAuthorizationを利用してAlertを表示できるのは1度きりです
通知や位置や写真の時と一緒ですね。

AlertのタイトルはApple側が表示するもので、日本語だとbeta4時点でこう表示されてました。
「"app"が他社が所有するAppやWebサイトを横断してあなたを追跡する許可を求めます」

???

一般ユーザはなんぞや???
って感じだと思う。
誰も許可したくならない…

info.plistの記述はAlertのメッセージ部分に表示されます。
こちらを許可していただければ、関連性の高い広告を配信することができるなど
許可するメリットを伝えることが重要かと思います。

また、info.plistの文言はAppleの審査の目が厳しい箇所でもあるので
強制させるような文言や、用途が何も伝わらない文言はやめましょう。

3.Alertを表示するコードの追加

必要なタイミングで許可情報を取得し、Alertを表示するコードを追加しましょう。

ViewController.swift
import UIKit
import AdSupport
import AppTrackingTransparency

class ViewController: UIViewController {

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        switch ATTrackingManager.trackingAuthorizationStatus {
        case .authorized:
            print("Allow Tracking")
            print("IDFA: \(ASIdentifierManager.shared().advertisingIdentifier)")
        case .denied:
            print("?拒否")
        case .restricted:
            print("?制限")
        case .notDetermined:
            showRequestTrackingAuthorizationAlert()
        @unknown default:
            fatalError()
        }
    }

    ///Alert表示
    private func showRequestTrackingAuthorizationAlert() {
        ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
            switch status {
            case .authorized:
                print("?")
                //IDFA取得
                print("IDFA: \(ASIdentifierManager.shared().advertisingIdentifier)")
            case .denied, .restricted, .notDetermined:
                print("?")
            @unknown default:
                fatalError()
            }
        })
    }
}

先述のように、requestTrackingAuthorizationを利用してAlertを表示できるのは1度きりです。そのため、表示するタイミングが非常に重要になってきます。

例えばリワード広告を出した後に、「より貴方が好む広告を表示するために情報を利用します」など…
自分も検討中です?

なお、これまでIDFAを取得できるか利用していた
ASIdentifierManager の isAdvertisingTrackingEnabled
はiOS14からDeprecatedになります。
https://developer.apple.com/documentation/adsupport/asidentifiermanager

注意点

冒頭で説明した通り、iOS14からは、端末兼アプリ単位の追跡許可設定になります。
端末単位で拒否されていると、requestTrackingAuthorizationしていなくても、trackingAuthorizationStatusがdeniedで返ってきます、Alertを表示することさえ許されません、無慈悲?

端末の設定は
設定 > プライバシー > トラッキング > Appからのトラッキングを許可(ON/OFF)
から設定できます。
各アプリの設定もここからできます。

また、iOS13までIDFAの設定画面にあった項目は
設定 > プライバシー > Appleの広告 > パーソナライズされた広告(ON/OFF)
に変更されています。

試したところ、パーソナライズされた広告がOFFの時でもtrackingAuthorizationStatusが.authorizedで返ってきてIDFAが取れていました
説明を読むとこちらは、App StoreやApple News、株価のターゲティングが無効になるそうです。

おわりに

この他に開発者が意識するべきことは、利用しているSDKがIDFAを利用しているかを把握し
各社に対応を伺うことです。

参考にAdjust様がものすごく丁重に対応すべき項目をまとめてくださっていたので共有します。
Adjust: iOS14の変更に向けての準備と対策

また、いくつかの広告会社さんからSDKのアップデート予定を告知していただきました?‍♂️

ユーザさんにメリットを伝えつつセキュリティを担保しつつ
サービスの目的も達成しうる対応をしていきましょう?

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

ARKitでDepthDataの深度情報を取得する方法

はじめに

この記事ではARKitを使ってフロントカメラからDepthDataを取得し、1pixelずつの深度情報を取得する方法を備忘録的にまとめます。
もしかしたらフロントカメラ以外にも使えるかもと思い、タイトルは少し広い内容をカバーできるようにしました。

いろんな事情でiPhone11で顔認識をした時に深度情報がfloatで欲しいと思い、いろいろ調べたんですが、一つにまとまった記事が存在しなかったので、深夜テンションで記事を作ろうと思いました。
(筆者はSwiftを勉強しはじめて数ヶ月のド素人なので、コードや文章の書き方が拙いかもしれませんがお許しください...)
(改善点や技術的補足がある人は大歓迎です)

対象となる人

  • ARKitで深度情報が欲しくなった人
  • (TrueDepth搭載のiPhoneのフロントカメラを使用する人)
  • ポインタという概念がちょこっとだけ分かる人

筆者の環境

  • Xcode11.6
  • Swift
  • iPhone11
  • iOS13.5

要約

  • CVPixelBufferをコネコネすればできる
  • UnsafeMutableRawPointerからUnsafeMutablePointerに変換する
  • UnsafeBufferPointerに変換してからArrayに変換する
  • 欲しいピクセルの深度を1次元配列から取得すれば深度情報が得られる

コード概要

とりあえずコードを初めに見せてから要点を解説していきます。
分からない人はsessionメソッドの部分に中身を丸コピしてもいいかもしれません。

ViewController.swift
func session(_ session: ARSession, didUpdate frame: ARFrame) {
    // nilチェック
    guard let depthData = frame.capturedDepthData else { return }

    let depthMap = depthData.depthDataMap

    // depthMapのCPU配置(?)
    CVPixelBufferLockBaseAddress(depthMap, .readOnly)

    let base = CVPixelBufferGetBaseAddress(depthMap) // 先頭ポインタの取得
    let width = CVPixelBufferGetWidth(depthMap) // 横幅の取得
    let height = CVPixelBufferGetHeight(depthMap) // 縦幅の取得

    // UnsafeMutableRawPointer -> UnsafeMutablePointer<Float32>
    let bindPtr = base?.bindMemory(to: Float32.self, capacity: width * height)

    // UnsafeMutablePointer -> UnsafeBufferPointer<Float32>
    let bufPtr = UnsafeBufferPointer(start: bindPtr, count: width * height)

    // UnsafeBufferPointer<Float32> -> Array<Float32>
    let depthArray = Array(bufPtr)

    // depthMapのCPU解放(?)
    CVPixelBufferUnlockBaseAddress(depthMap, .readOnly)

    let fixedArray = depthArray.map({ $0.isNaN ? 0 : $0 })

    print(fixedArray[width*200+400]) //(400,200)に対応する深度値

}

では解説に入っていきます。

CVPixelBufferについて

ARKitから深度情報を取得しようとした人はおそらく、ARFrameのメンバ変数であるcapturedDepthDataに目が付き、その中にあるdepthDataMapまではたどり着いたと思います。
しかしこのdepthDataMapのクラスはCVPixelBufferで、それからどうやって値を取得するか分かりにくいかと思います。

調べたところによると、CVPixelBufferはGPUのメモリ上にデータが置かれているため、そう簡単に値を取得することができないそうです。
いろいろな処理をするためにはCPUが管理できるメモリに配置される必要があります。

CVPixelBufferはCIImageに変換でき、そこからUIImage等に変換できるため、DepthMapの描画や、グレースケール画像としての画素値を取得することができたとは思います。

しかし、depthDataMap(AVDepthData)のリファレンスによると

A depth map describes at each pixel the distance to an object, in meters.
(デプスマップは、各ピクセルでのオブジェクトまでの距離をメートル単位で表します。)

とあるので、何かしらの方法を使えばメートル単位での深度情報が取得できることがわかります。距離が取れるのであれば、グレースケール画像の画素値では情報が削られてしまうのであまり好ましい方法ではありません。

そこで、CVPixelBufferをCPUで扱う関数が登場します。

CVPixelBufferLockBaseAddress

CVPixelBufferLockBaseAddress関数は、CVPixelBufferの値をGPUからCPUが扱えるメモリへ移行してくれるのです。これにより、GPUによりプログラムで扱えなかったデータが扱えるようになります。
(この感覚だと思っていますが確証はありません。そもそもGPU管理だと扱えないのかどうかすら怪しいので詳しい人がいたら教えて欲しいです)

そして、一連の作業が終了したあとはCVPixelBufferUnlockBaseAddressでGPUに値を返してあげます。
(この行為に意味があるかは分かってないです)

CPUで扱えるようになることで、CVPixelBufferGetBaseAddressを使えば先頭ポインタを取得することができます。

...ポインタ?

Swiftのポインタについて

Swiftでもポインタは避けて通れません。むしろObjective-cの名残も含め、C言語と親和性の高い作りになっていることが実感できました...

本題に戻ります。

UnsafeMutableRawPointer

CVPixelBufferGetAddressから取得できるのは、UnsafeMutableRawPointerという型のポインタです。
この型は、「変更ができない型なしのポインタ」という意味です。constでvoid的な感じです。変更できないのは参照する値が変更できないということだと認識しています。

このUnsafeMutableRawPointerから深度情報を取得するためには、まずデータ型の情報を与えてあげなければなりません。それがUnsafeMutablePointerになります。

UnsafeMutablePointer<T>

UnsafeMutableRawPointerと見間違えそうですが、Rawがあるかどうかがポイントです。
UnsafeMutablePointerは型があるポインタです。型を指定するので、深度情報がより取得できそうです。

深度情報はFloat32で保存されています。のでFloat32の型になるよう指定します。
(このことはCVPixelBufferGetPixelFormatTypeを使って調べることができます)

UnsafeMutableRawPointerからUnsafeMutablePointerに変換するにはbindMemoryを使いました。(すいません仕様はよく分かってません)
引数capacityは要素数だと思い、縦幅x横幅を指定しました。

実はこの状態からも値は取得できるのですが、配列にしておいた方が何かと都合が良さそうだと思い、Arrayを目指します。

UnsafeBufferPointer<T>

UnsafeBufferPointerはいうなれば配列ポインタです。これを経由することで配列に変換することができます。

UnsafeBufferPointerへの変換はinitする形でOKです。UnsafeBufferPointerはcountをメンバ変数として持っているので、initでまた要素数を指定してあげる必要があります。(無駄を感じるのでもう少し簡単にできそうですが...)

最後に、Array()を使うことで、最終的にCVPixelBufferからArrayに変換することができます。お疲れ様です。

DepthDataMapの値を読み取る

無事配列にすることができたのですが、1次元配列なので、欲しいピクセルの深度値を取得するためには多少の計算をする必要があります。

index = width * y + x

xとyは欲しい座標で、widthは画像の横幅、indexが配列で指定すべきインデックスとなります。
配列は左上から横に値を取得していったような形式になると思います。

depthDataMapにおける無効値はNaNで表現されます。上記のプログラムでは無効値を0に置き換え、すべて有効な数字として扱えるようにしています。必須ではないので書かなくても大丈夫です。

そして!重要なことが1つ!
これはフロントカメラに言えることなのですが、取得した深度画像は、本来の向きから90度左を向いた状態で取得することになります。なので、カメラの解像度が480x640の縦長の画像だとしたら、Arrayで取得できるのは、640x480の横長の画像として取得することになります。
画像の中心の深度値を取得したい場合は、(240,320)ではなく、(320,240)を取得する必要があることにご注意ください。
(分かる人はそもそもの向きを修正することができると思いますが...)

まとめ

  • DepthDataから深度値は取れる!
  • CVPixelBufferめんどい
  • ポインタしんどい
  • フロントカメラの向きがヤバイ

最後に

今回は深度情報を1次元配列にしましたが、おそらく2次元配列にする方法はあると思います。
すいませんそこまでやる方法が思いつかなかったので、何かやり方があれば教えてください?‍♂️

ちなみに、iOS14からAVDepthDataの代わりにARDepthDataというものが増えるそうです。
でもARDepthDataのメンバ変数であるdepthDataMapはCVPixelBufferクラスなので、結局この記事が参考になるかもしれません。

ここまで読んでいただきありがとうございました。

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

iOS シミュレータで動画を収録して gif に自動で変換するスクリプトを書いた

  • 知人に作ったアプリを見せたり
  • SNS に投稿したり
  • Pull Request のメッセージで動作を説明したり

するときに iOS シミュレータの動画を収録 → gif に変換ということを頻繁に行いませんか?
僕もそうで、特に Github の Pull Request で実装した様子を手軽に見せる手段として gif 画像を使います。

でも、毎回 収録 → gif に変換の手順を踏むのが面倒になったので適当にシェルスクリプトを書いてみました。

要求

  • iOS シミュレータの動画を収録して gif として output するのを 1 コマンドで解決する
  • ファイル名はなんでもいい
  • だけどできれば収録した順に並んでると嬉しい

設計

iOS シミュレータの動画を収録して gif として output するのを 1 コマンドで解決する

これに対してはシェルスクリプトを作るということで解決します。

ファイル名はなんでもいい

ファイル名に制限は設けません。

だけどできれば収録した順に並んでると嬉しい

最初はランダムな文字列.gif とかで考えましたが、この要求を満たすために、
日付-時間.gif
というファイル名の形式で output させることにしました。

作ってみる

ファイル名 (日付-時間.gif) から作る

こんなの慣れた人からしたら朝飯前なんでしょうけど、僕はあんまりなれてないので、ここから解説します。

この Qiita 記事 を参考にすると、 date コマンドによって日付および時刻が取得できそうです。
試しに

$ date
# → "Fri Aug  7 00:52:48 JST 2020" と出力された

いい感じです。あとはこれを整形します。
今回は 日付_時間.gif とするので

$ date +%Y%m%d_%H%M%S
# → "20200807_005517" と出力された

のように +%Y%m%d_%H%M%S を付けてあげると秒までの時間が出力されます。
これでファイル名で並べたときに収録した順番に並んでくれそうです。

今回はこれをファイル名として使いたいので、使い回せるように変数に格納しときましょう。

gif-generator.sh
DATETIME=`date +%Y%m%d_%H%M%S`
echo $DATETIME

なんとなく echo しておきました。

iOS シミュレータの動画を取るには?

こちらの Qiita 記事 によると、

  1. simulatorは起動しておく
  2. コマンド xcrun simctl io booted recordVideo test.mov 実行(ディレクトリどこでも)
  3. simulatorを操作
  4. ctrl + cで終了

でできそうです。
なので、先程 DATETIME 変数を作ったのでそれをファイル名としてシェルスクリプトに以下のように追加すればできそうですね。

DATETIME=`date +%Y%m%d_%H%M%S`
echo $DATETIME
+ xcrun simctl io booted recordVideo $DATETIME.mov

これを実行して ctrl + C で interrupt すれば 20200807_005517.mov のような動画が取れます。

あとはこれを gif にするだけだ!

動画を gif に変換

変換するにはコマンドラインから使えるツールとして有名な FFmpeg を使いました。
インストールは Homebrew でやるのが一番ラクだと思います。

brew install ffmpeg

コマンドの使い方はググってもらうとして、僕は Pull Reqest で動作を見せるので割と画質は高いほうがいいです。
調べてみると ffmpegでとにかく綺麗なGIFを作りたい という僕にぴったりな記事が見つかりました。

いくつか手法が紹介されていますが、その中で

手軽に綺麗なGIFを作りたい
グローバルパレットを使用して最適化

を使うことにしました。
コマンドは以下のとおりです。

ffmpeg -i input.mov -filter_complex "[0:v] fps=10,scale=640:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" output-palette.gif

input.mov に入力動画、output-palette.gif が出力する gif のファイル名ですね。
ではこれを、さっきのシェルスクリプトに追加してっと。

gif-generator.sh
DATETIME=`date +%Y%m%d_%H%M%S`
echo $DATETIME
xcrun simctl io booted recordVideo $DATETIME.mov
+ ffmpeg -i $DATETIME.mov -filter_complex "[0:v] fps=10,scale=640:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" $DATETIME.gif

はい、完成。
これで適当なディレクトリで sh gif-generator.sh 入力 → シミュレータ操作 → ctrl + C 押下で gif が作られます。
簡単ですね。

ちょっと改造

これで目的は達成されたわけですけど、もうちょっとだけ今後のことを考えて手を入れてみます。

xcrun simctl io booted recordVideo $DATETIME.mov の性質上、どうしても ctrl + C を押しますよね。
なので、 ctrl + C で呼ばれるかたまりに分けると今後拡張するときも楽そうです。

細かいことは検索してもらって、僕は最終的に以下のように変更しました。

+ #!/bin/sh
DATETIME=`date +%Y%m%d_%H%M%S`
echo $DATETIME
+
+ trap "final; exit 1" 2
+ 
+ function final {
+   echo "Ctrl+C pushed."
+
+   # mov -> gif by using ffmpeg
+   # ref. https://qiita.com/yusuga/items/ba7b5c2cac3f2928f040 
+   # ffmpeg -i $DATETIME.mov -r 10 -vf scale=640:-1 -f gif $DATETIME.gif
+   ffmpeg -i $DATETIME.mov -filter_complex "[0:v] fps=10,scale=640:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" $DATETIME.gif
+ }
+ 
xcrun simctl io booted recordVideo $DATETIME.mov
- ffmpeg -i $DATETIME.mov -filter_complex "[0:v] fps=10,scale=640:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" $DATETIME.gif
+

diff だと読みにくいかも…

gif-generator.sh
#!/bin/sh
DATETIME=`date +%Y%m%d_%H%M%S`
echo $DATETIME

trap "final; exit 1" 2

function final {
  echo "Ctrl+C pushed."

  # mov -> gif by using ffmpeg
  # ref. https://qiita.com/yusuga/items/ba7b5c2cac3f2928f040 
  # ffmpeg -i $DATETIME.mov -r 10 -vf scale=640:-1 -f gif $DATETIME.gif
  ffmpeg -i $DATETIME.mov -filter_complex "[0:v] fps=10,scale=640:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" $DATETIME.gif
}

xcrun simctl io booted recordVideo $DATETIME.mov

final 関数が ctrl + C を押されたときに呼ばれる関数です。
これでちょっと見通しよくなったかな?

おわり: 改造案をコメントで募集!

あとは煮るなり焼くなりどうぞ。
オプション受け取れるようにして fps, 解像度を指定できるようにしたり、色々改善できそうです。

僕はシェルスクリプトに全然慣れてないので、「こうしたらいいよ!」「これ無駄かも!」とかあったらコメントで教えてほしいです??‍♂️

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