20191020のSwiftに関する記事は17件です。

macOSアプリのダークモード対応

はじめに

macOSでは10.14(2018年リリース)からダークモードが追加されています。

macOS 10.14以降、アプリで独自定義のカラーを使う場合、ライトモード用とダークモード用のカラーを用意しておく必要があります。ライト/ダーク用のカラーはカラーアセットを利用して用意できます。

スクリーンショット 2019-10-20 19.50.47.png

カラーアセットから以下のコードで外観モードにあわせたカラーオブジェクトを生成できます。

let color = NSColor(named: NSColor.Name("MyColor"))

しかし、この方法ではモードを切り替え時、起動中のアプリでは自動的にカラーが変更されません。macOS 10.15ではNSColorにinit(name:dynamicProvider:)イニシャライザが追加されており、ドキュメントに説明は無いですが、察するにこれを使えば動的に変更できるものと思います。では、macOS 10.14ではどうするか?

macOS 10.14でダークモードに対応する

macOS 10.14では、独自定義のカラーをViewに設定していた場合、モードの変更を検知し、カラーを設定し直すコードを書く必要があります。

ダークモードか判定する

ダークモードか判定するisDarkModeコンピューテッドプロパティをNSApplicationに生やします。

extension NSApplication {
    public var isDarkMode: Bool {
        if #available(OSX 10.14, *) {
            let name = effectiveAppearance.name
            return name == .darkAqua
        }
        else {
            return false
        }
    }
}

外観モードにあわせてカラーオブジェクトを返す

ライト/ダークモード用のカラーオブジェクトを返す適当なメソッドを実装します。

struct CustomColors {
    static var background: NSColor {
        if NSApplication.shared.isDarkMode {
            return NSColor(red: 34.0 / 255.0, green: 34.0 / 255.0, blue: 34.0 / 255.0, alpha: 1.0)
        }
        return .white    // for Light Mode
    }
}

外観モードの変更を検知する

外観モードが変更されると、NSViewのviewDidChangeEffectiveAppearanceメソッドが呼ばれます。このメソッドをオーバライドし、カラーオブジェクトを再設定するようにします。

class CustomView: NSView {

    @available(OSX 10.14, *)
    override func viewDidChangeEffectiveAppearance() {
        layer?.backgroundColor = CustomColors.background.cgColor
    }
}

これで動的にカラーを変更することができます。ただ、このやり方は各ビューで継承クラスを作らないといけないため、使い勝手が悪いときがあります。

そこで僕は以下のようなクラスを作って監視するようにしています。

class AppearanceMonitor: NSView {

    public static let appearanceChangedNotification = NSNotification.Name("AppearanceChangedNotification")
    public static let shared                        = AppearanceMonitor(frame: .zero)

    public var isMonitoring: Bool {
        superview != nil
    }

    override func viewDidMoveToSuperview() {
        guard let _ = superview else {
            print("[\(self.className)] Monitoring stopped.")
            return
        }
        print("[\(self.className)] Monitoring started.")
    }

    @available(OSX 10.14, *)
    override func viewDidChangeEffectiveAppearance() {
        NotificationCenter.default.post(name: AppearanceMonitor.appearanceChangedNotification, object: nil, userInfo: nil)
    }

    public func start(with viewController: NSViewController) {
        if !isMonitoring {
            viewController.view.addSubview(self)
        }
    }

    public func stop() {
        if isMonitoring {
            removeFromSuperview()
        }
    }
}
class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // 監視用のビューを貼る(frame=.zeroなので見た目には影響がない)
        AppearanceMonitor.shared.start(with: self)
    }

    override func viewWillAppear() {
        super.viewWillAppear()

        NotificationCenter.default.addObserver(self,
                                               selector: #selector(appearanceChanged),
                                               name: AppearanceMonitor.appearanceChangedNotification,
                                               object: nil)
    }

    override func viewWillDisappear() {
        super.viewWillDisappear()

        NotificationCenter.default.removeObserver(self,
                                                  name: AppearanceMonitor.appearanceChangedNotification,
                                                  object: nil)
    }

    @objc func appearanceChanged() {
        // ライト/ダーク・モード切り替え時に呼ばれるのでUIを更新する
    }
}

他にAppleInterfaceThemeChangedNotificationを監視する方法もありますが、公式ドキュメントに記載がない(?)ため、積極的に使っていいものか悩ましい…。

参考

Supporting Dark Mode in Your Interface
https://developer.apple.com/documentation/xcode/supporting_dark_mode_in_your_interface

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

【Swift】構造体とクラスの使い分け

構造体とクラス

Swiftで用いられるクラスと構造体は関数や変数など、プログラムを組む上で非常に便利な機能です。
どちらも表現力が豊かで、クラスで実現可能な事の大半は構造体でも再現可能だったりします。
それではどのような状況の時にクラスを用い、どんな時に構造体を使う選択をしていけば良いのでしょうか???

今回はそのクラスと構造体の使い方についてまとめていけたらと思います。

構造体の利点

まず以下のclassで実装したコードをみてください。
注目する点はPersonクラスをオブジェクト化したTomとMargaretのfavaritefoodのfood変数を呼び出しているところです。
本来であればTomがCake、MargaretがOrangeと別々の値を出力して欲しいのですが、Tomを生成してから変数の値を変えたにも関わらず、結果として同じ値が出力されています。
このようにクラスは参照型であるため、favaritefood変数を参照し、オブジェクトごとに値は保持せずにどのオブジェクトも共通の値を参照しにいきます。そのため、以下のようなプログラムにはクラスは向いていません。

class FavariteFood {
  var food: String = nil
}

class Person {
  var favaritefood: FavariteFood

  init(favaritefood: FavariteFood) {
    self.favaritefood = favaritefood
  }
}

let favaritefood = FavariteFood()
favaritefood.food = "Cake"
let Tom = Person(favaritefood: favaritefood)
favaritefood.food = "Orange"
let Margaret = Person(favaritefood: favaritefood)
Tom.favaritefood.food //Orange
Margaret.favaritefood.food //Orange

続いて今度は構造体で先ほどのプログラムを組んでみます。
すると先ほどとは違い、Personのオブジェクトごとに値が異なっています。
これは構造体が参照型ではなく、値型であるためそれぞれのPerson型のインスタンスが別々のFavariteFood型を保持しているためです。

struct FavariteFood {
  var food: String = nil
}

struct Person {
  var favaritefood: FavariteFood
}

let favaritefood = FavariteFood()
favaritefood.food = "Cake"
let Tom = Person(favaritefood: favaritefood)
favaritefood.food = "Orange"
let Margaret = Person(favaritefood: favaritefood)
Tom.favaritefood.food //Cake
Margaret.favaritefood.food //Orange

コピーオンライト

構造体にはコピーオンライトという機能がついています。
この機能は先ほどのように構造体を複数のオブジェクトとして生成した場合、そのオブジェクトが保持する値のコピーを必要になるまで行わないようにしてくれます。
以上の説明では分かりにくいと思うので噛み砕いて説明していきます。
下記のコードを見ると、二つ目に定義されている配列list2にlist1が代入されています。ここで勘違いされがちなのが、この時点で配列の内容がコピーされているという事です。実はこの時点では配列の値はコピーされておらず、その次の3行目でlistに新しい値が追加されて、値の内容に違いが生じた時に初めてコピーされます。
なぜこのような機能があるのかというと、Array型やDictionary型などのコレクションを表す型を使用する場合サイズの大きなデータを扱う可能性があるため、代入するたびにコピーを行なっていてはパフォーマンスの低下に繋がります。そのため、そのコピーの作業を値が異なった際に行う事で、その時点時点で必要のないコピー作業を省きパフォーマンスを向上させているのです。

var list = ["dog", "cat"]
var list2 = list
list.append("elephant")
list //["dog", "cat", "elephatn"]
list2 //["dog", "cat"]

クラスの利点

参照の共有

クラスの利点の一つとして値を参照して共有することが挙げられます。
以下のコードを見ると、クラスと構造体のオブジェクトごとにcountの値が異なっています。
これは参照型か値型かの違いによるものです。まず構造体型は変数などの値を参照せず、各オブジェクトごとに別々の値を保持しています。
そのため実行結果でcountを刻み出力していても、その値がもともとcount0を保持しているfirstExampleに影響を与え値が変更されるということはありません。
次にクラスをみていくと構造体とは異なり、secondExampleの値にも影響を与え値が変更されています。これはクラスが参照型であるため、同じ名称の変数の値を参照したためになります。
このように構造体とクラスには違いがあり、今回のケースであると数えたcountを保持して使用したい場合はクラスの方を使用するべきだと言えます。

protocol Example {
  var text: String {get set}
  var count: Int {get set}
  mutating func action()
}

extension Example {
  mutating func action() {
    count += 1
    print("text: \(text), count: \(count)")
  }
}

struct FirstExample : Example {
  var text = "Hello World"
  var count = 0

  init() {}
}

class SecondExample : Example {
  var text = "Hello ParallelWorld"
  var count = 0
}

Struct Counter {
  var example: Example

  mutating func start(){
    for _ in 0..<5 {
      example.action()
    }
  }
}

let firstExample : Example = FirstExample()
var counter1 = Counter(example: firstExample)
counter.start()
firstExample.count // 0

let secondExample : Example = SecondExample()
var counter2 = Counter(example: secondExample)
counter2.start()
secondExample.count // 5

