20201012のSwiftに関する記事は14件です。

[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プロトコルを適用したクラスでなければ代入できません。
スクリーンショット 2020-10-12 17.58.03.png
1でこのクラスにUISearchBarDelegateプロトコルを適用させたのはこの為です。

3 入力値を受け取る

入力後に検索が押されたら処理を実行させたいので、以下のメソッドを使います。
スクリーンショット 2020-10-12 22.29.31.png
メソッド名わかりやすーーー。

    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学習中です。

基本的な構文はこちら(随時更新)にまとめています。

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

UITableViewのスクロール位置を戻したい!

Xcode-12.0 iOS-14.0 Swift-5.3

はじめに

UITableViewreloadData() をしたときにトップへスクロールしたいときがたまにあります。でもこれが結構めんどくさい。。。

色々方法を試したので備忘録として記載します。

やりたいことは「ボタン押下したときにリロード+スクロール位置をトップに戻したい」ということ!

こんな感じ(before が最下部表示で after が before の状態からリロード+トップへスクロールした状態です)

before after
before after

テーブルは前回の記事で書いたヘッダーが消えたり出たりする grouped スタイルのテーブルでやります!
(before はヘッダー非表示、after はヘッダー表示です)

方法

テーブルをトップにスクロールする方法として setContetnOffsetscrollToRow があり、 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 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
l_1 l_2 l_3 l_4

全パターンいい感じにいけてそう:tada:

実行結果3

layoutIfNeeded() を呼ぶことでそれぞれ思った通りの動作になってますがあまいです!セルの高さが固定なら問題ないのですがセルの高さが可変の場合はちょっと。。。:confounded:

下記のような2パターンのセルを用意します。

cell

実行結果2の処理を試してみると結果はこんな感じ(ちなみに layoutIfNeeded ないパターンだとパターン4以外は中途半端なスクロールになりました)

パターン1 パターン2 パターン3 パターン4
b_l_1 b_l_2 b_l_3 b_l_4

パターン3以外はいけてそうです:clap:おそらく tableView(_ :estimatedHeightForRowAt:) で適切な値を返してやるとパターン3でもうまくいくと思います。2パターンとかならいいですがここにオートレイアウトとか関与してくると色々高さ計算がめんどくさくなります。。。

セルの高さに関しては下記の記事に色々丁寧に記載されていたので参考に(私はあんまりわかってない。。。)

UITableViewのrowHeightやestimatedRowHeightに何を設定すると良いのか

結論

今回は計算とかいろいろめんどくさいのとヘッダーまでスクロールしたかったので下記を採用しました。(たぶんいけてそう:v:

// パターン1(setContetnOffset先スクロール)
tableView.setContentOffset(.zero, animated: false)
tableView.layoutIfNeeded()
tableView.reloadData()

動作はこんな感じ

scroll

おわりに

reloadData とトップへスクロールの組み合わせはわりとやることあるんですが結局どういう方法がいいのかな?といつも悩んでしまいます。。。

下記のようなやり方もあるみたいです。

iOS TableView reload and scroll top

他なにかいい方法ご存知であればぜひご教授ください:raised_hands:

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

guard文について理解しよう!

今回は、guard文について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承ください。

guard文とは?

guard文を一言でいうと、条件不成立のときに早期退出を行なうための条件分岐文。
基本的な書き方は以下の通りです。

qiita.rbvar
guard 条件式 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.rbvar
func 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.rbvar
func 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))//5

guard文の例

qiita.rbvar
func 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文では退出処理を書き忘れた場合にエラーになるため、単純ミスを未然に防げます!

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

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の中身から引用

スクリーンショット 2020-10-12 18.00.15.png
https://github.com/apple/swift/blob/9af806e8fd93df3499b1811deae7729176879cb0/stdlib/public/core/OutputStream.swift#L375

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

Swift Package Manager (SwiftPM) を使ってみよう ~作成編~

Xcode11からiOSアプリ開発にも使えるようになり、先日リリースされたXcode12ではかなり使いやすくなったSwiftPackageManager(以下、SwiftPM)について、プロジェクトへの導入方法とSwiftPMへ自作ライブリを作成方法のうち、この記事は自作ライブラリ作成方法になります。

↓導入方法はこちら
* Swift Package Manager (SwiftPM) を使ってみよう ~導入編~

ライブラリ作成方法 

導入環境

  • Xcode12

SwiftPM起動

[ Xcode起動 ] → [ File ] → [ New ] → [ Swift Packages ]

スクリーンショット 2020-10-12 9.35.17.png

[ プロジェクト名 ] → [ Cleate ]

スクリーンショット 2020-10-12 9.36.05.png

作成された画面
今回はあくまで自作ライブラリの登録までなので、

  • Package.swift
  • MyLibrary.swift

しか、触れません。(本当だったら、テストも書いた方がいいです。)

