- 投稿日:2020-10-12T23:23:20+09:00
[Swift]UISearchBarの使い方
今回の題
SearchBarに入力された値の取り方をアウトプットします。
delegateの理解なども含めた内容です。間違いや誤った解釈がありましたら、優しくコメントにてお教えいただけると助かります!!?♀️
コード
storyboardにUISearchBarを配置し、ViewControllerと繋げた以下の状態から始めます。
import UIKit class ViewController: UIViewController { @IBOutlet weak var searchField: UISearchBar! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } }1 ViewControllerに
UISearchBarDelegate
プロコトルを適用させるclass ViewController: UIViewController, UISearchBarDelegate {余談ですが、初めてこの部分を見たとき、
あれ、プロトコルを適用させたらそのプロトコルに定義されているメソッドや、プロパティを全部このクラスで定義しないとエラーになるんじゃ……。
と思ったのですが、どうやら
オプショナルメソッド
なるものがあり、そういったメソッドの実装に関してはoptinal(任意)なそうです。2 delegataプロパティに、このクラス自体を代入する
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. searchField.delegate = self // 追記 }こうすることで、SearchBarで起きたイベントをこのクラスで受け取り、処理できるようになります。
また、
UISearchBarクラスのdelegataプロパティには、UISearchBarDelegateプロトコルを適用したクラスでなければ代入できません。
1でこのクラスにUISearchBarDelegateプロトコルを適用させたのはこの為です。3 入力値を受け取る
入力後に
検索
が押されたら処理を実行させたいので、以下のメソッドを使います。
メソッド名わかりやすーーー。func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { // キーボードを閉じる view.endEditing(true) // 入力された値がnilでなければif文のブロック内の処理を実行 if let word = searchBar.text { // デバッグエリアに出力 print(word) } }searchBarSearchButtonClickedメソッドの引数searchBarはUISearchBarクラスのインスタンスで、
textプロパティに入力値を保持しています
。
textプロパティはオプショナル型なので入力値の存在確認(Unwrap)をif let文
で行なってから処理を行うようにしています。これで完成です。
全体図
import UIKit class ViewController: UIViewController, UISearchBarDelegate { @IBOutlet weak var searchField: UISearchBar! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. searchField.delegate = self } func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { view.endEditing(true) if let word = searchBar.text { print(word) } } }一言
楽しみながらswift学習中です。
基本的な構文はこちら(随時更新)にまとめています。
- 投稿日:2020-10-12T19:47:54+09:00
UITableViewのスクロール位置を戻したい!
はじめに
UITableView
でreloadData()
をしたときにトップへスクロールしたいときがたまにあります。でもこれが結構めんどくさい。。。
色々方法を試したので備忘録として記載します。やりたいことは「ボタン押下したときにリロード+スクロール位置をトップに戻したい」ということ!
こんな感じ(before が最下部表示で after が before の状態からリロード+トップへスクロールした状態です)
before after テーブルは前回の記事で書いたヘッダーが消えたり出たりする
grouped
スタイルのテーブルでやります!
(before はヘッダー非表示、after はヘッダー表示です)方法
テーブルをトップにスクロールする方法として
setContetnOffset
とscrollToRow
があり、reloadData
の前にスクロールさせるのか後にスクロールさせるのかで4パターンできると思います。実行結果1
実装はこんな感じ
// パターン1(setContetnOffset先スクロール) tableView.setContentOffset(.zero, animated: false) tableView.reloadData() // パターン2(scrollToRow先スクロール) tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false) tableView.reloadData() // パターン3(setContetnOffset後スクロール) tableView.reloadData() tableView.setContentOffset(.zero, animated: false) // パターン4(scrollToRow後スクロール) tableView.reloadData() tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false)結果はこんな感じ
パターン1 パターン2 パターン3 パターン4 パターン1とパターン4(でも row 設定なのでヘッダーまではスクロールしてない)はいけてそうだけどパターン2、3が中途半端な位置になっている。。。
実行結果2
なんかわからんけどとりあえずコンテンツサイズが確定してない=レイアウトが中途半端な状態だから上のような結果になるんだろうと思い
layoutIfNeeded()
呼べばいいんじゃね?ということで下記のように実装してみた。// パターン1(setContetnOffset先スクロール) tableView.setContentOffset(.zero, animated: false) tableView.layoutIfNeeded() tableView.reloadData() // パターン2(scrollToRow先スクロール) tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false) tableView.layoutIfNeeded() tableView.reloadData() // パターン3(setContetnOffset後スクロール) tableView.reloadData() tableView.layoutIfNeeded() tableView.setContentOffset(.zero, animated: false) // パターン4(scrollToRow後スクロール) tableView.reloadData() tableView.layoutIfNeeded() tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: false)結果はこんな感じ
パターン1 パターン2 パターン3 パターン4 全パターンいい感じにいけてそう
実行結果3
layoutIfNeeded()
を呼ぶことでそれぞれ思った通りの動作になってますがあまいです!セルの高さが固定なら問題ないのですがセルの高さが可変の場合はちょっと。。。下記のような2パターンのセルを用意します。
実行結果2の処理を試してみると結果はこんな感じ(ちなみに
layoutIfNeeded
ないパターンだとパターン4以外は中途半端なスクロールになりました)
パターン1 パターン2 パターン3 パターン4 パターン3以外はいけてそうですおそらく
tableView(_ :estimatedHeightForRowAt:)
で適切な値を返してやるとパターン3でもうまくいくと思います。2パターンとかならいいですがここにオートレイアウトとか関与してくると色々高さ計算がめんどくさくなります。。。セルの高さに関しては下記の記事に色々丁寧に記載されていたので参考に(私はあんまりわかってない。。。)
UITableViewのrowHeightやestimatedRowHeightに何を設定すると良いのか結論
今回は計算とかいろいろめんどくさいのとヘッダーまでスクロールしたかったので下記を採用しました。(たぶんいけてそう)
// パターン1(setContetnOffset先スクロール) tableView.setContentOffset(.zero, animated: false) tableView.layoutIfNeeded() tableView.reloadData()動作はこんな感じ
おわりに
reloadData
とトップへスクロールの組み合わせはわりとやることあるんですが結局どういう方法がいいのかな?といつも悩んでしまいます。。。下記のようなやり方もあるみたいです。
iOS TableView reload and scroll top
他なにかいい方法ご存知であればぜひご教授ください
- 投稿日:2020-10-12T19:33:49+09:00
guard文について理解しよう!
今回は、guard文について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承ください。guard文とは?
guard文を一言でいうと、条件不成立のときに早期退出を行なうための条件分岐文。
基本的な書き方は以下の通りです。qiita.rbvarguard 条件式 else{ 条件式がfalseの場合に実行される文 guard文が記述されているスコープ外に退出する必要がある (つまり、returnを記述する必要がある) }※{}←スコープ
では、基本的な例を見ていきましょう!qiita.rbvarでは、基本的な例を見ていきましょう! func someFunction(){ let value = 99 guard value >= 100 else{ print("100未満の値です")//値が100未満だったので実行される return } someFunction() 実行結果:100未満の値ですguard文で宣言された変数や定数へのアクセス
guard文は、if文と同様にguard-let文が利用できます。
if文を復習したい人は下記のURLをチェック!!
if文とは?("https://qiita.com/syunta061689/items/65d54a58936a5849a67a")if-let文との違いは、guard-let文で宣言された変数や定数はguard-let文以降でも利用可能ということです。
次の例ではguard-let文で宣言した定数intにアクセスしています。
qiita.rbvarfunc someFunction(){ let a: Any = 1 //Any型 guard let int= a as? Int //aをInt型にダウンキャストできますか? else{//そうでないなら以下を実行してください print("aはInt型ではありません") return } print("値はInt型の\(int)です")//intはguard文以降でも使用可能! } someFunction() 実行結果:値はInt型の1です。if文との使い分け
では、今度は具体的な例を用いて、if文との使い分けを深堀りしていきましょう.
次の例では、if文とguard文で、2つのInt型を受け取り、両方の値を持っていればその和を返し、どちらかが値を持っていなければnilを返すという処理をしていきます。if文の例
qiita.rbvarfunc add(_ optionalA: Int?,_ optionalB: Int?)-> Int?{ let a: Int if let unwrappedA = optionalA{ a = unwrappedA }else{ print("第一引数に値が入っていません") return nil } let b: Int if let unwrappedB = optionalB{ b = unwrappedB }else{ print("第引数に値が入っていません") return nil } return a+b } add(optional(3)optional(2))//5guard文の例
qiita.rbvarfunc add(_ optionalA: Int?, _ optionalB: Int?)-> Int?{ guard let a = optionalA else{ print("第1引数に値が入っていません") return nil } guard let b = optionalB else{ print("第2引数に値が入っていません") return nil } return a+b } add(Optional(3),Optional(2))//5このように、条件に応じて早期退出するコードは、 guard文を使用して実装した方がシンプルとなります
また、guard文では退出処理を書き忘れた場合にエラーになるため、単純ミスを未然に防げます!
- 投稿日:2020-10-12T18:02:12+09:00
if let b = a as? Intより if case let b as Int = a の方が速い
Swiftにおいてキャストしてその値を使うときは
as?
でキャスト、
Optional
型にしてそのOptional
型に関する
Optional Binding
によって値を取り出すことが多いと思います。let a: Any = 120 if let b = a as? Int { // b を使う処理 }しかし、これだと一回
Optional
を経由しているため流れが少し複雑です。別の書き方があります。
let a: Any = 120 if case let b as Int = a { // b を使う処理 }これだと、Optionalを通らず安全にキャストできます。
速度面
1万回実行時の速度
as?
case
0.011s 0.001s 約10倍の差がある。
Swift Standart Libraryでも
case
の方が多様されている。Swift.printの中身から引用
https://github.com/apple/swift/blob/9af806e8fd93df3499b1811deae7729176879cb0/stdlib/public/core/OutputStream.swift#L375
- 投稿日:2020-10-12T17:08:43+09:00
Swift Package Manager (SwiftPM) を使ってみよう ~作成編~
Xcode11からiOSアプリ開発にも使えるようになり、先日リリースされたXcode12ではかなり使いやすくなったSwiftPackageManager(以下、SwiftPM)について、プロジェクトへの導入方法とSwiftPMへ自作ライブリを作成方法のうち、この記事は自作ライブラリ作成方法になります。
↓導入方法はこちら
* Swift Package Manager (SwiftPM) を使ってみよう ~導入編~ライブラリ作成方法
導入環境
- Xcode12
SwiftPM起動
[ Xcode起動 ] → [ File ] → [ New ] → [ Swift Packages ]
[ プロジェクト名 ] → [ Cleate ]
作成された画面
今回はあくまで自作ライブラリの登録までなので、
- Package.swift
- MyLibrary.swift
しか、触れません。(本当だったら、テストも書いた方がいいです。)
Package.swiftに、読み込みたい対象のファイルや外部ライブラリのURLを記載します。
今回は、外部ライブラリでNukeを読み込み、UIImageをExtensionして、UIImage側でNukeのLoadを行ったものを作ります。Package.swift// swift-tools-version:5.3 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "MyLibrary", platforms:[.iOS(.v11)], // 使用するプラットフォーム(今回はiOS11以上) products: [ .library( name: "MyLibrary", targets: ["MyLibrary"]), ], dependencies: [ .package(url: "https://github.com/kean/Nuke.git", from: "9.1.2") // 読み込みたい外部ライブラリ ], targets: [ depends on. .target( name: "MyLibrary", dependencies: [ .product(name: "Nuke", package: "Nuke") // 読み込んだライブラリを使用する ]), .testTarget( name: "MyLibraryTests", dependencies: ["MyLibrary"]), ] )これで、ライブラリの使用するまでは完了。
MyLibrary.swiftimport UIKit import Nuke public enum ProcessorsOption { case resize case resizeRound(radius: CGFloat) case resizeCircle } public typealias AspectMode = ImageProcessors.Resize.ContentMode public extension UIImageView { func loadUrl(imageUrl: String?, processorOption: ProcessorsOption = ProcessorsOption.resize, aspectMode: AspectMode = .aspectFill, crop: Bool = false, placeHolder: UIImage? = nil, failureImage: UIImage? = nil, contentMode: UIView.ContentMode? = nil) { guard let url: String = imageUrl, let loadUrl: URL = URL(string: url) else { self.image = failureImage return } let resizeProcessor = ImageProcessors.Resize(size: self.bounds.size, contentMode: aspectMode, crop: crop) let processors: [ImageProcessing] switch processorOption { case .resize: processors = [resizeProcessor] case .resizeRound(let radius): processors = [resizeProcessor, ImageProcessors.RoundedCorners(radius: radius)] case .resizeCircle: processors = [resizeProcessor, ImageProcessors.Circle()] } let request = ImageRequest( url: loadUrl, processors: processors ) var contentModes: ImageLoadingOptions.ContentModes? if let mode = contentMode { contentModes = ImageLoadingOptions.ContentModes.init(success: mode, failure: mode, placeholder: mode) } let loadingOptions = ImageLoadingOptions(placeholder: placeHolder, failureImage: failureImage, contentModes: contentModes) Nuke.loadImage(with: request, options: loadingOptions, into: self) } }ライブラリに内容は割愛させてもらいます。
(Nukeの画像ロードとリサイズを行ってくれるものをExtensionにしてみました。)このままでは、No such module 'UIKit'が出ており、ビルドできません。
[ Tests ] → [ MyLibraryTests ] → [ MyLibraryTests.swift ]の中身を変更
ビルドを「MyMac」から「Any Any iOS Device」へ変更
ビルドが成功すれば完成です。
最後に作成したものをGitコマンドやソースツリーを使い、CommitしてTagを付与し、githubにPushすれば完了です。
↑こちらについては、
- https://tyoshikawa1106.hatenablog.com/entry/2016/05/16/230237
- https://backlog.com/ja/git-tutorial/stepup/17/
を、参照してください。
*Tagは[1.0.0]のようにしないとエラーになります。全体配布を考えられるなら、ライセンスをつけることをお勧めします。
githubでライセンスをつける方法はを参照ください。
ちなみに、今回作成したものをgithubに公開しています。
よかったらStarでもつけて使ってみてください。github
https://github.com/isamiodagiri/ExtendedImageViewWithNuke
↓導入方法はこちらになります。
参考
協力
自作ライブラリをSwiftPMに登録する方法で、外部ライブラリを導入する方法だけが参考だけでは分からず、「アプリ道場サロン」の方々にも協力していただきました。
ありがとうございました。
- 投稿日:2020-10-12T17:07:14+09:00
Swift Package Manager (SwiftPM) を使ってみよう ~導入編~
Xcode11からiOSアプリ開発にも使えるようになり、先日リリースされたXcode12ではかなり使いやすくなったSwiftPackageManager(以下、SwiftPM)について、プロジェクトへの導入方法とSwiftPMへ自作ライブリを作成方法のうち、この記事は導入方法になります。
↓作成方法はこちら
* Swift Package Manager (SwiftPM) を使ってみよう ~自作ライブラリ作成編~SwiftPMとは
- Apple製のパッケージ管理ツール
- 「CocoaPods」や「Carthage」のようなもの
- SwiftPM ドキュメント(英語)
- SwiftPM Github
ライブラリ導入方法
導入環境
- Xcode12
パッケージの追加
[ PROJECT ] → [ Swift Packages ] → [ + ]
[ 導入したいライブラリのURLを入力 ] → [ Next ]
※「導入したいライブラリ名 SwiftPM」などで検索すれば、導入したいライブラリのURLはわかると思います。
[ バージョン指定(今回は「Version」で行ってます)] → [ Next ]
バージョン指定方法は、それぞれ下記のようになっています。
タイプ 内容 Version 特定のバージョンまたはバージョンの範囲を指定する Branch ブランチ名を指定する Commit コミットのIDを指定する また、「Version」では更にオプションが指定できます。
タイプ 内容 例 例の意味 Up to Next Major 指定バージョン以上かつ次のメジャーバージョンより小さい 1.0.0 < 2.0.0 1.0.0以上かつ2.0.0より小さい Up to Next Minor 指定バージョン以上かつ次のマイナーバージョンより小さい 1.0.0 < 1.3.0 1.0.0以上かつ1.3.0より小さい Range 指定バージョン以上かつ指定バージョンより小さい 1.0.0 < 1.5.0 1.0.0以上かつ1.5.0より小さい Exact 特定のバージョンを指定する 1.0.0 1.0.0 [ 「Add to Target」にて追加先を確認 ] → [ Finish ]
以上が、SwiftPMでのライブラリ導入方法になります。
呼び出し方は、他のライブラリ同様ViewController.swift// // ViewController.swift // SwiftPMTest // // Created by Isami Odagiri on 2020/10/03. // import UIKit import Nukeで、使用できます。
参考
- 投稿日:2020-10-12T17:04:12+09:00
iOS14でタブバーが消えたまま戻らなくなる現象に遭遇した
Overview
画面遷移の度に
hidesBottomBarWhenPushed = true
するとpopToRootViewController()
した時にtabBarが戻らなくなりました。iOS13では発生しなかったため、iOS14のバグと思われます。
※ Developer Forumsにも上がっていました雑に再現コード
長いので折りたたみ
FirstViewController.swiftfinal class FirstViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() title = "First" } } extension FirstViewController { @IBAction func next(_ sender: Any?) { guard let vc = storyboard?.instantiateViewController(identifier: "second") else { return } vc.hidesBottomBarWhenPushed = true navigationController?.pushViewController(vc, animated: true) } }SecondViewController.swiftfinal class SecondViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() title = "Second" } } extension SecondViewController { @IBAction func next(_ sender: Any?) { guard let vc = storyboard?.instantiateViewController(identifier: "last") else { return } vc.hidesBottomBarWhenPushed = true navigationController?.pushViewController(vc, animated: true) } }LastViewController.swiftfinal class LastViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() title = "Last" } } extension LastViewController { @IBAction func popToRoot(_ sender: Any?) { navigationController?.popToRootViewController(animated: true) } }対応策
↑のコードの場合であれば
SecondViewController
のvc.hidesBottomBarWhenPushed = true
を消してしまえばLastViewController
からのpopToRootViewController
でタブバーが戻ってくるようになります。
ただし、複雑な画面遷移だと思わぬ副作用が発生するリスクもあるので、バグ修正されるのを待った方がいいのかも?
- 投稿日:2020-10-12T16:08:31+09:00
Moyaを使ってmultipart/form-dataでimageのPOST
今更ながら若干ハマった為、メモ
var task: Task { switch self { case .hoge(let files): //filesは[Data] let fileMFDataArray: [MultipartFormData] = files.enumerated().map({ (index, data) in MultipartFormData(provider: .data(data), name: "file", //こっちがKey fileName: "\(Int(Date().timeIntervalSince1970))\(index)", //UnixTime + index番号 のファイル名を生成 mimeType: "multipart/form-data") }) return .uploadMultipart(fileMFDataArray) } }あとは忘れずにheaderを["Content-Type": "multipart/form-data"]にすれば完了!
- 投稿日:2020-10-12T15:56:25+09:00
【Swift】「Delegateは代理人ってなんやねん」という方向けの図解。
- 投稿日:2020-10-12T13:27:29+09:00
プライバシーポリシー
個人情報の第三者提供
本アプリにおいて、第三者に個人を特定できる情報を提供することはありません。
個人情報の管理には細心の注意を払い、以下に挙げた通りに扱います。データ解析
アプリの利便性向上を図るため、個人を特定できない匿名状態でのアクセス解析を行っております。アプリクラッシュ時、クラッシュ箇所を匿名で開発者に送信され、早急なバグの修正に役立たせております。また、デバイスやアプリバージョンの使用率,特定の機能の使用率などを解析を行い、今後のアプリの改善に役立たせております。
お問い合わせ先
ご不明な点がございましたら、以下の連絡先までお問い合わせください。
cychoo.iosapp@gmail.com
- 投稿日:2020-10-12T12:42:37+09:00
[Swift] アプリのサポートがiOS11以降であればIntとInt64を使い分ける必要はなかった話
私は今までなんとなく、「値がデカくなる可能性がある整数にはInt64、そうでもない整数はInt」で変数宣言するように気を配ってきましたが、実行環境次第ではそんな考慮は不要、という話です。
本記事の前提環境:
・iOS 11以降
・Swift 5冒頭のような考慮をしてきた背景としては、
Intの最大値・最小値は、『実行環境が32bitか64bitによって違う』という特性があって、32bitだと範囲が狭いためです。
型 最大値 最小値 Int 2147483647
9223372036854775807-2147483648
-9223372036854775808
(実行環境が32bitか64bitによって違う)Int64 9223372036854775807 -9223372036854775808 なので32bit環境のIntにおいては、例えば扱う数値が金額(円)であれば21億4千7百万ウン円が最大値なので、セレブの場合はアプリがクラッシュする恐れがあります。
ですが、よくよく考えたら私が今担当しているアプリはiOS 11以降のiPhoneでしか使えません。
調べたところ、iOS 11以降では32bitの実行環境はないようです。
参考:iOS端末 画面サイズ・対応OS早見表(iOS7〜12)
ってことは、iOS 11以降のみサポートのアプリであれば、IntとInt64を使い分ける意味はないので、冒頭のような考慮は不要でした。
- 投稿日:2020-10-12T12:41:55+09:00
iOSアプリ開発のスキルロードマップ(紹介)
Developer Roadmapsという有名なサイトがありまして、Webフロントエンド、バックエンド、Androidなどのスキルロードマップを図として提案してくれています。
しかし、iOS版はないのですね。。。
ってことで探してみたところ、「Reddit」というアメリカの投稿SNSに割と良さげなモノが投稿されていました。
引用:2018 Roadmap to iOS Development
数年前に書かれたモノであるためか、
「Objective-Cは必須にしなくても良いのでは…」など若干ツッコミたいところもありますが、、、それ以外は、
「黄色背景のスキルを学習していくと、iOSエンジニアとして及第点に到達するよ」
という観点で、概ね同意できるかなと思いました。ところで、最後のApp Storeの項にPick any(何かを選択)の区分で「Lucky Reviewer」という項目があるのは、Appleに対する皮肉ですかね(笑
- 投稿日:2020-10-12T09:20:18+09:00
ARKit+SceneKit+Metalで光学迷彩②
前回 「ARKit+SceneKit+Metalで光学迷彩①」の続きで光学迷彩の調子が悪い感じを表現してみた
ノイズテクスチャの描画方法
①ブロックノイズテクスチャをコンピュートシェーダーで生成
②①をマテリアルとしたキャラクターの描画パスを追加
③前回作成した最終画像の生成処理に②を加える
・光学迷彩画像、または、②のブロックノイズ画像のどちらかを描画する
・描画のタイミングはランダムアプリ実行時にXcodeで
Capture GPU Frame
すると次のようにレンダリングパスを確認できる(Xcode12で確認)。今回の追加したのは手書きの赤線部分。
パス毎にどんな色・デプスが出力されているのか確認できるので便利。デバッグ実行中にカメラアイコンをタップするとCapture GPU Frame
ができる。
コンピュートシェーダーによるブロックノイズ生成とSCNNodeへの設定
ノイズテクスチャの生成に必要なのは時間で変わる情報
timeParam
と xy座標。
描画の度にインクリメントされるtimeParam
の値をシェーダーに渡し、シェーダーはその情報とxy座標を元にノイズ色を決める。
ノイズ生成のタイミングはrenderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval)
で動作している。・シェーダー
shader.metal// 乱数生成 float rand(float2 co) { return fract(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453); } // ブロックノイズ画像生成シェーダー kernel void blockNoise(const device float& time [[buffer(0)]], texture2d<float, access::write> out [[texture(0)]], uint2 id [[thread_position_in_grid]]) { // 8pxのブロック float2 uv = float2(id.x / 8, id.y / 8); float noise = fract(rand(rand(float2(float(uv.x * 50 + time), float(uv.y * 50 + time) + time)))); float4 color = float4(0.0, noise, 0.0, 1.0); out.write(color, id); }・swift(シェーダー呼び出し部分)
ViewController.swiftprivate func setupMetal() { (略) // ノイズ作成用コンピュートシェーダー let noiseShader = library.makeFunction(name: "blockNoise")! self.computeState = try! self.device.makeComputePipelineState(function: noiseShader) // シェーダーに渡す時間情報のバッファ self.timeParamBuffer = self.device.makeBuffer(length: MemoryLayout<Float>.size, options: .cpuCacheModeWriteCombined) self.timeParamPointer = UnsafeMutableRawPointer(self.timeParamBuffer.contents()).bindMemory(to: Float.self, capacity: 1) // スレッドグループ・グリッド self.threadgroupSize = MTLSizeMake(16, 16, 1) let threadCountW = (noiseTetureSize + self.threadgroupSize.width - 1) / self.threadgroupSize.width let threadCountH = (noiseTetureSize + self.threadgroupSize.height - 1) / self.threadgroupSize.height self.threadgroupCount = MTLSizeMake(threadCountW, threadCountH, 1) } func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { // 描画毎にインクリメント self.timeParam += 1; self.timeParamPointer.pointee = self.timeParam let commandBuffer = self.commandQueue.makeCommandBuffer()! let computeEncoder = commandBuffer.makeComputeCommandEncoder()! computeEncoder.setComputePipelineState(computeState) computeEncoder.setBuffer(self.timeParamBuffer, offset: 0, index: 0) computeEncoder.setTexture(noiseTexture, index: 0) computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) computeEncoder.endEncoding() commandBuffer.commit() commandBuffer.waitUntilCompleted() }シェーダーの出力は
MTLTexture
で受け取る。
ポイントになるのは、受け取ったテクスチャをキャラクターのマテリアルとしてどうやって渡すか。ViewController.swift// ノイズを書き込むテクスチャ let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm, width: noiseTetureSize, height: noiseTetureSize, mipmapped: false) textureDescriptor.usage = [.shaderWrite, .shaderRead] self.noiseTexture = device.makeTexture(descriptor: textureDescriptor)! // ノイズテクスチャを光学迷彩対象のNodeのマテリアルに設定 let node = self.rootNode.childNode(withName: "CamouflageNode", recursively: true)! let material = SCNMaterial() material.diffuse.contents = self.noiseTexture! material.emission.contents = self.noiseTexture! // 影ができないようにする node.geometry?.materials = [material]これは生成したノイズ画像(テクスチャ)を
SCNMaterial
のdiffuse.contents
に設定して、それをキャラクターノードのジオメトリに設定するだけ。あとは SceneKit がやってくれる。SCNProgramを使う方向であれこれ試していたが、こちらの記事 にやり方が書いてあった。マルチパスレンダリング
前回の記事で出力していた光学迷彩部分を、今回描画する部分(ノイズテクスチャが貼られたキャラクター)で置き換えたりする(ランダムなタイミングによって表示を切り替えてチラつきを表現)。
SCNTechnique
に追加したパスは次の通り。technique.json"pass_noise_node" : { "draw" : "DRAW_NODE", "includeCategoryMask" : 2, "outputs" : { "color" : "noise_color_node" } },ノイズテクスチャでキャラクターを描画するだけなので、これだけ。
"color" : "noise_color_node"
に色情報を出力している。最終画像の生成シェーダーには次のように変更。上記パスで出力された
noiseColorNode
を引数に追加している。// ノイズ発生タイミング生成 bool spike(float time) { float flickering = 0.3; // チラつき具合。大きくするとチラつきやすくなる float piriod = -0.8; // チラつく期間。小さくするとチラつく時間が長くなる if (rand(time * 0.1) > (1.0 - flickering) && sin(time) > piriod) { return true; } else { return false; } } // シーン全体とノード法線の合成用フラグメントシェーダー fragment half4 mix_fragment(MixColorInOut vert [[stage_in]], constant SCNSceneBuffer& scn_frame [[buffer(0)]], // 描画フレームの情報 texture2d<float, access::sample> colorScene [[texture(0)]], depth2d<float, access::sample> depthScene [[texture(1)]], texture2d<float, access::sample> colorNode [[texture(2)]], depth2d<float, access::sample> depthNode [[texture(3)]], texture2d<float, access::sample> noiseColorNode [[texture(4)]]) { float ds = depthScene.sample(s, vert.uv); // シーン全体描画時のデプス float dn = depthNode.sample(s, vert.uv); // ノード描画時のデプス float4 fragment_color; if (dn > ds) { if (spike(scn_frame.time)) { // ノイズのタイミングではノイズテクスチャの色を採用 fragment_color = noiseColorNode.sample(s, fract(vert.uv)); } else { // 光学迷彩対象のオブジェクトがシーンで描画したオブジェクトの手前にあるので、光学迷彩効果を出す (略) }
spike()
でランダムな true/false 情報を作り、それでノイズのあるキャラクターか、光学迷彩のキャラクターか表示色を切り替えている。ソースコード全体
・マルチパスレンダリング定義
technique.json{ "targets" : { "color_scene" : { "type" : "color" }, "depth_scene" : { "type" : "depth" }, "color_node" : { "type" : "color" }, "depth_node" : { "type" : "depth" }, "noise_color_node" : { "type" : "color" } }, "passes" : { "pass_scene" : { "draw" : "DRAW_SCENE", "excludeCategoryMask" : 2, "outputs" : { "color" : "color_scene", "depth" : "depth_scene" }, "colorStates" : { "clear" : true, "clearColor" : "sceneBackground" }, "depthStates" : { "clear" : true, "func" : "less" } }, "pass_node" : { "draw" : "DRAW_NODE", "includeCategoryMask" : 2, "metalVertexShader" : "node_vertex", "metalFragmentShader" : "node_fragment", "outputs" : { "color" : "color_node", "depth" : "depth_node" }, "depthStates" : { "clear" : true, "func" : "less" } }, "pass_noise_node" : { "draw" : "DRAW_NODE", "includeCategoryMask" : 2, "outputs" : { "color" : "noise_color_node" } }, "pass_mix" : { "draw" : "DRAW_QUAD", "inputs" : { "colorScene" : "color_scene", "depthScene" : "depth_scene", "colorNode" : "color_node", "depthNode" : "depth_node", "noiseColorNode" : "noise_color_node" }, "metalVertexShader" : "mix_vertex", "metalFragmentShader" : "mix_fragment", "outputs" : { "color" : "COLOR" }, "colorStates" : { "clear" : "true" } } }, "sequence" : [ "pass_scene", "pass_node", "pass_noise_node", "pass_mix" ] }・シェーダー
#include <metal_stdlib> using namespace metal; #include <SceneKit/scn_metal> // SceneKit -> Shader の受け渡し型 // 定義は https://developer.apple.com/documentation/scenekit/scnprogram 参照 struct VertexInput { float4 position [[attribute(SCNVertexSemanticPosition)]]; // 頂点座標 float2 texCoords [[attribute(SCNVertexSemanticTexcoord0)]]; // テクスチャ座標 float2 normal [[attribute(SCNVertexSemanticNormal)]]; // 法線 }; // SceneKit -> Shader の受け渡し型(ノード毎) // 定義は https://developer.apple.com/documentation/scenekit/scnprogram 参照 struct PerNodeBuffer { float4x4 modelViewProjectionTransform; }; struct NodeColorInOut { float4 position [[position]]; float4 normal; }; struct MixColorInOut { float4 position [[position]]; float2 uv; }; // 乱数生成 float rand(float2 co) { return fract(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453); } // ノイズ発生タイミング生成 bool spike(float time) { float flickering = 0.3; // チラつき具合。大きくするとチラつきやすくなる float piriod = -0.8; // チラつく期間。小さくするとチラつく時間が長くなる if (rand(time * 0.1) > (1.0 - flickering) && sin(time) > piriod) { return true; } else { return false; } } // ノード用頂点シェーダー vertex NodeColorInOut node_vertex(VertexInput in [[stage_in]], constant SCNSceneBuffer& scn_frame [[buffer(0)]], // 描画フレームの情報 constant PerNodeBuffer& scn_node [[buffer(1)]]) // Node毎の情報 { NodeColorInOut out; out.position = scn_node.modelViewProjectionTransform * in.position; out.normal = scn_node.modelViewProjectionTransform * float4(in.normal, 1.0); return out; } // ノード用フラグメントシェーダー fragment half4 node_fragment(NodeColorInOut vert [[stage_in]]) { // 使用する法線はx, yのみ。色情報として扱うので、-1.0 ~ 1.0 -> 0.0 ~ 1.0 に変換しておく float4 color = float4((vert.normal.x + 1.0) * 0.5 , (vert.normal.y + 1.0) * 0.5, 0.0, 0.0); return half4(color); // 法線を色情報として出力。この情報で光学迷彩対象の背景を歪める } // シーン全体とノード法線の合成用頂点シェーダー vertex MixColorInOut mix_vertex(VertexInput in [[stage_in]], constant SCNSceneBuffer& scn_frame [[buffer(0)]]) { MixColorInOut out; out.position = in.position; // 座標系を -1.0 ~ 1.0 -> 0.0 ~ 1.0 に変換。y軸は反転。 out.uv = float2((in.position.x + 1.0) * 0.5 , (in.position.y + 1.0) * -0.5); return out; } constexpr sampler s = sampler(coord::normalized, address::repeat, // clamp_to_edge/clamp_to_border(iOS14)はだめ。 filter::nearest); // シーン全体とノード法線の合成用フラグメントシェーダー fragment half4 mix_fragment(MixColorInOut vert [[stage_in]], constant SCNSceneBuffer& scn_frame [[buffer(0)]], // 描画フレームの情報 texture2d<float, access::sample> colorScene [[texture(0)]], depth2d<float, access::sample> depthScene [[texture(1)]], texture2d<float, access::sample> colorNode [[texture(2)]], depth2d<float, access::sample> depthNode [[texture(3)]], texture2d<float, access::sample> noiseColorNode [[texture(4)]]) { float ds = depthScene.sample(s, vert.uv); // シーン全体描画時のデプス float dn = depthNode.sample(s, vert.uv); // ノード描画時のデプス float4 fragment_color; if (dn > ds) { if (spike(scn_frame.time)) { // ノイズのタイミングではノイズテクスチャの色を採用 fragment_color = noiseColorNode.sample(s, fract(vert.uv)); } else { // 光学迷彩対象のオブジェクトがシーンで描画したオブジェクトの手前にあるので、光学迷彩効果を出す float3 normal_map = colorNode.sample(s, vert.uv).rgb; // 0.0 ~ 1.0 -> -1.0 ~ 1.0 に戻して座標として使えるようにする normal_map.xy = normal_map.xy * 2 - 1.0; // 採用する背景色の位置をノードの法線方向(xy平面)に少しずらして取得することを歪んだ背景にする float2 uv = vert.uv + normal_map.xy * 0.1; if (uv.x > 1.0 || uv.x < 0.0) { // 画面の外の色を採用しないようにする(samplerのaddressingで解決したかったがうまくいかなかった) fragment_color = colorScene.sample(s, fract(vert.uv)); } else { fragment_color = colorScene.sample(s, fract(uv)); } } } else { // 光学迷彩対象のオブジェクトがシーンで描画したオブジェクトの奥にあるので、シーン側の色をそのまま採用 fragment_color = colorScene.sample(s, fract(vert.uv)); } return half4(fragment_color); } // ブロックノイズ画像生成シェーダー kernel void blockNoise(const device float& time [[buffer(0)]], texture2d<float, access::write> out [[texture(0)]], uint2 id [[thread_position_in_grid]]) { // 8pxのブロック float2 uv = float2(id.x / 8, id.y / 8); float noise = fract(rand(rand(float2(float(uv.x * 50 + time), float(uv.y * 50 + time) + time)))); float4 color = float4(0.0, noise, 0.0, 1.0); out.write(color, id); }・swift
ViewController.swiftimport ARKit import SceneKit class ViewController: UIViewController, ARSCNViewDelegate { @IBOutlet weak var scnView: ARSCNView! private var rootNode: SCNNode! private let device = MTLCreateSystemDefaultDevice()! private var commandQueue: MTLCommandQueue! private var computeState: MTLComputePipelineState! = nil private var noiseTexture: MTLTexture! = nil private let noiseTetureSize = 256 private var threadgroupSize: MTLSize! private var threadgroupCount: MTLSize! private var timeParam: Float = 0 private var timeParamBuffer: MTLBuffer! private var timeParamPointer: UnsafeMutablePointer<Float>! override func viewDidLoad() { super.viewDidLoad() // キャラクター読み込み。WWDC2017 SceneKit Demoを借用 https://developer.apple.com/videos/play/wwdc2017/604/ guard let scene = SCNScene(named: "art.scnassets/max.scn"), let rootNode = scene.rootNode.childNode(withName: "root", recursively: true) else { return } self.rootNode = rootNode self.rootNode.isHidden = true // Metal セットアップ self.setupMetal() // Scene Technique セットアップ self.setupSCNTechnique() // AR Session 開始 self.scnView.delegate = self let configuration = ARWorldTrackingConfiguration() configuration.planeDetection = [.horizontal] self.scnView.session.run(configuration, options: [.removeExistingAnchors, .resetTracking]) } private func setupMetal() { self.commandQueue = self.device.makeCommandQueue()! let library = self.device.makeDefaultLibrary()! // ノイズを書き込むテクスチャ let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm, width: noiseTetureSize, height: noiseTetureSize, mipmapped: false) textureDescriptor.usage = [.shaderWrite, .shaderRead] self.noiseTexture = device.makeTexture(descriptor: textureDescriptor)! // ノイズテクスチャを光学迷彩対象のNodeのマテリアルに設定 let node = self.rootNode.childNode(withName: "CamouflageNode", recursively: true)! let material = SCNMaterial() material.diffuse.contents = self.noiseTexture! material.emission.contents = self.noiseTexture! // 影ができないようにする node.geometry?.materials = [material] // ノイズ作成用コンピュートシェーダー let noiseShader = library.makeFunction(name: "blockNoise")! self.computeState = try! self.device.makeComputePipelineState(function: noiseShader) // シェーダーに渡す時間情報のバッファ self.timeParamBuffer = self.device.makeBuffer(length: MemoryLayout<Float>.size, options: .cpuCacheModeWriteCombined) self.timeParamPointer = UnsafeMutableRawPointer(self.timeParamBuffer.contents()).bindMemory(to: Float.self, capacity: 1) // スレッドグループ・グリッド self.threadgroupSize = MTLSizeMake(16, 16, 1) let threadCountW = (noiseTetureSize + self.threadgroupSize.width - 1) / self.threadgroupSize.width let threadCountH = (noiseTetureSize + self.threadgroupSize.height - 1) / self.threadgroupSize.height self.threadgroupCount = MTLSizeMake(threadCountW, threadCountH, 1) } private func setupSCNTechnique() { guard let path = Bundle.main.path(forResource: "technique", ofType: "json") else { return } let url = URL(fileURLWithPath: path) guard let techniqueData = try? Data(contentsOf: url), let dict = try? JSONSerialization.jsonObject(with: techniqueData) as? [String: AnyObject] else { return } // マルチパスレンダリングを有効にする let technique = SCNTechnique(dictionary: dict) scnView.technique = technique } func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { // 描画毎にインクリメント self.timeParam += 1; self.timeParamPointer.pointee = self.timeParam let commandBuffer = self.commandQueue.makeCommandBuffer()! let computeEncoder = commandBuffer.makeComputeCommandEncoder()! computeEncoder.setComputePipelineState(computeState) computeEncoder.setBuffer(self.timeParamBuffer, offset: 0, index: 0) computeEncoder.setTexture(noiseTexture, index: 0) computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) computeEncoder.endEncoding() commandBuffer.commit() commandBuffer.waitUntilCompleted() } func renderer(_: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor, self.rootNode.isHidden else { return } self.rootNode.simdPosition = planeAnchor.center self.rootNode.isHidden = false DispatchQueue.main.async { // 検出した平面上にオブジェクトを表示 node.addChildNode(self.rootNode) } } }
- 投稿日:2020-10-12T03:16:40+09:00
macにケーブル接続したiPhoneの画面をリアルタイム取得する
macにケーブル接続したiPhoneの画面をリアルタイム取得するミニマム実装を作りました。
これまではQuickTime Playerを起動し「新規ムービー収録」からiPhoneを選択するなどのアプリ外での取り回しが必要だったのですが、これで自作プログラムで実現可能となります。
GitHubにアップしています。
https://github.com/satoshi0212/DeviceCameraMonitorSampleこの実装含め、仮想カメラ/AR/映像表現などの情報更新はTwitterで投稿しています。
https://twitter.com/shmdevelop実装ポイント
プロジェクト設定
「Hardware」「Camera」 の選択が必要。
plist
plistに
Privacy - Camera Usage Description
を追加してください。Device探索時設定
AVCaptureDevice.DiscoverySession
実行前に以下を指定することでオプトインで外部デバイスが表示されるようになります。var prop = CMIOObjectPropertyAddress( mSelector: CMIOObjectPropertySelector(kCMIOHardwarePropertyAllowScreenCaptureDevices), mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal), mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementMaster)) var allow: UInt32 = 1; CMIOObjectSetPropertyData(CMIOObjectID(kCMIOObjectSystemObject), &prop, 0, nil, UInt32(MemoryLayout.size(ofValue: allow)), &allow)そして以下のパラメータで探索するとdevicesにiPhoneが含まれています。
見つかったdevicesをmodelID
、manufacturer
で適宜フィルタするとiPhoneデバイスが特定できます。let devices = AVCaptureDevice.DiscoverySession(deviceTypes: [.externalUnknown], mediaType: nil, position: .unspecified).devices if let device = devices.filter({ $0.modelID == "iOS Device" && $0.manufacturer == "Apple Inc." }).first { ... }ただし起動直後や探索直後はiPhoneが見つからない場合があるため
AVCaptureDeviceWasConnectedNotification
のnotificationをobserveする必要もありました。let nc = NotificationCenter.default nc.addObserver(forName: NSNotification.Name(rawValue: "AVCaptureDeviceWasConnectedNotification"), object: nil, queue: .main) { (notification) in print(notification) guard let device = notification.object as? AVCaptureDevice else { return } ... }余談: 表示用リサイズ
アップした実装では画面表示用にリサイズしました。
高さを固定値として比率を計算し幅を算出しimageViewのサイズ指定。
画像の方がCGAffineTransform
でサイズ変換しています。private func resizeIfNeeded(w: CGFloat, h: CGFloat) { guard targetRect == nil else { return } let aspect = h / fixedHeight let rect = CGRect(x: 0, y: 0, width: floor(w / aspect), height: fixedHeight) imageView.frame = rect targetRect = rect } ... func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { connection.videoOrientation = .portrait DispatchQueue.main.async(execute: { let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)! let ciImage = CIImage(cvPixelBuffer: pixelBuffer) let w = CGFloat(CVPixelBufferGetWidth(pixelBuffer)) let h = CGFloat(CVPixelBufferGetHeight(pixelBuffer)) self.resizeIfNeeded(w: w, h: h) guard let targetRect = self.targetRect else { return } let m = CGAffineTransform(scaleX: targetRect.width / w, y: targetRect.height / h) let resizedImage = ciImage.transformed(by: m) let cgimage = self.context.createCGImage(resizedImage, from: targetRect)! let image = NSImage(cgImage: cgimage, size: targetRect.size) self.imageView.image = image }) }