//実行結果
text: HellWorld, count: 1
text: HellWorld, count: 2
text: HellWorld, count: 3
text: HellWorld, count: 4
text: HellWorld, count: 5
text: HellParallelWorld, count: 1
text: HellParallelWorld, count: 2
text: HellParallelWorld, count: 3
text: HellParallelWorld, count: 4
text: HellParallelWorld, count: 5

インスタンスのライフサイクルでの処理

以下のコードを見るとインスタンス化後のdataの値は"a data"だが、インスタンスの値をnilにした後にはdataがnilになっています。
これは構造体にないクラスの特徴のデイニシャライザが影響しています。デイニシャライザはdenitにより使用でき、インスタンスが破棄された際に実行され、その時に一時ファイルも削除されます。そのため以下のコードのように初期化の際にはinitが実行され、破棄された際にはdeinitが実行されこのような結果になりました。
そのためインスタンスのライフサイクルに合わせて処理を行いたい場合は構造体よりもクラスを使用するべきだと言えます。

var data: String?

class SomeClass {
  init() {
    print("Build a data")
    data = "a data"
  }
  deinit {
    print("Reset adata")
    data = nil
  }
}

var someClass: SomeClass? = SomeClass()
data //a data

someClass = nil
data  //nil

//実行結果
Build a data
Reset a data

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

iosアプリのアプリ内課金テスト(sandbox )で経験したおかしなこと

iOSの課金テスト、特に定期購読サービスのテストは結構不便なところがあります。

その上に、sandboxの課金環境はとても不安定で、テストのときによく躓くことがありました。

何個か書いてみたいと思います。

sandboxユーザーを切り替えることが出来ない

ios13 の問題ですが、iosは2つのアカウントでログインすることが出来ます。

普通にApp storeで利用しているアカウントでログインすることが出来ますが、

課金のテスト用で利用出来るsandboxのアカウントでもログインすることが出来ます。

sandboxのアカウントに一度ログインしてしまうと、二度とログアウトすることが出来ないのです・・・。_

これが結構テストのときに不便で、ログアウトする方法は iOS13.1にアップデートするか、端末の設定のところから端末をリセットする事です。

課金すると、端末には課金成功と表示されるが、実際のレスポンスはエラーが返却される・・・

これは結構厄介です。

テスターから、appleの課金は成功したのに、課金に失敗したと表示されますと報告が・・・。

ケーブルをつないでdebugしてみると、レスポンスがエラーが返却されるのに、エラーメッセージは 完了しました という文言が返却されました。

https://forums.developer.apple.com/thread/121096

これは他にも同じ症状の人がいたので、それからは調べることをやめました。

App Storeにアクセスできない・・・

これもどうしようか迷った、結構厄介な問題でした。

ストアからダウンロードしたものは何も問題ないのに、testflightからダウンロードしたものは課金アイテムの情報の取得も、購入も出来ませんでした。

エラーはこんな感じでした。

Error Domain=AMSErrorDomain Code=301 "Invalid Status Code" UserInfo={AMSStatusCode=503, 
NSLocalizedDescription=Invalid Status Code, 
NSLocalizedFailureReason=The response has an invalid status code}

301だったり、503だったりしたのでサーバーの問題か・・・?

でもappleのsystem statusをみても今は問題ないと表示されている

こちらですが、 DNS設定に 8.8.8.8と8.8.4.4 を追加したら課金周りのテストができるようになりました。

https://stackoverflow.com/questions/58443064/how-to-handle-skproductsrequest-301-invalid-status-code

こちらのstackoverflowに助けられました・・・。

単純に課金ができない

上記の理由でなくても、単純にsandbox環境は課金ができなくなるときがあります。

これは時間がすぎれば症状がなおるので、少し時間を空けてからテストすることにしましょう・・・。


他にもsandboxの問題を経験した方がいらっしゃれば、ぜひコメントお願いします!

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

【Swift】アクセスコントロール

アクセスコントロール

アクセスコントロールとは、モジュール内の型や型の要素に対する外部からのアクセスを制限することを指します。

アクセスレベル

アクセスコントロールをどの範囲で制限するかを下記のキーワードで決定できます。

open
モジュール内外の全てに対してアクセス許可を出す。

public
基本的にはopenと同じだが、モジュール外で継承したりオーバーライドはできない。

internal
同一モジュール内のアクセスに限りアクセス許可を出す。

fileprivate
同一のソースファイル内のアクセスのみを許可を出す。

private
対象の要素が属しているスコープに限りアクセス許可を出す。

デフォルトのアクセスレベル

型全体のデフォルトのアクセルレベル → internal

型全体のアクセスレベルがprivate、fileprivateに指定されている場合の型内部の要素 → 型のアクセスレベルと同一

型全体のアクセスレベルがopen、public、internalに指定されている場合の型内部の要素 → internal

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

iOS13のモーダルはFullscreenもいいけどCurrentContextも検討してみて

iOS13からモーダルを表示するときはセミモーダルがデフォルトの挙動となりました。
スクリーンショット 2019-10-20 20.02.06.png
完全なフルスクリーンではなく後ろのViewが少し見えるようなデザインで、下にスワイプで元のViewに戻ることができます。

これをされると困るデザインがあります。例えばログイン後に表示されるViewはセミモーダルではなくフルスクリーンで表示したいでしょう。

StoryboardからPresentationをFullscreenにすることで次のモーダルを完全なフルスクリーン状態で遷移できます。
スクリーンショット 2019-10-20 20.06.52.png

Swiftの場合はこうします。

let vc = ViewController()
vc.modalPresentationStyle = .fullScreen
present(vc, animated: true)

スクリーンショット 2019-10-20 20.09.27.png
この状態であればスワイプで戻ることもできません。
ここまでであればあまり問題はなさそうに見えます。

さらにModalで遷移したいとき

ここからさらにモーダルを表示したいとします。このときはセミモーダルのほうが利便性があがるので普通の遷移にします。
するとどうでしょう。

スクリーンショット 2019-10-20 20.12.18.png
なんと後ろのViewが下がらないままセミモーダルが出てしまいました。

iOS13においてfullscrrenでモーダル表示したものはずっとfullscreenの状態を維持するのでこのような動きになるようです。
でもこれじゃかっこよくない!下がってほしい!というときはCurrent Contextを使ってみます。

先ほどのFullscrrenからCurrent Contextに変更します。するとStoryboard上では灰色はフルスクリーンで、その次のモーダルでは灰色Viewが下にずれているのが確認できます。
(よく見ると上のStoryboardでもFullscreen選択時のモーダルの見え方はシミュレートされていましたね)
スクリーンショット 2019-10-20 20.15.10.png

ではこれで解決!かと思いきや実際実行してみると・・・

スクリーンショット 2019-10-20 20.17.49.png
なんと今度はステータスバーが全く見えなくなってしまいました。

CurrentContext時にStatusBarを見えるようにする

これは推測ですが、CurrentContextによって諸々の状態を参照する先が灰色のViewになっているのだと思われます。灰色Viewの時点ではステータスバーは黒色テキストなので、モーダル遷移後も灰色Viewからステータスバーの情報を取得していると考えると、黒の背景に黒のテキストとなってしまい見えなくなってしまったものと考えられます。

これを回避するにはViewController毎にステータスバーの色は何色にすべきかという情報を設定します。

灰色のViewControllerにpreferredStatusBarStyleプロパティをオーバーライドします。

class ViewController: UIViewController {
    override var preferredStatusBarStyle: UIStatusBarStyle {
        if presentedViewController != nil {
            return .lightContent
        } else {
            return super.preferredStatusBarStyle
        }
    }
}

presentedViewControllerは現在のViewController(灰色)からみてさらにモーダル表示しているViewControllerがあればそのViewControllerが、なければnilが返ってきます。
つまりpresentedViewControllerがnilでないなら何かしらモーダルを表示しているということです。
このときはステータスバーを白とし、それ以外はデフォルトの挙動としました。
ここを拡張することでもう少し細かな要望にも応えることができます。例えば次のModalもフルスクリーンだったらデフォルト挙動にするとかですね。

青色のViewControllerのpreferredStatusBarStyleは参照されませんでした。これもCurrentContextが影響していると考えられます。

これでめでたくセミモーダル時にステータスバーが白になりました。
スクリーンショット 2019-10-20 20.22.55.png

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

iOS13のモーダルはFullscreenもいいけどCurrentContextも検討したい

iOS13からモーダルを表示するときはセミモーダルがデフォルトの挙動となりました。
スクリーンショット 2019-10-20 20.02.06.png
完全なフルスクリーンではなく後ろのViewが少し見えるようなデザインで、下にスワイプで前のViewに戻ることができます。

これをされると困るデザインがあります。例えばログイン後に表示されるViewはセミモーダルではなくフルスクリーンで表示したいでしょう。

StoryboardからPresentationをFullscreenにすることで次のモーダルを完全なフルスクリーン状態で遷移できます。
スクリーンショット 2019-10-20 20.06.52.png

Swiftの場合はこうします。

let vc = ViewController()
vc.modalPresentationStyle = .fullScreen
present(vc, animated: true)

スクリーンショット 2019-10-20 20.09.27.png
この状態であればスワイプで戻ることもできません。
ここまでであればあまり問題はなさそうに見えます。

さらにModalで遷移したいとき

ここからさらにモーダルを表示したいとします。このときはセミモーダルのほうが利便性があがるので普通の遷移にします。
するとどうでしょう。

