- 投稿日:2019-10-20T23:21:54+09:00
macOSアプリのダークモード対応
はじめに
macOSでは10.14(2018年リリース)からダークモードが追加されています。
macOS 10.14以降、アプリで独自定義のカラーを使う場合、ライトモード用とダークモード用のカラーを用意しておく必要があります。ライト/ダーク用のカラーはカラーアセットを利用して用意できます。
カラーアセットから以下のコードで外観モードにあわせたカラーオブジェクトを生成できます。
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
- 投稿日:2019-10-20T23:16:03+09:00
【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
- 投稿日:2019-10-20T22:33:17+09:00
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の問題を経験した方がいらっしゃれば、ぜひコメントお願いします!
- 投稿日:2019-10-20T21:52:14+09:00
【Swift】アクセスコントロール
アクセスコントロール
アクセスコントロールとは、モジュール内の型や型の要素に対する外部からのアクセスを制限することを指します。
アクセスレベル
アクセスコントロールをどの範囲で制限するかを下記のキーワードで決定できます。
open
モジュール内外の全てに対してアクセス許可を出す。public
基本的にはopenと同じだが、モジュール外で継承したりオーバーライドはできない。internal
同一モジュール内のアクセスに限りアクセス許可を出す。fileprivate
同一のソースファイル内のアクセスのみを許可を出す。private
対象の要素が属しているスコープに限りアクセス許可を出す。デフォルトのアクセスレベル
型全体のデフォルトのアクセルレベル → internal
型全体のアクセスレベルがprivate、fileprivateに指定されている場合の型内部の要素 → 型のアクセスレベルと同一
型全体のアクセスレベルがopen、public、internalに指定されている場合の型内部の要素 → internal
- 投稿日:2019-10-20T20:29:48+09:00
iOS13のモーダルはFullscreenもいいけどCurrentContextも検討してみて
iOS13からモーダルを表示するときはセミモーダルがデフォルトの挙動となりました。
完全なフルスクリーンではなく後ろのViewが少し見えるようなデザインで、下にスワイプで元のViewに戻ることができます。これをされると困るデザインがあります。例えばログイン後に表示されるViewはセミモーダルではなくフルスクリーンで表示したいでしょう。
StoryboardからPresentationを
Fullscreen
にすることで次のモーダルを完全なフルスクリーン状態で遷移できます。
Swiftの場合はこうします。
let vc = ViewController() vc.modalPresentationStyle = .fullScreen present(vc, animated: true)
この状態であればスワイプで戻ることもできません。
ここまでであればあまり問題はなさそうに見えます。さらにModalで遷移したいとき
ここからさらにモーダルを表示したいとします。このときはセミモーダルのほうが利便性があがるので普通の遷移にします。
するとどうでしょう。
なんと後ろのViewが下がらないままセミモーダルが出てしまいました。iOS13においてfullscrrenでモーダル表示したものはずっとfullscreenの状態を維持するのでこのような動きになるようです。
でもこれじゃかっこよくない!下がってほしい!というときはCurrent Context
を使ってみます。先ほどのFullscrrenからCurrent Contextに変更します。するとStoryboard上では灰色はフルスクリーンで、その次のモーダルでは灰色Viewが下にずれているのが確認できます。
(よく見ると上のStoryboardでもFullscreen選択時のモーダルの見え方はシミュレートされていましたね)
ではこれで解決!かと思いきや実際実行してみると・・・
なんと今度はステータスバーが全く見えなくなってしまいました。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-20T20:29:48+09:00
iOS13のモーダルはFullscreenもいいけどCurrentContextも検討したい
iOS13からモーダルを表示するときはセミモーダルがデフォルトの挙動となりました。
完全なフルスクリーンではなく後ろのViewが少し見えるようなデザインで、下にスワイプで前のViewに戻ることができます。これをされると困るデザインがあります。例えばログイン後に表示されるViewはセミモーダルではなくフルスクリーンで表示したいでしょう。
StoryboardからPresentationを
Fullscreen
にすることで次のモーダルを完全なフルスクリーン状態で遷移できます。
Swiftの場合はこうします。
let vc = ViewController() vc.modalPresentationStyle = .fullScreen present(vc, animated: true)
この状態であればスワイプで戻ることもできません。
ここまでであればあまり問題はなさそうに見えます。さらにModalで遷移したいとき
ここからさらにモーダルを表示したいとします。このときはセミモーダルのほうが利便性があがるので普通の遷移にします。
するとどうでしょう。
なんと後ろのViewが下がらないままセミモーダルが出てしまいました。iOS13においてfullscrrenでモーダル表示したものはずっとfullscreenの状態を維持するのでこのような動きになるようです。
でもこれじゃかっこよくない!下がってほしい!というときはCurrent Context
を使ってみます。先ほどのFullscrrenからCurrent Contextに変更します。するとStoryboard上では灰色はフルスクリーンで、その次のモーダルでは灰色Viewが下にずれているのが確認できます。
(よく見ると上のStoryboardでもFullscreen選択時のモーダルの見え方はシミュレートされていましたね)
ではこれで解決!かと思いきや実際実行してみると・・・
なんと今度はステータスバーが全く見えなくなってしまいました。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-20T19:52:08+09:00
【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上での操作はコードを通して全部できるようになってるっぽかったから、ソースコード見て必要な機能があったら実装していこう。
- 投稿日:2019-10-20T17:05:46+09:00
【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-500down x 下にxcm進む
x:20-500left x 左にxcm進む
x:20-500right x 右にxcm進む
x:20-500forward x 前にxcm進む
x:20-500back x 後ろにxcm進む
x:20-500cw x 時計回りにx度回転
x: 1-3600ccw x 反時計回りにx度回転
x: 1-3600flip 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-100curve 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-60Telloのステータス情報は以下のコマンドで確認できます。
コマンド 説明 応答 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サーバを作り受信させる必要があります。参考
- 投稿日:2019-10-20T16:31:14+09:00
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.swift
にvar 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になっているプロパティで警告されてしまいます。
正しい実装方法を調べてみましたが、わかりませんでした。
コメントをいただければと思います。
(あわせてこの記事とサンプルプロジェクトを変更していきたいと思います。)参考サイト
- 投稿日:2019-10-20T15:08:41+09:00
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は攻略したもんだと思う、、、気がする
- 投稿日:2019-10-20T14:25:18+09:00
【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() } }
- 投稿日:2019-10-20T09:34:00+09:00
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 đỡ.
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 ạ.
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年間だけ勉強すれば、募集しても大丈夫でしょうか?
- 投稿日:2019-10-20T07:01:52+09:00
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.plist
のPrivacy - Camera Usage Description
を有効化してください。下記ライブラリを利用するため
Podfile
を編集してください。pod 'Moya/RxSwift' pod 'RxSwift' pod 'SVProgressHUD'
pod install
を実行してください。pod install
APIRequestの実装
初めに
endpoint
やSubscriptionKey
をConst
に定義しておきましょう。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
を作成します。下の画像を参考に作成してください。真ん中の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でリアルタイム顔認識をする方法
- 投稿日:2019-10-20T06:54:12+09:00
iOSでリアルタイム顔検出を行う
こんにちはrenです。
今回はリアルタイム顔検出のアプリを作っていきます。
一週間程度でぱぱっと作ったものなので、間違いなどあればご指摘等いただけると嬉しいです。環境
macOS Catalina 10.15
Xcode 11.1
iOS13
Swift 5流れ
「Login」ボタンをタップ
↓
フロントカメラを起動する
↓
出力された映像を切り出し、画像に変換する
↓
画像から顔を検出する
↓
検出された顔の座標にフレームを表示させる準備
今回はカメラを利用するので
Info.plist
にPrivacy - 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でリアルタイム顔認識をする方法
- 投稿日:2019-10-20T02:52:42+09:00
ARKit個人的まとめ - Day1
仕事でARKitを触ることになったのでまとめ
やったこと
カメラの映像に
・テキストを配置する
・テクスチャを配置する
・それらをタップで動かす
・それらをドラッグで動かす
・平面を検出する使ったもの
・ARKit
・SceneKitコード
テキストを配置する
・backgroundColor出ても最初の一瞬だけ
・SCNViewとARSCNViewは違うらしい->あとで追記
・willAppearでconfig設定してrun & willDisAppearでpauseの流れが鉄板らしい
->平面検出のときはここにさらに設定を加えたりするARText.swiftimport 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.swiftimport 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
- 投稿日:2019-10-20T02:52:42+09:00
ARKit個人的まとめ - Vol.1
仕事でARKitを触ることになったのでまとめ
やったこと
カメラの映像に
・テキストを作成・配置する
・テクスチャを作成・配置する
・タップで動かす
・ドラッグで動かす
・平面を検出する使ったもの
・ARKit
・SceneKitコード
テキストを配置する
・backgroundColor出ても最初の一瞬だけ
・SCNViewとARSCNViewは違うらしい->あとで追記
・willAppearでconfig設定してrun & willDisAppearでpauseの流れが鉄板らしい
->平面検出のときはここにさらに設定を加えたりするARText.swiftimport 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.swiftimport 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
- 投稿日:2019-10-20T02:52:42+09:00
ARKit, SceneKit個人的まとめ - Vol.1
仕事でARKitを触ることになったのでまとめ
・基礎編はこちらやったこと
カメラの映像に
・テキストを作成・配置する
・テクスチャを作成・配置する
・タップで動かす
・ドラッグで動かす
・平面を検出する使ったもの
・ARKit
・SceneKitコード
テキストを配置する
・backgroundColor出ても最初の一瞬だけ
・SCNViewとARSCNViewは違うらしい->あとで追記
・willAppearでconfig設定してrun & willDisAppearでpauseの流れが鉄板らしい
->平面検出のときはviewDidLoad
ではなくrender(_ didAdd)
に書く
->リアルタイムでテクスチャの座標を更新したい場合とかARText.swiftimport 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.swiftimport 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