スクリーンショット 2020-10-12 9.36.37.png

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.swift
import 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 ]の中身を変更
スクリーンショット 2020-10-12 16.19.16.png

ビルドを「MyMac」から「Any Any iOS Device」へ変更

スクリーンショット 2020-10-12 16.20.09.png

ビルドが成功すれば完成です。

最後に作成したものをGitコマンドやソースツリーを使い、CommitしてTagを付与し、githubにPushすれば完了です。
↑こちらについては、

を、参照してください。
*Tagは[1.0.0]のようにしないとエラーになります。

全体配布を考えられるなら、ライセンスをつけることをお勧めします。
githubでライセンスをつける方法は

を参照ください。

ちなみに、今回作成したものをgithubに公開しています。
よかったらStarでもつけて使ってみてください。

github

https://github.com/isamiodagiri/ExtendedImageViewWithNuke

↓導入方法はこちらになります。

参考

協力

自作ライブラリをSwiftPMに登録する方法で、外部ライブラリを導入する方法だけが参考だけでは分からず、「アプリ道場サロン」の方々にも協力していただきました。
ありがとうございました。

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

Swift Package Manager (SwiftPM) を使ってみよう ~導入編~

Xcode11からiOSアプリ開発にも使えるようになり、先日リリースされたXcode12ではかなり使いやすくなったSwiftPackageManager(以下、SwiftPM)について、プロジェクトへの導入方法とSwiftPMへ自作ライブリを作成方法のうち、この記事は導入方法になります。

↓作成方法はこちら
* Swift Package Manager (SwiftPM) を使ってみよう ~自作ライブラリ作成編~

SwiftPMとは

ライブラリ導入方法 

導入環境

  • Xcode12

パッケージの追加

[ PROJECT ] → [ Swift Packages ] → [ + ]
スクリーンショット 2020-10-05 11.04.29.png

[ 導入したいライブラリのURLを入力 ] → [ Next ]
※「導入したいライブラリ名 SwiftPM」などで検索すれば、導入したいライブラリのURLはわかると思います。
スクリーンショット 2020-10-05 11.17.58.png

[ バージョン指定(今回は「Version」で行ってます)] → [ Next ]
スクリーンショット 2020-10-05 11.33.26.png
バージョン指定方法は、それぞれ下記のようになっています。

タイプ 内容
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 ]
スクリーンショット 2020-10-07 15.11.28.png

プロジェクトに追加されていることを確認
スクリーンショット 2020-10-07 15.12.17.png

以上が、SwiftPMでのライブラリ導入方法になります。
呼び出し方は、他のライブラリ同様

ViewController.swift
//
//  ViewController.swift
//  SwiftPMTest
//
//  Created by Isami Odagiri on 2020/10/03.
//

import UIKit
import Nuke

で、使用できます。

参考

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

iOS14でタブバーが消えたまま戻らなくなる現象に遭遇した

Overview

画面遷移の度に hidesBottomBarWhenPushed = true すると popToRootViewController() した時にtabBarが戻らなくなりました。

iOS13では発生しなかったため、iOS14のバグと思われます。
Developer Forumsにも上がっていました

雑に再現コード

長いので折りたたみ
FirstViewController.swift
final 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.swift
final 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.swift
final class LastViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Last"
    }
}

extension LastViewController {
    @IBAction func popToRoot(_ sender: Any?) {
        navigationController?.popToRootViewController(animated: true)
    }
}

対応策

↑のコードの場合であればSecondViewControllervc.hidesBottomBarWhenPushed = trueを消してしまえばLastViewControllerからのpopToRootViewControllerでタブバーが戻ってくるようになります。
ただし、複雑な画面遷移だと思わぬ副作用が発生するリスクもあるので、バグ修正されるのを待った方がいいのかも?

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

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"]にすれば完了!

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

【Swift】「Delegateは代理人ってなんやねん」という方向けの図解。

はじめに

Swiftを始めた人なら誰しもが最初に思ったであろう
教材・記事「Delegateは代理人で..」
読者「代理人ってなんやねん」

この記事では図解でDelegateを紹介し、使い方まで説明します。
読み終えた頃には
「なるほどDelegateは代理人だな」
と思っていることでしょう。

Delegateを理解する

スクリーンショット 2020-09-29 21.32.32.png

スクリーンショット 2020-09-29 21.36.38.png

終わりに

いかがでしたでしょうか?
「Delegateは代理人なんだな」と思っていただけたことかと思います。
周りにDelegateの理解に苦しんでいる方がいたら是非、
この記事を紹介してみてください。

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

プライバシーポリシー

個人情報の第三者提供

本アプリにおいて、第三者に個人を特定できる情報を提供することはありません。
個人情報の管理には細心の注意を払い、以下に挙げた通りに扱います。

データ解析