スクリーンショット 2019-10-20 20.12.18.png
なんと後ろのViewが下がらないままセミモーダルが出てしまいました。

iOS13においてfullscrrenでモーダル表示したものはずっとfullscreenの状態を維持するのでこのような動きになるようです。
でもこれじゃかっこよくない!下がってほしい!というときはCurrent Contextを使ってみます。

先ほどのFullscrrenからCurrent Contextに変更します。するとStoryboard上では灰色はフルスクリーンで、その次のモーダルでは灰色Viewが下にずれているのが確認できます。
(よく見ると上のStoryboardでもFullscreen選択時のモーダルの見え方はシミュレートされていましたね)
スクリーンショット 2019-10-20 20.15.10.png

ではこれで解決!かと思いきや実際実行してみると・・・

スクリーンショット 2019-10-20 20.17.49.png
なんと今度はステータスバーが全く見えなくなってしまいました。

CurrentContext時にStatusBarを見えるようにする

これは推測ですが、CurrentContextによって諸々の状態を参照する先が灰色のViewになっているのだと思われます。灰色Viewの時点ではステータスバーは黒色テキストなので、モーダル遷移後も灰色Viewからステータスバーの情報を取得していると考えると、黒の背景に黒のテキストとなってしまい見えなくなってしまったものと考えられます。

これを回避するにはViewController毎にステータスバーの色は何色にすべきかという情報を設定します。

灰色のViewControllerにpreferredStatusBarStyleプロパティをオーバーライドします。

class ViewController: UIViewController {
    override var preferredStatusBarStyle: UIStatusBarStyle {
        if presentedViewController != nil {
            return .lightContent
        } else {
            return super.preferredStatusBarStyle
        }
    }
}

presentedViewControllerは現在のViewController(灰色)からみてさらにモーダル表示しているViewControllerがあればそのViewControllerが、なければnilが返ってきます。
つまりpresentedViewControllerがnilでないなら何かしらモーダルを表示しているということです。
このときはステータスバーを白とし、それ以外はデフォルトの挙動としました。
ここを拡張することでもう少し細かな要望にも応えることができます。例えば次のModalもフルスクリーンだったらデフォルト挙動にするとかですね。

青色のViewControllerのpreferredStatusBarStyleは参照されませんでした。これもCurrentContextが影響していると考えられます。

これでめでたくセミモーダル時にステータスバーが白になりました。
スクリーンショット 2019-10-20 20.22.55.png

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

【swifter】Twitter連携アプリを作るためのswifterの主な関数の使い方の記録

Swifterとは

swiftで書かれたTwitter Framework.
ソースコードも公開されているため、使いやすい.
TWitterKitがサポート終了したし、こっち使えばいいんじゃね。

ソースコード : github : Swifter

Swifterに用意された主な関数の使い方

連携する

    //login処理
    // swifter構造体の宣言
    let swifter = Swifter(consumerKey: <twitter consumer key>, consumerSecret: <twitter consumer secret>)
    // のちに使う変数
    var twitterTokenKey:String!
    var twitterTokenSecret:String!

    swifter.authorize(
            withCallback: URL(string: "swifter-<twitter consumer key>://")!,
            presentingFrom: self,
            success: { accessToken, response in
                print(response)
                guard let accessToken = accessToken else { return }
                twitterTokenKey = accessToken.key
                twitterTokenSecret = accessToken.secret
            }, failure: { error in
                print(error)
            }
        )

tweetする

返り値はjsonデータ形式

    // さっきゲットしたtwitterTokenKeyとtwitterTokenSecretを使ってswifterを宣言し直す
    swifter = Swifter(consumerKey: <twitter consumer key>, consumerSecret: <twitter consumer secret>, oauthToken: twitterTokenKey, oauthTokenSecret: twitterTokenSecret)

    swifter.postTweet(status: "Tweetの内容", success: { json in
            // 成功時の処理
            print(json)
        }, failure: { error in
            // 失敗時の処理
            print(error)
        })

timelineを取得する

返り値はjsonデータ形式

        // こっちは、連携中のユーザーのフォローしてるユーザーのツイートを時系列順に。
        swifter.getHomeTimeline(count: 50,success: { json in
            // 成功時の処理
            print(json)
        }, failure: { error in
            // 失敗時の処理
            print(error)
        })

        // こっちは、@~~~で指定したユーザーのツイートを時系列順に取得。        
        swifter.getTimeline(for: .screenName("<twitterの@~~~のuser名の~~~の部分>"),success: { json in
            // 成功時の処理
            print(json)
        }, failure: { error in
            // 失敗時の処理
            print(error)
        })

まとめ

他にも、twitter上での操作はコードを通して全部できるようになってるっぽかったから、ソースコード見て必要な機能があったら実装していこう。

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

【Swift】TelloとUDP通信②- Telloからメッセージを受信する -

概要

前回、Telloに対しUDP通信でコマンドを送信しましたが、今回はコマンドに対しての応答について説明していきます。
各コマンドに対し、Telloから応答メッセージが送信されるため、端末側で受信処理を行うことでメッセージを取得できます。

受信処理

Telloにコマンドを送信した時、Telloから応答メッセージが送信されます。
NWConnection の receive を使用することで、受信処理を行えます。

let connection = NWConnection(host: "192.168.10.1", port: 8889, using: .udp)

//送信処理
connection.send(content: message, contentContext: .defaultMessage, isComplete: true, completion: .contentProcessed( error in ))

//受信処理
connection.receive(minimumIncompleteLength: 0, maximumLength: Int(Int32.max)) { (data: Data?, contentContext: NWConnection.ContentContext?, aBool: Bool, error: NWError?) in
         //処理内容           
}

取得したdataは文字に変換することで内容を確認できます。

if let data = data, let message = String(data: data, encoding: .utf8) {
         //応答メッセージ
         print("message: \(message)")
}

応答メッセージ

Tello SDKのコマンドでは、離陸、着陸等の操作に対しての応答と、バッテリー等のステータス情報に対しての応答があります。

操作コマンドは以下になります。応答としてはokかerrorを送信します。

コマンド 説明
command SDKモード開始
takeoff 離陸
land 着地
streamon ビデオストリーム開始
streamoff ビデオストリーム停止
emergency 緊急停止
up x 上にxcm進む
x:20-500
down x 下にxcm進む
x:20-500
left x 左にxcm進む
x:20-500
right x 右にxcm進む
x:20-500
forward x 前にxcm進む
x:20-500
back x 後ろにxcm進む
x:20-500
cw x 時計回りにx度回転
x: 1-3600
ccw x 反時計回りにx度回転
x: 1-3600
flip x xにひっくり返す
X:l (左) r (右) f (前) b (後ろ)
go x y z speed x y zにspeed(cm/s)で進む
x: 20-500 y: 20-500 z: 20-500 speed: 10-100
curve x1 y1 z1 x2 y2 z2 speed 現在の座標と2つの与えられた座標で定義された曲線をspeed(cm/s)で飛行
x1, x2: 20-500
y1, y2: 20-500
z1, z2: 20-500
speed: 10-60

Telloのステータス情報は以下のコマンドで確認できます。

コマンド 説明 応答
speed? 速度 x:1-100 (cm/s)
battery? バッテリー x:0-100
time? 飛行時間 x (s)
height? 高さ x:0-3000 (cm)
temp? 温度 x:0-90 (℃)
attitude? IMU姿勢データ pitch roll yaw
baro? 気圧 x (m)
acceleration? IMU角加速度データ x y z (0.001g)
tof? TOFから距離値 x:30-1000 (cm)
wifi? Wi-Fi SNR SNR

動画情報の取得

Telloのカメラからの情報は stremon のコマンドで動画情報を受信できるようになります。
取得する場合、PORT:8889からは取得できないため、IP:0.0.0.0 PORT:11111 のUDPサーバを作り受信させる必要があります。

参考

receive
Tello SDK

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

Xcode11で外部ディスプレイに表示できるようにする方法

はじめに

iPhone/iPadとApple Lightning - Digital AVアダプタを用いていることでHDMIモニター(TV)に映像を出力することができます。
通常はミラーリングとなりますが、本方法を用いると液晶(OLED)とHDMIで別の表示することができます。
例えば、iPhoneの液晶(OLED)を操作画面として、HDMIを表示画面にするなどできます。

Xcode11で外部ディスプレイに表示する方法を試行錯誤したので投稿します。
まだわかっていないところがあり、コメントいただければと思います。

実装

いくつかのサイトを参考に実装しました。
最後に参考サイトのリンクしますので確認してみてください。
また、サンプルプロジェクトをgithubに公開していますので参考にしてください。

UISceneを無効化

Xcode11からUIScreenからUISceneに変わっています。しかしながら新しいUISceneで外部ディスプレイ対応する方法がわかりませんでした?
なのでUISceneを無効化します。

githubのサンプルプロジェクトのcommitログを参考にしてください。
概要は下記の通りです。
* AppDelegate.swiftのUISceneに関連するメソッドを無効化(削除でも可)
* SceneDelegate.swiftの全てのメソッドを無効化(削除でも可)
* AppDelegate.swiftvar window: UIWindow?を追加
* Info.plistのUISceneに関する記述を削除

外部ディスプレイの接続切断通知を受け取れるように設定

接続切断通知を受け取れるようにします。

通知設定
    /// 外部ディスプレイの接続と切断の通知を受け取れるようにする
    private func setupNotification() {
        let center = NotificationCenter.default

        center.addObserver(self, selector: #selector(externalScreenDidConnect(notification:)), name: UIScreen.didConnectNotification, object: nil)

        center.addObserver(self, selector: #selector(externalScreenDidDisconnect(notification:)), name: UIScreen.didDisconnectNotification, object: nil)
    }

接続処理を実装

Storyboardで作成した画面にStoryboardIDを設定しておき接続時に画面を生成します。
必要に応じて外部から呼び出せるようにインスタンスを保持しておくと便利だと思います。

接続処理
    /// 外部ディスプレイを接続されたときに処理する
    /// - Parameter notification:接続されたUIScreenインスタンス
    @objc func externalScreenDidConnect(notification: NSNotification) {
        guard let screen = notification.object as? UIScreen else { return }
        guard externalWindow == nil else { return }
        guard let viewController = self.storyboard?.instantiateViewController(withIdentifier: "ExternalDisplayViewController") as? ExternalDisplayViewController else { return }

        externalWindow = UIWindow(frame: screen.bounds)
        externalWindow?.rootViewController = viewController
        //iOS13.0からDeprecatedになっているがUISceneを無効化しているのでこちらのプロパティーを設定する
        externalWindow?.screen = screen
        externalWindow?.isHidden = false

        //外部ディスプレイのViewControllerをあとで操作できるようにインスタンスを保持する
        externalDisplayViewController = viewController
    }

切断処理を実装

外部ディスプレイが切断時の処理も記述します。

切断処理
    /// 外部ディスプレイを切断されたときに処理する
    /// - Parameter notification: 切断されたUIScreenインスタンス
    @objc func externalScreenDidDisconnect(notification: NSNotification) {
        guard let screen = notification.object as? UIScreen else { return }
        guard let _externalWindow = externalWindow else { return }

        //切断通知されたscreenと現在表示中のscreenが同じかチェックする
        if screen == _externalWindow.screen {
            _externalWindow.isHidden = true
            externalWindow = nil
            //外部ディスプレイのViewControllerを削除する
            externalDisplayViewController = nil
        }
    }

最後に

他の記事を参考に作成しましたが、externalWindow?.screenするとDeprecatedになっているプロパティで警告されてしまいます。
正しい実装方法を調べてみましたが、わかりませんでした。
コメントをいただければと思います。
(あわせてこの記事とサンプルプロジェクトを変更していきたいと思います。)

参考サイト

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

ARKit, SceneKit個人的まとめ - 基礎編

シーンとノード

boundingBox, boundingSphere

boundingBox

図形や立体を最小で囲む図形の箱(StoryboardのTransformsでも見れる)

let (min, max) = hogeNode.boundingBox
//hogeNodeの幅を計算
let width = CGFloat(max.x - min.x)
//元の大きさ(boundingBox)の1/10にスケールする
let magnification = 0.1 * 1 / w
hogeNode.scale = SCNVector3(magnification, magnification, magnification)

boundingSphere

図形が変わっただけで考え方は同じ

->minが左下座標, maxが右上座標
boundingBox - SCNBoundingVolume | Apple Developer Documentation
->centerが中心座標, radiusが(対象がギリギリで全部おさまる)半径
boundingSphere - SCNBoundingVolume | Apple Developer Documentation

アニメーション

アニメーションはcoreAnimatonなど色々あるが
sceneKitではSCNActionを使うのが一般的らしい
確かにシンプルで使いやすい感じ。

他参考

iOS で SceneKit を試す(Swift 3) その16 - Scene Editor の Node Inspector - Apple Engine
->シリーズ90まであるので大変だけどこれ一通りやってしまえばSceneKitは攻略したもんだと思う、、、気がする

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

【Swift5.1】DuctTapeで初期化時の値の代入をもっと簡潔にする

はじめに

Swiftでオブジェクトのインスタンス化をしつつpropertyに値を代入する場合は、以下のような実装になるかと思います。

let label: UILabel = {
    let label = UILabel()
    label.numberOfLines = 0
    label.textColor = .red
    label.text = "Hello, World!!"
    return label
}()

上記と同じ状態のインスタンスを、メソッドチェーンで実現する方法を紹介しようと思います。

DuctTapeを利用する

DuctTapeというOSSを利用することで、先程の実装を以下のような実装にできます。

let label = UILabel().ductTape
    .numberOfLines(0)
    .textColor(.red)
    .text("Hello, World!!")
    .build()

NSObjectを継承したオブジェクトのインスタンスの.ductTapeにアクセスすると、Builderが返ってきます。
値を代入したいpropertyがある場合、そのproperty名と同一名のclosureにアクセスできるので、closureの引数として代入したい値を渡します。
一通りの値の設定が終わったら.build()を呼び出すことで、任意の値が代入された状態のインスタンスが返されます。

メソッドにアクセスする場合

インスタンスのメソッドにアクセスしたい場合は、Builderの.reinforceを介してインスタンスにアクセス
できるようになるため、渡されたインスタンスからメソッドにアクセスします。

let collectionView = UICollectionView().ductTape
    .backgroundColor(.red)
    .reinforce { $0.register(UITableViewCell.self, forCellWithReuseIdentifier: "Cell") }
    .build()

NSObjectを継承していないオブジェクトでも利用する

以下のように、Builderのinitializerの引数にインスタンスを渡すことで利用が可能になります。

class Dog {
    var name: String = ""
}

let dog = Builder(Dog())
    .name("Copernicus")
    .build()

DuctTapeの仕組み

Swift5.1から利用できるKeyPath dynamicMemberLookupを利用して実装してします。
そのためpropertyごとに代入用の処理を実装する必要はなく、subscript<Value>(dynamicMember keyPath: ReferenceWritableKeyPath<Base, Value>) -> (Value) -> Builder<Base>を実装してしまえば、返り値のclosureの引数で受け取った値をKeyPathを介してインスタンスに代入する処理を実現できます。
また、KeyPath dynamicMemberLookupによる実装のため、該当のproperty名が自動補完されます。

@dynamicMemberLookup
public struct Builder<Base: AnyObject> {

    private let _build: () -> Base

    public init(_ build: @escaping () -> Base) {
        self._build = build
    }

    public init(_ base: Base) {
        self._build = { base }
    }

    public subscript<Value>(dynamicMember keyPath: ReferenceWritableKeyPath<Base, Value>) -> (Value) -> Builder<Base> {
        { [build = _build] value in
            Builder {
                let object = build()
                object[keyPath: keyPath] = value
                return object
            }
        }
    }

    public func build() -> Base {
        _build()
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

IOS develop!IOS 開発

—VietNamese—
Xin chào mọi người, hiện em/mình đang học về ios developer qua swift, thông qua một số khoá học online.
Udemy: https://www.udemy.com/course/ios11-app-development-bootcamp/learn/lecture/7719428?start=75#overview
Sau đó em/mình sẽ học https://www.raywenderlich.com/
và cuối cùng là khoá của standford online.
em/mình có một số thắc mắc khi học nên nhờ mọi người giúp đỡ.

  1. Hiện tại em đang tự học , sau 1 năm tự học em có thể trở thành senior k? anh chị nào có kiến thức trước xin cho em kinh nghiệm và chia sẽ dùm em con đường và các khoá học tài liệu để e có thể định hình và đi tiếp ạ.

  2. Hiện nay các thị trường ở singapore hay châu âu hay nhật bản việc tuyển dụng ios với senior ra sao ạ? Yêu cầu và lương ra sao ạ? từ một người mới học ios với tự học có thể sau 1 năm sẽ ứng tuyển được k ạ?

Mong có anh chị nào có kinh nghiệm trước đó xin được xin ý kiến ạ.

—English—
Hello everyone, now I am studing about iOS development on swift by online cousre on Udemy:
Link: https://www.udemy.com/course/ios11-app-development-bootcamp/learn/lecture/7719428?start=75#overview
then, I will learn https://www.raywenderlich.com/
after, I learn Stanford online.

I have some question, can You help me?
1. Now I am studying swift, after 1 years only I can become senior developer? Can I do it? Can you give me advice to done this plan. Can you give me course, book, or seminar to help me do my plan. Thanks a lot
2. Now, What condition of IOS Developer in Singapore, Europe or Japan. ? after one years only studying, Can I apply?
Can you help me to answer?
---日本語---
みなさん、おはようございます。
現在、私はオンラインコースでIOS開発を学んでいます。
Udemy: https://www.udemy.com/course/ios11-app-development-bootcamp/learn/lecture/7719428?start=75#overview
このコースが終わった後、 https://www.raywenderlich.com/
最後はstandford online.
私は学ぶ中に質問がございます。
1。上のコースを学べば、1年間だけ上級開発ができますか?出来なければ、他のコース、本、セミナーがございましたら、教えてくれませんか?
2。私はシンガポールやヨーロッパや日本にてIOS開発募集について条件はなんでしょうか?1年間だけ勉強すれば、募集しても大丈夫でしょうか?

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

Azure Face API を利用して爆速で顔認証アプリを作る(パート2)

こんにちは、renです。

今回はAzure Face APIを利用して簡単な顔認証アプリを作っていきます。

こちらの記事はパート2(アプリ編)です
パート1(認証編)はこちら

環境

macOS Catalina 10.15
Xcode 11.1
iOS13
Swift 5

アプリの流れ

初期表示画面

認証画面へ遷移

カメラ起動

顔検出

画像として取得

データ型に変換し、検出 Face - Detect APIに投げる

レスポンスの faceId を 取得し、識別 Face - identify APIに投げる

レスポンスの confidence をチェックし、 9割以上で認証を成功とみなす

ホーム画面へ遷移

準備

カメラを利用するアプリなので、 Info.plistPrivacy - Camera Usage Description を有効化してください。

下記ライブラリを利用するため Podfile を編集してください。

pod 'Moya/RxSwift'
pod 'RxSwift'
pod 'SVProgressHUD'

pod install を実行してください。

pod install

APIRequestの実装

初めに endpointSubscriptionKeyConst に定義しておきましょう。

public struct Const {
    static let endpoint = "Your Endpoint"
    static let subscriptionKey = "Your SubscriptionKey"

    static let baseURL = "https://\(endpoint)/face/v1.0/"
    static let personGroupId = "sample_person_group"
}

APIリクエストでは Moya を利用します。

Moya を利用するために、まず Target を作成します。

import Moya

enum Target {
    case detect(imageData: Data)
    case identify(faceId: String)
}

extension Target: TargetType {

    var baseURL: URL {
        return URL(string: Const.baseURL)!
    }

    var path: String {
        switch self {
        case .detect:
            return "detect"
        case .identify:
            return "identify"
        }
    }

    var method: Moya.Method {
        switch self {
        case .detect:
            return .post
        case .identify:
            return .post
        }
    }

    var sampleData: Data {
        return Data()
    }

    var task: Task {
        switch self {
        case .detect(let imageData):

            return .requestCompositeData(bodyData: imageData, urlParameters: ["recognitionModel" : "recognition_02"])

        case .identify(let faceId):
            let parameters: [String: Any] = [
                "personGroupId": Const.personGroupId,
                "faceIds": [faceId]
            ]

            return .requestParameters(parameters: parameters, encoding: JSONEncoding.default)

        }

    }

    var headers: [String: String]? {
        var header = [
            "Ocp-Apim-Subscription-Key" : Const.subscriptionKey
        ]

        switch self {
        case .detect:
            header["Content-Type"] = "application/octet-stream"
            return header
        case .identify:
            header["Content-Type"] = "application/json"
            return header
        }
    }

}

次は APIRequest クラスを作成します。

import RxSwift
import Moya
import SVProgressHUD

enum Result<ResponseModel: Codable, ErrorResponseModel: Codable> {
    case success(ResponseModel)
    case invalid(ErrorResponseModel)
    case failure(Error)
}

public final class APIRequest {

    private let provider = MoyaProvider<Target>()

    func request<ResponseModel: Codable, ErrorResponseModel: Codable>
        (target: Target, response: ResponseModel.Type, errorResponse: ErrorResponseModel.Type,
         completion: @escaping ((Result<ResponseModel, ErrorResponseModel>) -> Void )) {

        SVProgressHUD.setDefaultMaskType(.black)
        SVProgressHUD.show()
        provider.request(target) { result in
            SVProgressHUD.dismiss()
            switch result {
            case let .success(response):
                do {
                    let serializedResponse = try response.filterSuccessfulStatusCodes().map(ResponseModel.self)
                    dump(serializedResponse)

                    completion(.success(serializedResponse))

                } catch {
                    guard let errorResponse = self.serializeError(response: response, errorResponse: errorResponse) else { completion(.failure(error)); return }
                    dump(errorResponse)

                    completion(.invalid(errorResponse))

                }
            case let .failure(error):
                dump(error)
                completion(.failure(error))
            }
        }
    }

    private func serializeError<ErrorResponseModel: Codable>
        (response: Moya.Response, errorResponse: ErrorResponseModel.Type) -> ErrorResponseModel? {

        do {
            let errorResponse = try response.map(ErrorResponseModel.self)
            dump(errorResponse)
            return errorResponse
        } catch {
            return nil
        }
    }
}

次は、それぞれのAPIのレスポンスモデルを作成します。

struct FaceDetectResponse: Codable {
    let faceId: String
    let faceRectangle: FaceRectangle

    struct FaceRectangle: Codable {
        let top: Int
        let left: Int
        let width: Int
        let height: Int
    }
}

struct FaceIdentifyResponse: Codable {
    let faceId: String
    let candidates: [Candidates]

    struct Candidates: Codable {
        let personId: String
        let confidence: Double
    }

}

struct ErrorResponse: Codable {
    public let error: ErrorStatus

    struct ErrorStatus: Codable {
        let code: String
        let message: String
    }
}

これにてAPIリクエストの部分は完成です。

Viewの作成

次に View を作成します。下の画像を参考に作成してください。

スクリーンショット 2019-10-20 13.02.49.png

真ん中のView(FaceAuthView)のViewControllerを作成します。

import UIKit

class FaceAuthViewController: UIViewController {

    // カメラの映像が映るView
    @IBOutlet private weak var cameraView: UIView!
    // cameraViewに覆いかぶさっているView
    @IBOutlet private weak var overlayView: UIView!
    // overlayViewの中心の透明なView
    @IBOutlet private weak var centerView: UIView!
    // アラートを表示するためのLabel
    @IBOutlet private weak var alertLabel: UILabel!

    // 顔検出を行うクラス
    private var faceDetecer: FaceDetecer?
    // 顔の周りに表示する枠
    private let frameView = UIView()
    // 顔検出されたときの画像
    private var image = UIImage()

    // APIリクエストを行うクラス
    private let apiRequest = APIRequest()

    override func viewDidLoad() {
        setupFrameView()
    }

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

    override func viewWillDisappear(_ animated: Bool) {
        if let faceDetecer = faceDetecer {
            faceDetecer.stopRunning()
        }
        faceDetecer = nil
    }

    private func setupFrameView() {
        frameView.layer.borderWidth = 3
        view.addSubview(frameView)
    }

    private func setupFaceDetecer() {
        faceDetecer = FaceDetecer(view: cameraView, completion: {faceRect, image in
            self.frameView.frame = faceRect
            self.image = image
            self.isInFrame(faceRect: faceRect)
        })
    }

    private func dispOverlayView() {
        overlayView.isHidden = false
    }

    private func isInFrame(faceRect: CGRect) {

        let xIsInFrame = centerView.frame.minX < faceRect.minX && faceRect.maxX < centerView.frame.maxX
        let yIsInFrame = centerView.frame.minY < faceRect.minY && faceRect.maxY < centerView.frame.maxY

        if xIsInFrame && yIsInFrame {
            stopRunning()
            faceDetect(image: image)
        }

    }

    private func faceDetect(image: UIImage) {

        guard let imageData = image.jpegData(compressionQuality: 1.0) else { return }

        apiRequest.request(target: .detect(imageData: imageData), response: [FaceDetectResponse].self, errorResponse: ErrorResponse.self) { respose in
            switch respose {
            case .success(let faceDetectResponse):

                guard let faceId = faceDetectResponse.first?.faceId else {
                    self.alertLabel.text = "顔認証に失敗しました"
                    self.startRunning()
                    return
                }

                self.faceIdentify(faceId: faceId)

            case .invalid(let errorResponse):
                print(errorResponse)
                self.startRunning()
            case .failure(let error):
                print(error)
                self.startRunning()
            }
        }

    }

    private func faceIdentify(faceId: String) {
        apiRequest.request(target: .identify(faceId: faceId), response: [FaceIdentifyResponse].self, errorResponse: ErrorResponse.self) { respose in
            switch respose {
            case .success(let faceIdentifyResponse):

                // 最初の顔で判定
                guard let candidate = faceIdentifyResponse.first?.candidates.first?.confidence else {
                    self.alertLabel.text = "顔が登録されていません"
                    self.startRunning()
                    return
                }

                let candidateInt = Int(candidate * 100)
                self.alertLabel.text = "信頼度は \(candidateInt)% です"

                if candidate > 0.9 {
                    self.login()
                } else {
                    self.startRunning()
                }

            case .invalid(let errorResponse):
                print(errorResponse)
                self.startRunning()
            case .failure(let error):
                print(error)
                self.startRunning()
            }
        }
    }

    private func startRunning() {
        guard let faceDetecer = faceDetecer else { return }
        faceDetecer.startRunning()
    }

    private func stopRunning() {
        guard let faceDetecer = faceDetecer else { return }
        faceDetecer.stopRunning()
    }

    private func login() {
        self.performSegue(withIdentifier: "gotoHome", sender: nil)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction private func tappedBackButton(_ sender: UIButton) {
        dismiss(animated: true, completion: nil)
    }

}

顔検出を行うクラス FaceDetecer を作成

import UIKit
import AVFoundation

final class FaceDetecer: NSObject {
    private let captureSession = AVCaptureSession()
    private var videoDataOutput = AVCaptureVideoDataOutput()
    private var view: UIView
    private var completion: (_ rect: CGRect, _ image: UIImage) -> Void

    required init(view: UIView, completion: @escaping (_ rect: CGRect, _ image: UIImage) -> Void) {
        self.view = view
        self.completion = completion
        super.init()
        self.initialize()
    }

    private func initialize() {
        addCaptureSessionInput()
        registerDelegate()
        setVideoDataOutput()
        addCaptureSessionOutput()
        addVideoPreviewLayer()
        setCameraOrientation()
        startRunning()
    }

    private func addCaptureSessionInput() {
        do {
            guard let frontVideoCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else { return }
            let frontVideoCameraInput = try AVCaptureDeviceInput(device: frontVideoCamera) as AVCaptureDeviceInput
            captureSession.addInput(frontVideoCameraInput)
        } catch let error {
            print(error)
        }
    }

    private func setVideoDataOutput() {
        videoDataOutput.alwaysDiscardsLateVideoFrames = true

        guard let pixelFormatTypeKey = kCVPixelBufferPixelFormatTypeKey as AnyHashable as? String else { return }
        let pixelFormatTypeValue = Int(kCVPixelFormatType_32BGRA)

        videoDataOutput.videoSettings = [pixelFormatTypeKey : pixelFormatTypeValue]
    }

    private func setCameraOrientation() {
        for connection in videoDataOutput.connections where connection.isVideoOrientationSupported {
            connection.videoOrientation = .portrait
            connection.isVideoMirrored = true
        }
    }

    private func registerDelegate() {
        let queue = DispatchQueue(label: "queue", attributes: .concurrent)
        videoDataOutput.setSampleBufferDelegate(self, queue: queue)
    }

    private func addCaptureSessionOutput() {
        captureSession.addOutput(videoDataOutput)
    }

    private func addVideoPreviewLayer() {
        let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        videoPreviewLayer.frame = view.bounds
        videoPreviewLayer.videoGravity = .resizeAspectFill

        view.layer.addSublayer(videoPreviewLayer)
    }

    func startRunning() {
        captureSession.startRunning()
    }

    func stopRunning() {
        captureSession.stopRunning()
    }

    private func convertToImage(from sampleBuffer: CMSampleBuffer) -> UIImage? {

        guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil }

        CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))

        let baseAddress = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)
        let width = CVPixelBufferGetWidth(imageBuffer)
        let height = CVPixelBufferGetHeight(imageBuffer)

        let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer)
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
        let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)

        guard let imageRef = context?.makeImage() else { return nil }

        CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
        let resultImage = UIImage(cgImage: imageRef)

        return resultImage
    }
}