アプリの利便性向上を図るため、個人を特定できない匿名状態でのアクセス解析を行っております。アプリクラッシュ時、クラッシュ箇所を匿名で開発者に送信され、早急なバグの修正に役立たせております。また、デバイスやアプリバージョンの使用率,特定の機能の使用率などを解析を行い、今後のアプリの改善に役立たせております。

お問い合わせ先

ご不明な点がございましたら、以下の連絡先までお問い合わせください。
cychoo.iosapp@gmail.com

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

[Swift] アプリのサポートがiOS11以降であればIntとInt64を使い分ける必要はなかった話

私は今までなんとなく、「値がデカくなる可能性がある整数にはInt64、そうでもない整数はInt」で変数宣言するように気を配ってきましたが、実行環境次第ではそんな考慮は不要、という話です。

本記事の前提環境:
・iOS 11以降
・Swift 5

冒頭のような考慮をしてきた背景としては、
Intの最大値・最小値は、『実行環境が32bitか64bitによって違う』という特性があって、32bitだと範囲が狭いためです。

最大値 最小値
Int 2147483647
9223372036854775807
-2147483648
-9223372036854775808
(実行環境が32bitか64bitによって違う)
Int64 9223372036854775807 -9223372036854775808

引用:意外と知られていないSwift数値型の細かい仕様

なので32bit環境のIntにおいては、例えば扱う数値が金額(円)であれば21億4千7百万ウン円が最大値なので、セレブの場合はアプリがクラッシュする恐れがあります。

ですが、よくよく考えたら私が今担当しているアプリはiOS 11以降のiPhoneでしか使えません。

調べたところ、iOS 11以降では32bitの実行環境はないようです。

参考:iOS端末 画面サイズ・対応OS早見表(iOS7〜12)

ってことは、iOS 11以降のみサポートのアプリであれば、IntとInt64を使い分ける意味はないので、冒頭のような考慮は不要でした。

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

iOSアプリ開発のスキルロードマップ(紹介)

Developer Roadmapsという有名なサイトがありまして、Webフロントエンド、バックエンド、Androidなどのスキルロードマップを図として提案してくれています。

しかし、iOS版はないのですね。。。

ってことで探してみたところ、「Reddit」というアメリカの投稿SNSに割と良さげなモノが投稿されていました。

引用:2018 Roadmap to iOS Development

ix44k24k9ik01.png

数年前に書かれたモノであるためか、
「Objective-Cは必須にしなくても良いのでは…」など若干ツッコミたいところもありますが、、、

それ以外は、

「黄色背景のスキルを学習していくと、iOSエンジニアとして及第点に到達するよ」
という観点で、概ね同意できるかなと思いました。

ところで、最後のApp Storeの項にPick any(何かを選択)の区分で「Lucky Reviewer」という項目があるのは、Appleに対する皮肉ですかね(笑

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

ARKit+SceneKit+Metalで光学迷彩②

前回 「ARKit+SceneKit+Metalで光学迷彩①」の続きで光学迷彩の調子が悪い感じを表現してみた

demo2.gif

ノイズテクスチャの描画方法

①ブロックノイズテクスチャをコンピュートシェーダーで生成
②①をマテリアルとしたキャラクターの描画パスを追加
③前回作成した最終画像の生成処理に②を加える
 ・光学迷彩画像、または、②のブロックノイズ画像のどちらかを描画する
 ・描画のタイミングはランダム

アプリ実行時にXcodeで Capture GPU Frame すると次のようにレンダリングパスを確認できる(Xcode12で確認)。今回の追加したのは手書きの赤線部分。
xcode2.png
パス毎にどんな色・デプスが出力されているのか確認できるので便利。デバッグ実行中にカメラアイコンをタップするとCapture GPU Frame ができる。
xcode1.png

コンピュートシェーダーによるブロックノイズ生成と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);
}

※このシェーダーで次のような画像が出力される。
noise.png

・swift(シェーダー呼び出し部分)

ViewController.swift
private 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]

これは生成したノイズ画像(テクスチャ)をSCNMaterialdiffuse.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.swift
import 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)
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

macにケーブル接続したiPhoneの画面をリアルタイム取得する

macにケーブル接続したiPhoneの画面をリアルタイム取得するミニマム実装を作りました。

これまではQuickTime Playerを起動し「新規ムービー収録」からiPhoneを選択するなどのアプリ外での取り回しが必要だったのですが、これで自作プログラムで実現可能となります。

output.gif

GitHubにアップしています。
https://github.com/satoshi0212/DeviceCameraMonitorSample

この実装含め、仮想カメラ/AR/映像表現などの情報更新はTwitterで投稿しています。
https://twitter.com/shmdevelop

実装ポイント

プロジェクト設定

「Hardware」「Camera」 の選択が必要。

スクリーンショット 2020-10-11 23.41.04.png

plist

plistに Privacy - Camera Usage Description を追加してください。

スクリーンショット 2020-10-12 2.26.56.png

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を modelIDmanufacturer で適宜フィルタすると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
        })
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む