extension FaceDetecer: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        DispatchQueue.main.sync(execute: {

            guard let image = convertToImage(from: sampleBuffer), let ciimage = CIImage(image: image) else { return }
            guard let detector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) else { return }
            guard let feature = detector.features(in: ciimage).first else { return }

            sendFaceRect(feature: feature, image: image)

        })
    }

    private func sendFaceRect(feature: CIFeature, image: UIImage) {
        var faceRect = feature.bounds

        let widthPer = view.bounds.width / image.size.width
        let heightPer = view.bounds.height / image.size.height

        // 原点を揃える
        faceRect.origin.y = image.size.height - faceRect.origin.y - faceRect.size.height

        // 倍率変換
        faceRect.origin.x *= widthPer
        faceRect.origin.y *= heightPer
        faceRect.size.width *= widthPer
        faceRect.size.height *= heightPer

        completion(faceRect, image)
    }
}

これで実装は終了です。

AzureFaceAPIは無料プランだと1分間に20回までしかリクエストを送れないのでご注意ください。

あとがき

今回はリアルタイム顔検出とAzureFaceAPIの連携で顔認証アプリを作成しました。
初めてカメラの機能を実装したのですが、思ったよりも簡単にできて驚きました。
これも普段からQiitaを書いてくださる皆様のおかげだと思います。ありがとうございます。

サンプルアプリはGitHubに公開しています。
https://github.com/renchild8/FaceAuthSample

この記事は下記の記事を参考にしています。
[コピペで使える]swift3/swift4/swift5でリアルタイム顔認識をする方法

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

iOSでリアルタイム顔検出を行う

こんにちはrenです。

今回はリアルタイム顔検出のアプリを作っていきます。
一週間程度でぱぱっと作ったものなので、間違いなどあればご指摘等いただけると嬉しいです。

環境

macOS Catalina 10.15
Xcode 11.1
iOS13
Swift 5

流れ

「Login」ボタンをタップ

フロントカメラを起動する

出力された映像を切り出し、画像に変換する

画像から顔を検出する

検出された顔の座標にフレームを表示させる

準備

今回はカメラを利用するのでInfo.plistPrivacy - Camera Usage Descriptionを追加してください

実装

// 初期表示View
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func tappedLogin(_ sender: Any) {
        self.performSegue(withIdentifier: "gotoFaceDetect", sender: nil)
    }

}

// 顔検出をするView
import UIKit

class FaceDetectViewController: UIViewController {

    @IBOutlet weak var cameraView: UIView!

    // 顔検出をするためのクラス
    private var faceDetecter: FaceDetecter?
    // 検出された顔のフレームを表示するためのView
    private let frameView = UIView()
    // 切り出された画像
    private var image = UIImage()

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

    override func viewWillDisappear(_ animated: Bool) {
        if let faceDetecter = faceDetecter {
            faceDetecter.stopRunning()
        }
        faceDetecter = nil
    }

    private func setup() {
        frameView.layer.borderWidth = 3
        view.addSubview(frameView)
        faceDetecter = FaceDetecter(view: cameraView, completion: {faceRect, image in
            self.frameView.frame = faceRect
            self.image = image
        })
    }

    private func stopRunning() {
        guard let faceDetecter = faceDetecter else { return }
        faceDetecter.stopRunning()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func tappedBackButton(_ sender: UIButton) {
        dismiss(animated: true, completion: nil)
    }

}

// 顔検出をするクラス
import UIKit
import AVFoundation

final class FaceDetecter: NSObject {
    private let captureSession = AVCaptureSession()
    private var videoDataOutput = AVCaptureVideoDataOutput()
    private var view: UIView
    private var completion: (_ rect: CGRect, _ image: UIImage) -> Void

    required init(view: UIView, completion: @escaping (_ rect: CGRect, _ image: UIImage) -> Void) {
        self.view = view
        self.completion = completion
        super.init()
        self.initialize()
    }

    private func initialize() {
        addCaptureSessionInput()
        registerDelegate()
        setVideoDataOutput()
        addCaptureSessionOutput()
        addVideoPreviewLayer()
        setCameraOrientation()
        startRunning()
    }

    private func addCaptureSessionInput() {
        do {
            guard let frontVideoCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else { return }
            let frontVideoCameraInput = try AVCaptureDeviceInput(device: frontVideoCamera) as AVCaptureDeviceInput
            captureSession.addInput(frontVideoCameraInput)
        } catch let error {
            print(error)
        }
    }

    private func setVideoDataOutput() {
        videoDataOutput.alwaysDiscardsLateVideoFrames = true

        guard let pixelFormatTypeKey = kCVPixelBufferPixelFormatTypeKey as AnyHashable as? String else { return }
        let pixelFormatTypeValue = Int(kCVPixelFormatType_32BGRA)

        videoDataOutput.videoSettings = [pixelFormatTypeKey : pixelFormatTypeValue]
    }

    private func setCameraOrientation() {
        for connection in videoDataOutput.connections where connection.isVideoOrientationSupported {
            connection.videoOrientation = .portrait
            connection.isVideoMirrored = true
        }
    }

    private func registerDelegate() {
        let queue = DispatchQueue(label: "queue", attributes: .concurrent)
        videoDataOutput.setSampleBufferDelegate(self, queue: queue)
    }

    private func addCaptureSessionOutput() {
        captureSession.addOutput(videoDataOutput)
    }

    private func addVideoPreviewLayer() {
        let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        videoPreviewLayer.frame = view.bounds
        videoPreviewLayer.videoGravity = .resizeAspectFill

        view.layer.addSublayer(videoPreviewLayer)
    }

    func startRunning() {
        captureSession.startRunning()
    }

    func stopRunning() {
        captureSession.stopRunning()
    }

    private func convertToImage(from sampleBuffer: CMSampleBuffer) -> UIImage? {

        guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil }

        CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))

        let baseAddress = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)
        let width = CVPixelBufferGetWidth(imageBuffer)
        let height = CVPixelBufferGetHeight(imageBuffer)

        let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer)
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
        let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)

        guard let imageRef = context?.makeImage() else { return nil }

        CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
        let resultImage = UIImage(cgImage: imageRef)

        return resultImage
    }
}

extension FaceDetecter: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        DispatchQueue.main.sync(execute: {

            guard let image = convertToImage(from: sampleBuffer), let ciimage = CIImage(image: image) else { return }
            guard let detector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) else { return }
            guard let feature = detector.features(in: ciimage).first else { return }

            sendFaceRect(feature: feature, image: image)

        })
    }

    private func sendFaceRect(feature: CIFeature, image: UIImage) {
        var faceRect = feature.bounds

        let widthPer = view.bounds.width / image.size.width
        let heightPer = view.bounds.height / image.size.height

        // 原点を揃える
        faceRect.origin.y = image.size.height - faceRect.origin.y - faceRect.size.height

        // 倍率変換
        faceRect.origin.x *= widthPer
        faceRect.origin.y *= heightPer
        faceRect.size.width *= widthPer
        faceRect.size.height *= heightPer

        completion(faceRect, image)
    }
}

この記事は下記の記事を参考にしています。
[コピペで使える]swift3/swift4/swift5でリアルタイム顔認識をする方法

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

ARKit個人的まとめ - Day1

仕事でARKitを触ることになったのでまとめ

やったこと

カメラの映像に
・テキストを配置する
・テクスチャを配置する
・それらをタップで動かす
・それらをドラッグで動かす
・平面を検出する

使ったもの

・ARKit
・SceneKit

コード

テキストを配置する

・backgroundColor出ても最初の一瞬だけ
・SCNViewとARSCNViewは違うらしい->あとで追記
・willAppearでconfig設定してrun & willDisAppearでpauseの流れが鉄板らしい
->平面検出のときはここにさらに設定を加えたりする

ARText.swift
import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let scene = SCNScene()
        scnView.backgroundColor = UIColor.red
        scnView.allowsCameraControl = true //カメラ位置をタップでコントロール可能にする
        scnView.scene = scene
        scnView.autoenablesDefaultLighting = true

        let str = "本日は晴天なり"
        let depth: CGFloat = 0.5
        let text = SCNText(string: str, extrusionDepth: depth)
        text.font = UIFont(name: "HiraKakuProN-W6", size: 0.5);
        let textNode = SCNNode(geometry: text)

        let (min, max) = (textNode.boundingBox)
        let x = CGFloat(max.x - min.x)
        let y = CGFloat(max.y - min.y)
        textNode.position = SCNVector3(-(x/2), -1, -2)

        print("\(str) width=\(x)m height=\(y)m depth=\(depth)m")

        scnView.scene.rootNode.addChildNode(textNode)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        let configuration = ARWorldTrackingConfiguration()
        scnView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        scnView.session.pause()
    }
}

テクスチャを配置する

あとで追記

それらをタップで動かす

あとで追記

それらをドラッグで動かす・平面を検出する

・secondSceneViewという名前に特に意味はないです
・namedにはファイル名、inDirectoryにはパスを書きましょう(当たり前のはずなのにここ逆にしてて時間ムッチャ無駄にしたのでメモ)
->inDirectoryがパス
->ここのsceneはこの情報のSCNSceneが存在するかどうかnilチェックしてるだけ
->viewDidLoadのsceneSecondとは別物

guard let scene = SCNScene(named: "FinalBaseMesh.scn", inDirectory: "art.scnassets") else {fatalError()}

・withNameはidentityのNameにしよう
->Nameはファイル名(.scn)と一緒でないとここでクラッシュする
->GeometoryのNameもこれと同じにすると平面検出ごとにテクスチャが新規追加される
->テクスチャは出るけどなんか一色の塊みたいな感じになってて変
->あとで追記

guard let bearNode = scene.rootNode.childNode(withName: "FinalBaseMesh", recursively: true) else {fatalError()}
ARDrag+DetectSurface.swift
import UIKit
import SceneKit
import ARKit

class SecondViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet var secondSceneView: ARSCNView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let sceneSecond = SCNScene()
        secondSceneView.scene = sceneSecond
        secondSceneView.delegate = self
        secondSceneView.showsStatistics = true
        secondSceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints
        //tapGesture
        secondSceneView.addGestureRecognizer(UITapGestureRecognizer(
            target: self, action: #selector(self.tapView(sender:))))
        //ドラッグはあとで
//        sceneView.addGestureRecognizer(UIPanGestureRecognizer(
//            target: self, action: #selector(self.dragView(sender:))))
    }

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

        let configuration = ARWorldTrackingConfiguration()
        //検知対象を指定する。現在は水平のみ指定可能。
        configuration.planeDetection = .horizontal
        //セッションを稼働
        secondSceneView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        //セッションを停止
        secondSceneView.session.pause()
    }


    // MARK: - ARSCNViewDelegate

    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        print("\(self.classForCoder)/" + #function)
        guard let planeAnchor = anchor as? ARPlaneAnchor else {fatalError()}
        //sceneとnodeを読み込み
        guard let scene = SCNScene(named: "FinalBaseMesh.scn", inDirectory: "art.scnassets") else {fatalError()}
        //identityちゃんと設定
        guard let bearNode = scene.rootNode.childNode(withName: "FinalBaseMesh", recursively: true) else {fatalError()}
        // nodeのスケールを調整する
        let (min, max) = bearNode.boundingBox
        let w = CGFloat(max.x - min.x)
        // 1mを基準にした縮尺を計算
        let magnification = 1.0 / w
        bearNode.scale = SCNVector3(magnification, magnification, magnification)
        // nodeのポジションを設定
        bearNode.position = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z)

        //作成したノードを追加
        DispatchQueue.main.async(execute: {
            node.addChildNode(bearNode)
        })

    }

    @objc func tapView(sender: UIGestureRecognizer) {
        let tapPoint = sender.location(in: secondSceneView)
        let results = secondSceneView.hitTest(tapPoint, types: .existingPlaneUsingExtent)
        if !results.isEmpty {
            if let result = results.first ,
                let anchor = result.anchor ,
                let node = secondSceneView.node(for: anchor) {

                let action1 = SCNAction.rotateBy(x: CGFloat(-90 * (Float.pi / 180)), y: 0, z: 0, duration: 0.5)
                let action2 = SCNAction.wait(duration: 1)

                DispatchQueue.main.async(execute: {
                    node.runAction(
                        SCNAction.sequence([
                        action1,
                        action2,
                        action1.reversed()
                        ])
                    )
                })

            }
        }
    }

    @objc func dragView(sender: UIGestureRecognizer) {
        let tapPoint = sender.location(in: secondSceneView)

        let results = secondSceneView.hitTest(tapPoint, types: .existingPlane)
        if !results.isEmpty {
            if let result = results.first ,
                let anchor = result.anchor ,
                let node = secondSceneView.node(for: anchor) {

                DispatchQueue.main.async(execute: {
                    // 実世界の座標をSCNVector3で返したものを反映
                    node.position = SCNVector3(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y, result.worldTransform.columns.3.z)
                })
            }
        }
    }

}

分からないこと

・ライフサイクルとrenderの違いはなに
->backgroundcolor一瞬しか反映しない
・ドラッグでちゃんと動かす方法

参考

ARKitで簡単ARやってみた - Qiita
ARKitのはじめかた その1「5分で出来るARアプリ」 - Qiita
SceneKitのシーンやノードを理解【3次元世界への入り口】 – techpartner

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

ARKit個人的まとめ - Vol.1

仕事でARKitを触ることになったのでまとめ

やったこと

カメラの映像に
・テキストを作成・配置する
・テクスチャを作成・配置する
・タップで動かす
・ドラッグで動かす
・平面を検出する

使ったもの

・ARKit
・SceneKit

コード

テキストを配置する

・backgroundColor出ても最初の一瞬だけ
・SCNViewとARSCNViewは違うらしい->あとで追記
・willAppearでconfig設定してrun & willDisAppearでpauseの流れが鉄板らしい
->平面検出のときはここにさらに設定を加えたりする

ARText.swift
import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let scene = SCNScene()
        scnView.backgroundColor = UIColor.red
        scnView.allowsCameraControl = true //カメラ位置をタップでコントロール可能にする
        scnView.scene = scene
        scnView.autoenablesDefaultLighting = true

        let str = "本日は晴天なり"
        let depth: CGFloat = 0.5
        let text = SCNText(string: str, extrusionDepth: depth)
        text.font = UIFont(name: "HiraKakuProN-W6", size: 0.5);
        let textNode = SCNNode(geometry: text)

        let (min, max) = (textNode.boundingBox)
        let x = CGFloat(max.x - min.x)
        let y = CGFloat(max.y - min.y)
        textNode.position = SCNVector3(-(x/2), -1, -2)

        print("\(str) width=\(x)m height=\(y)m depth=\(depth)m")

        scnView.scene.rootNode.addChildNode(textNode)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        let configuration = ARWorldTrackingConfiguration()
        scnView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        scnView.session.pause()
    }
}

テクスチャを作成・配置する

あとで追記

ドラッグ・タップで動かす・平面を検出する

・secondSceneViewという名前に特に意味はないです
・namedにはファイル名、inDirectoryにはパスを書きましょう(当たり前のはずなのにここ逆にしてて時間ムッチャ無駄にしたのでメモ)
->inDirectoryがパス
->ここのsceneはこの情報のSCNSceneが存在するかどうかnilチェックしてるだけ
->viewDidLoadのsceneSecondとは別物

guard let scene = SCNScene(named: "FinalBaseMesh.scn", inDirectory: "art.scnassets") else {fatalError()}

・withNameはidentityのNameにしよう
->Nameはファイル名(.scn)と一緒でないとここでクラッシュする
->GeometoryのNameもこれと同じにすると平面検出ごとにテクスチャが新規追加される
->テクスチャは出るけどなんか一色の塊みたいな感じになってて変
->あとで追記

guard let bearNode = scene.rootNode.childNode(withName: "FinalBaseMesh", recursively: true) else {fatalError()}
ARDrag+DetectSurface.swift
import UIKit
import SceneKit
import ARKit

class SecondViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet var secondSceneView: ARSCNView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let sceneSecond = SCNScene()
        secondSceneView.scene = sceneSecond
        secondSceneView.delegate = self
        secondSceneView.showsStatistics = true
        secondSceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints
        //tapGesture
        secondSceneView.addGestureRecognizer(UITapGestureRecognizer(
            target: self, action: #selector(self.tapView(sender:))))
        secondSceneView.addGestureRecognizer(UIPanGestureRecognizer(
            target: self, action: #selector(self.dragView(sender:))))
    }

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

        let configuration = ARWorldTrackingConfiguration()
        //検知対象を指定する。現在は水平のみ指定可能。
        configuration.planeDetection = .horizontal
        //セッションを稼働
        secondSceneView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        //セッションを停止
        secondSceneView.session.pause()
    }


    // MARK: - ARSCNViewDelegate

    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        print("\(self.classForCoder)/" + #function)
        guard let planeAnchor = anchor as? ARPlaneAnchor else {fatalError()}
        //sceneとnodeを読み込み
        guard let scene = SCNScene(named: "FinalBaseMesh.scn", inDirectory: "art.scnassets") else {fatalError()}
        //identityちゃんと設定
        guard let bearNode = scene.rootNode.childNode(withName: "FinalBaseMesh", recursively: true) else {fatalError()}
        // nodeのスケールを調整する
        let (min, max) = bearNode.boundingBox
        let w = CGFloat(max.x - min.x)
        // 1mを基準にした縮尺を計算
        let magnification = 1.0 / w
        bearNode.scale = SCNVector3(magnification, magnification, magnification)
        // nodeのポジションを設定
        bearNode.position = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z)

        //作成したノードを追加
        DispatchQueue.main.async(execute: {
            node.addChildNode(bearNode)
        })

    }

    @objc func tapView(sender: UIGestureRecognizer) {
        let tapPoint = sender.location(in: secondSceneView)
        let results = secondSceneView.hitTest(tapPoint, types: .existingPlaneUsingExtent)
        if !results.isEmpty {
            if let result = results.first ,
                let anchor = result.anchor ,
                let node = secondSceneView.node(for: anchor) {

                let action1 = SCNAction.rotateBy(x: CGFloat(-90 * (Float.pi / 180)), y: 0, z: 0, duration: 0.5)
                let action2 = SCNAction.wait(duration: 1)

                DispatchQueue.main.async(execute: {
                    node.runAction(
                        SCNAction.sequence([
                        action1,
                        action2,
                        action1.reversed()
                        ])
                    )
                })

            }
        }
    }

    @objc func dragView(sender: UIGestureRecognizer) {
        let tapPoint = sender.location(in: secondSceneView)

        let results = secondSceneView.hitTest(tapPoint, types: .existingPlane)
        if !results.isEmpty {
            if let result = results.first ,
                let anchor = result.anchor ,
                let node = secondSceneView.node(for: anchor) {

                DispatchQueue.main.async(execute: {
                    // 実世界の座標をSCNVector3で返したものを反映
                    node.position = SCNVector3(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y, result.worldTransform.columns.3.z)
                })
            }
        }
    }

}

分からないこと

・ライフサイクルとrenderの違いはなに
->backgroundcolor一瞬しか反映しない
・ドラッグでちゃんと動かす方法

参考

ARKitで簡単ARやってみた - Qiita
ARKitのはじめかた その1「5分で出来るARアプリ」 - Qiita
SceneKitのシーンやノードを理解【3次元世界への入り口】 – techpartner

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

ARKit, SceneKit個人的まとめ - Vol.1

仕事でARKitを触ることになったのでまとめ
基礎編はこちら

やったこと

カメラの映像に
・テキストを作成・配置する
・テクスチャを作成・配置する
・タップで動かす
・ドラッグで動かす
・平面を検出する

使ったもの

・ARKit
・SceneKit

コード

テキストを配置する

・backgroundColor出ても最初の一瞬だけ
・SCNViewとARSCNViewは違うらしい->あとで追記
・willAppearでconfig設定してrun & willDisAppearでpauseの流れが鉄板らしい
->平面検出のときはviewDidLoadではなくrender(_ didAdd)に書く
->リアルタイムでテクスチャの座標を更新したい場合とか

ARText.swift
import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let scene = SCNScene()
        scnView.backgroundColor = UIColor.red
        scnView.allowsCameraControl = true //カメラ位置をタップでコントロール可能にする
        scnView.scene = scene

        let str = "本日は晴天なり"
        let depth: CGFloat = 0.5
        let text = SCNText(string: str, extrusionDepth: depth)
        text.font = UIFont(name: "HiraKakuProN-W6", size: 0.5);
        let textNode = SCNNode(geometry: text)

        let (min, max) = (textNode.boundingBox)
        let x = CGFloat(max.x - min.x)
        let y = CGFloat(max.y - min.y)
        textNode.position = SCNVector3(-(x/2), -1, -2)

        print("\(str) width=\(x)m height=\(y)m depth=\(depth)m")

        scnView.scene.rootNode.addChildNode(textNode)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        let configuration = ARWorldTrackingConfiguration()
        scnView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        scnView.session.pause()
    }
}

テクスチャを作成・配置する

あとで追記

ドラッグ・タップで動かす・平面を検出する

・secondSceneViewという名前に特に意味はないです
・namedにはファイル名、inDirectoryにはパスを書きましょう(当たり前のはずなのにここ逆にしてて時間ムッチャ無駄にしたのでメモ)
->ここのsceneはこの情報のSCNSceneが存在するかどうかnilチェックしてるだけ
->viewDidLoadのsceneSecondとは別物

guard let scene = SCNScene(named: "FinalBaseMesh.scn", inDirectory: "art.scnassets") else {fatalError()}

・withNameはidentityのNameにしよう
->Nameはファイル名(.scn)と一緒でないとここでクラッシュする
->GeometoryのNameもこれと同じにすると平面検出ごとにテクスチャが新規追加される
->テクスチャは出るけどなんか一色の塊みたいな感じになってて変(Sampleにあるship.scnはlightの設定加えたらでた)

secondSceneView.autoenablesDefaultLighting = true

->あとで追記

guard let bearNode = scene.rootNode.childNode(withName: "FinalBaseMesh", recursively: true) else {fatalError()}
ARDrag+DetectSurface.swift
import UIKit
import SceneKit
import ARKit

class SecondViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet var secondSceneView: ARSCNView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let sceneSecond = SCNScene()
        secondSceneView.scene = sceneSecond
        secondSceneView.delegate = self
        secondSceneView.showsStatistics = true
        secondSceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints
        //tapGesture
        secondSceneView.addGestureRecognizer(UITapGestureRecognizer(
            target: self, action: #selector(self.tapView(sender:))))
        secondSceneView.addGestureRecognizer(UIPanGestureRecognizer(
            target: self, action: #selector(self.dragView(sender:))))
    }

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

        let configuration = ARWorldTrackingConfiguration()
        //検知対象を指定する。現在は水平のみ指定可能。
        configuration.planeDetection = .horizontal
        //セッションを稼働
        secondSceneView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        //セッションを停止
        secondSceneView.session.pause()
    }


    // MARK: - ARSCNViewDelegate

    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        print("\(self.classForCoder)/" + #function)
        guard let planeAnchor = anchor as? ARPlaneAnchor else {fatalError()}
        //sceneとnodeを読み込み
        guard let scene = SCNScene(named: "FinalBaseMesh.scn", inDirectory: "art.scnassets") else {fatalError()}
        //identityちゃんと設定
        guard let bearNode = scene.rootNode.childNode(withName: "FinalBaseMesh", recursively: true) else {fatalError()}
        // nodeのスケールを調整する
        let (min, max) = bearNode.boundingBox
        let w = CGFloat(max.x - min.x)
        // 1mを基準にした縮尺を計算
        let magnification = 1.0 / w
        bearNode.scale = SCNVector3(magnification, magnification, magnification)
        // nodeのポジションを設定
        bearNode.position = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z)

        //作成したノードを追加
        DispatchQueue.main.async(execute: {
            node.addChildNode(bearNode)
        })

    }

    @objc func tapView(sender: UIGestureRecognizer) {
        let tapPoint = sender.location(in: secondSceneView)
        let results = secondSceneView.hitTest(tapPoint, types: .existingPlaneUsingExtent)
        if !results.isEmpty {
            if let result = results.first ,
                let anchor = result.anchor ,
                let node = secondSceneView.node(for: anchor) {

                let action1 = SCNAction.rotateBy(x: CGFloat(-90 * (Float.pi / 180)), y: 0, z: 0, duration: 0.5)
                let action2 = SCNAction.wait(duration: 1)

                DispatchQueue.main.async(execute: {
                    node.runAction(
                        SCNAction.sequence([
                        action1,
                        action2,
                        action1.reversed()
                        ])
                    )
                })

            }
        }
    }

    @objc func dragView(sender: UIGestureRecognizer) {
        let tapPoint = sender.location(in: secondSceneView)

        let results = secondSceneView.hitTest(tapPoint, types: .existingPlane)
        if !results.isEmpty {
            if let result = results.first ,
                let anchor = result.anchor ,
                let node = secondSceneView.node(for: anchor) {

                DispatchQueue.main.async(execute: {
                    // 実世界の座標をSCNVector3で返したものを反映
                    node.position = SCNVector3(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y, result.worldTransform.columns.3.z)
                })
            }
        }
    }

}

分からないこと

・ライフサイクルとrenderの違いはなに
->backgroundcolor一瞬しか反映しない

SCNViewだと怒られないことがARSCNViewだと怒られた
->インスタンス書いてなかったから?

Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: 'Could not instantiate class named ARSCNView because no class named ARSCNView was found; the class needs to be defined in source code or linked in from a library (ensure the class is part of the correct target)'
@IBOutlet var sceneView: ARSCNView!

    override func viewDidLoad() {
        super.viewDidLoad()

        //let scnView = self.view as! SCNView
        sceneView.backgroundColor = UIColor.black
    }

参考

ARKitで簡単ARやってみた - Qiita
ARKitのはじめかた その1「5分で出来るARアプリ」 - Qiita
SceneKitのシーンやノードを理解【3次元世界への入り口】 – techpartner

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