20201108のSwiftに関する記事は12件です。

【Swift】ミュージックライブラリから曲を再生する方法

ミュージックライブラリを使用する方法

ミュージックライブラリを使用する場合、ミュージックライブラリへのアクセスを許可させる必要があります。

info.plist
Privacy - Media Library Usage Description

曲の再生は、MediaPlayerを使用してミュージックライブラリから曲を取得し、
MPMediaItemCollectionに分けることで、再生させるようにできます。

import MediaPlayer

ミュージックライブラリから曲の一覧を取得する方法

ミュージックライブラリからの取得は曲以外に、アルバム、アーティストから一覧を取得することもできます。

// 曲の一覧を取得
let mPMediaQuery = MPMediaQuery.songs()

// アルバムの一覧を取得
let mPMediaQuery = MPMediaQuery.albums()

// アーティストの一覧を取得
let mPMediaQuery = MPMediaQuery.artists()

曲を取得した後は、再生させるため曲ごとに分ける必要があります。

let mPMediaQuery = MPMediaQuery.songs()
if let collections = mPMediaQuery.collections {
     for collection in collections {
          // タイトル
          print(collection.items[0].title!)
     }
}

再生する曲をセット

setQueue(with: MPMediaItemCollection)でミュージックにある曲を指定して再生することができます。

var player = MPMusicPlayerController.applicationMusicPlayer

// 曲をセット
player.setQueue(with: MPMediaItemCollection)

// 曲を再生できるようにする
player.prepareToPlay()

再生 

player.play()

一時停止

player.pause()

停止

player.stop()

リピート

設定値によって、リピート方法を変更することができます。

// ユーザ好みのリピート
player.repeatMode = .default

// リピートしない
player.repeatMode = .none

// 現在の曲をリピート
player.repeatMode = .one

// 現在のプレイリストをリピート
player.repeatMode = .one

シャッフル

設定値によって、シャッフル方法を変更できます。

// シャッフルしない
player.shuffleMode = .off

// ユーザ好みのシャッフル
player.shuffleMode = .default

// 曲ごとにシャッフル
player.shuffleMode = .songs

// アルバムごとにシャッフル
player.shuffleMode = .albums

参考

https://developer.apple.com/documentation/mediaplayer/mpmusicplayercontroller

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

【Swift5】画面遷移の方法

はじめに

学習でのアプリ開発で、1 Storyboard — 1 ViewControllerで設計しているのですが、画面遷移についての方法をいつもネットで検索していると気づきましたので、備忘録として投稿します。

上記の方法で設計している理由としまして、1つのViewControllerにつき1つのStoryBoardを用意する作り方は共同開発におけるコンフリクトが少なくなるとのことです。

動作環境

【Xcode】Version 12.0.1
【Swift】Version 5.3

実装方法 ( navigationController を使う)

// 遷移先のView(NextViewController)を取得
let storyboard = UIStoryboard(name: "Next", bundle: nil)
let nextVC = storyboard.instantiateViewController(withIdentifier: "NextVC") as! NextViewController
// NextViewControllerへ画面遷移
self.navigationController?.pushViewController(nextVC, animated: true)

実装方法 ( navigationController を使わない)

// 遷移先のView(NextViewController)を取得
let storyboard = UIStoryboard(name: "Next", bundle: nil)
let nextVC = storyboard.instantiateViewController(withIdentifier: "NextVC") as! NextViewController
// NextViewControllerへ画面遷移
self.present(nextVC, animated: true, completion: nil)

実装方法 ( storyboard の設定)

  • Storyboardの画面をインスタンス化するので、Storyboard IDをつけておく必要があります。Use Storyboard IDにチェックをつけるのを忘れないように注意してください。

スクリーンショット 2020-11-08 20.16.46(2).png

まとめ

画面遷移の方法はまだまだありますので、随時更新できればなと思います。

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

【Swift5】TabBarControllerを用いたXLPagerTabStripの実装

はじめに

XLPagerTabStripというSwift製のiOSライブラリを使った際に、TabBarControllerと絡めた実装をしました。storyboardでの設定等で少し詰まったりもしましたので、備忘録として投稿します。

動作環境

【Xcode】Version 12.0.1
【Swift】Version 5.3
【CocoaPods】version 1.9.3

実装後の画面

output.gif

実装手順

1. TabBarControllerの用意

  1. Main.storyboardの元々あるViewControllerを削除して、TabBarControllerを追加します。付属のViewControllerも消しておいてください。(storyboard1つに対してViewController1つとする理由です。)
    スクリーンショット 2020-11-08 1.45.48(2).png

  2. Is Initial View Controllerにチェックを入れます。
    スクリーンショット 2020-11-08 1.46.33(2).png

  3. MainTabBarController.swiftを作成し、Main.storyboardClassに割り当ててください。

MainTabBarController.swift
class MainTabBarController: UITabBarController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // 各画面となるViewControllerを格納する変数を用意
        var viewControllers = [UIViewController]()

        // それぞれの画面の設定(tabの画像など)
        let tab1VC = UIStoryboard(name: "Bookmarks", bundle: nil).instantiateInitialViewController()
        tab1VC?.tabBarItem = UITabBarItem(tabBarSystemItem: .bookmarks, tag: 0)
        viewControllers.append(tab1VC!)

        let tab2VC = UIStoryboard(name: "Favorites", bundle: nil).instantiateInitialViewController()
        tab2VC?.tabBarItem = UITabBarItem(tabBarSystemItem: .favorites, tag: 0)
        viewControllers.append(tab2VC!)

        self.setViewControllers(viewControllers, animated: false)
    }
}

2. 各画面となるViewControllerの用意

  • 上記のtab1VCBookmarks.storyboardtab2VCFavorites.storyboardとして用意します。それぞれUINavigationControllerを設定し、見分けがつくようにタイトルを付けます。 スクリーンショット 2020-11-08 2.12.25(2).png

3. XLPagerTabStripを導入

  • CocoaPodsを利用します。
  1. ターミナルを開いてcd 対象ファイルで移動します。
  2. pod initpodfile作成
  3. podfileに下記を追加し保存して、pod installにて完了です。
podfile.rb
pod 'XLPagerTabStrip'

4. 管理元クラスButtonBarPagerTabStripViewControllerの用意

  • ViewControllerたちを管理するButtonBarPagerTabStripViewControllerを継承したクラスを用意します。今回はBookmarksを管理元とします。BookmarksViewController.swiftを作成します。
BookmarksViewController.swift
import UIKit
import XLPagerTabStrip

// 継承元を書き換える ( UIViewController → ButtonBarPagerTabStripViewController )
class BookmarksViewController: ButtonBarPagerTabStripViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
        //管理されるViewControllerを返す処理
        let firstVC = UIStoryboard(name: "First", bundle: nil).instantiateViewController(withIdentifier: "First")
        let secondVC = UIStoryboard(name: "Second", bundle: nil).instantiateViewController(withIdentifier: "Second")
        let thirdVC = UIStoryboard(name: "Third", bundle: nil).instantiateViewController(withIdentifier: "Third")
        let childViewControllers:[UIViewController] = [firstVC, secondVC, thirdVC]
        return childViewControllers
    }

}

1. ボタン部分になるCollection Viewの設置

  • ボタン部分になるCollection Viewを配置し、下記画像のようにAutoLayoutを設定します。Cellは消しておきましょう。
  • Collection ViewのクラスをButtonBarViewと結びつけ、ModuleをXLPagerTabStripとします。
    スクリーンショット 2020-11-08 2.33.16(2).png

  • Collection Viewを右クリックします。Referencing OutletsNew Referencing OutletViewControllerにドラッグし、ButtonBarViewを選択します。
    スクリーンショット 2020-11-08 3.15.15(2).png

2. 切り替え部分になるScrollViewの設置

  • 切り替え部分になるScrollViewを配置し、下記画像のようにAutoLayoutを設定します。
  • あいまいなレイアウトと警告がでるのでContent Layout Guidesのチェックを外しましょう。

スクリーンショット 2020-11-08 2.35.37(2).png

  • ScrollViewを右クリックします。Referencing OutletsNew Referencing OutletViewControllerにドラッグし、containerViewを選択します。
    スクリーンショット 2020-11-08 2.54.55(2).png

  • 正しく繋ぐことができると、ScrollViewの表示がContainerViewに変わります。

5. 管理されるViewControllerたちの用意

  • スワイプやボタンを押されて遷移するViewControllerになります。
  • IndicatorInfoProviderプロトコルを実装したUIViewControllerを下記3つそれぞれ用意します。
  1. First.storyboardFirstViewControllerを用意。( storyboardの背景色は赤色
  2. Second.storyboardSecondViewControllerを用意。( storyboardの背景色は青色
  3. Third.storyboardThirdViewControllerを用意。( storyboardの背景色は緑色
FirstViewController.swift
import UIKit
import XLPagerTabStrip

class FirstViewController: UIViewController {

    //ここがボタンのタイトルに利用されます
    var itemInfo: IndicatorInfo = "First"

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

extension FirstViewController: IndicatorInfoProvider {
    func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
        return itemInfo
    }
}
  • Labelを配置し、TextFirstFont24AutoLayoutの設定をします。
    スクリーンショット 2020-11-08 2.59.11(2).png

  • Classの割り当てと、Storyboard IDを設定します。
    スクリーンショット 2020-11-08 3.13.26(2).png

6. buttonBarの見た目と振る舞いを追加

MainTabBarController.swift
import UIKit
import XLPagerTabStrip

class BookmarksViewController: ButtonBarPagerTabStripViewController {

    override func viewDidLoad() {
        // 画面UIについての処理
        setupUI()
        super.viewDidLoad()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // 強制的に再選択し、changeCurrentIndexProgressiveを動作させる( 0番目 → 1番目 → 0番目 )
        moveToViewController(at: 1, animated: false)
        moveToViewController(at: 0, animated: false)
    }

    func setupUI() {
        // ButtonBar全体の背景色
        settings.style.buttonBarBackgroundColor = UIColor.white
        // ButtonBarItemの背景色
        settings.style.buttonBarItemBackgroundColor = UIColor.white
        // ButtonBarItemの文字色
        settings.style.buttonBarItemTitleColor = UIColor.lightGray
        // ButtonBarItemのフォントサイズ
        settings.style.buttonBarItemFont = .boldSystemFont(ofSize: 14)
        // 選択中のButtonBarインジケーターの色
        settings.style.selectedBarBackgroundColor = UIColor.black
        // 選択中のButtonBarインジケーターの太さ
        settings.style.selectedBarHeight = 2.0
        // ButtonBarの左端の余白
        settings.style.buttonBarLeftContentInset = 8
        // ButtonBarの右端の余白
        settings.style.buttonBarRightContentInset = 8
        // Button内の余白
        settings.style.buttonBarItemLeftRightMargin = 32
        // スワイプやButtonBarItemタップ等でページを切り替えた時の動作
        changeCurrentIndexProgressive = { oldCell, newCell, progressPercentage, changeCurrentIndex, animated in
            // 変更されたか、選択前後のCellをアンラップ
            guard changeCurrentIndex, let oldCell = oldCell, let newCell = newCell else { return }
            // 選択前のセルの状態を指定
            oldCell.label.textColor = UIColor.lightGray
            // 選択後のセルの状態を指定する
            newCell.label.textColor = UIColor.black
        }
    }

    override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
        //管理されるViewControllerを返す処理
        let firstVC = UIStoryboard(name: "First", bundle: nil).instantiateViewController(withIdentifier: "First")
        let secondVC = UIStoryboard(name: "Second", bundle: nil).instantiateViewController(withIdentifier: "Second")
        let thirdVC = UIStoryboard(name: "Third", bundle: nil).instantiateViewController(withIdentifier: "Third")
        let childViewControllers:[UIViewController] = [firstVC, secondVC, thirdVC]
        return childViewControllers
    }

}

参考

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

【Vision・Core ML・iOS・Swift】リアルタイム映像のオブジェクトを識別するPART2

この記事は何か?

リアルタイム映像のオブジェクトを識別するの続きです。ソースコードを読んでいきます。

カメラからリアルタイム映像をキャプチャするコード

実装を省略して、全体を見てみます。
サンプルコードには、下記のViewControllerクラスを継承した「別のビューコントローラ」があります。実際のところは、そちらで物体認識が行われます。

ViewController全体
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {

    var bufferSize: CGSize = .zero
    var rootLayer: CALayer! = nil

    @IBOutlet weak private var previewView: UIView!
    private let session = AVCaptureSession()
    private var previewLayer: AVCaptureVideoPreviewLayer! = nil
    private let videoDataOutput = AVCaptureVideoDataOutput()

    private let videoDataOutputQueue = DispatchQueue(label: "VideoDataOutput", 
                                                       qos: .userInitiated, 
                                                attributes: [], 
                                      autoreleaseFrequency: .workItem)

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupAVCapture()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func setupAVCapture() {...}

    func startCaptureSession() {...}

    // Clean up capture setup
    func teardownAVCapture() {...}

    func captureOutput(_ captureOutput: AVCaptureOutput, didDrop didDropSampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        // print("frame dropped")
    }

    public func exifOrientationFromDeviceOrientation() -> CGImagePropertyOrientation {...}
}

プロパティ

UIとなるビューは、UIView型のpreviewViewだけです。これが、カメラからの映像を表示します。

メンバープロパティ
    var bufferSize: CGSize = .zero
    var rootLayer: CALayer! = nil

    @IBOutlet weak private var previewView: UIView!
    private let session = AVCaptureSession()
    private var previewLayer: AVCaptureVideoPreviewLayer! = nil
    private let videoDataOutput = AVCaptureVideoDataOutput()

    private let videoDataOutputQueue = DispatchQueue(label: "VideoDataOutput", 
                                                       qos: .userInitiated, 
                                                attributes: [], 
                                      autoreleaseFrequency: .workItem)

メソッド

captureOutput(_:didOutput:from:) メソッド

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        // サブクラスで実装
    }

viewDidLoad()メソッドとdidReceiveMemoryWarning()メソッド

ビューコントローラのライフサイクルメソッドです。

    override func viewDidLoad() {
        super.viewDidLoad()
        setupAVCapture()
        // session.startRunnning() // ビデオを表示するだけなら、ここでセッションを開始できるはず
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

setupAVCapture()メソッド

事実上、アプリ起動後に呼ばれる最初のメソッドです。

セッション作成、入力と出力の追加、プレビュー設定
func setupAVCapture() {
    var deviceInput: AVCaptureDeviceInput!  // 入力デバイスは、メソッド全域にわたって参照できるようにする

    // デバイスから広角カメラを取得して、入力デバイスでラップする
    let videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first
    do {
        deviceInput = try AVCaptureDeviceInput(device: videoDevice!)
    } catch {
        print("Could not create video device input: \(error)")
        return
    }

    /* ここからセッションの設定 */
    session.beginConfiguration()
    session.sessionPreset = .vga640x480 // Visonモデルより少しだけ大きめにする

    // セッションに入力デバイスを追加する
    guard session.canAddInput(deviceInput) else {
        print("Could not add video device input to the session")
        session.commitConfiguration()
        return
    }
    session.addInput(deviceInput)

    // セッションにデータ出力を追加する
    if session.canAddOutput(videoDataOutput) {
        session.addOutput(videoDataOutput)
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
        videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
    } else {
        print("Could not add video data output to the session")
        session.commitConfiguration()
        return
    }
    let captureConnection = videoDataOutput.connection(with: .video)
    // 常にフレームを処理する
    captureConnection?.isEnabled = true
    do {
        /* ここから、設定のためデバイスをロックする */
        try videoDevice!.lockForConfiguration()
        let dimensions = CMVideoFormatDescriptionGetDimensions((videoDevice?.activeFormat.formatDescription)!)
        bufferSize.width = CGFloat(dimensions.width)
        bufferSize.height = CGFloat(dimensions.height)
        videoDevice!.unlockForConfiguration()
        /* デバイスのロックを解除する */
    } catch {
        print(error)
    }
    session.commitConfiguration()
    /* セッションの設定はここまで */

    // セッションのプレビューレイヤー
    previewLayer = AVCaptureVideoPreviewLayer(session: session)         // セッションのプレビュー
    previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill    // プレビューレイヤーの外寸
    rootLayer = previewView.layer           // UI上のプレビューを基底レイヤーとする
    previewLayer.frame = rootLayer.bounds   // セッションプレビューの外寸をUIに合わせる
    rootLayer.addSublayer(previewLayer)     // セッションプレビューを基底レイヤーに追加する
}

セッションの設定内容

セッションの設定はbeginConfiguration()メソッドで開始して、commitConfiguration()メソッドで完了します。

setup()メソッドより抜粋
    /* ここからセッションの設定 */
    session.beginConfiguration()
    session.sessionPreset = .vga640x480 // Visionモデルより、少しだけ大きめに

    // セッションに入力デバイスを追加する
    guard session.canAddInput(deviceInput) else {
        // 追加できなければ、メソッド終了
        print("Could not add video device input to the session")
        session.commitConfiguration()
        return
    }
    session.addInput(deviceInput)

    // セッションにデータ出力を追加する
    if session.canAddOutput(videoDataOutput) {
        session.addOutput(videoDataOutput)
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
        videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
    } else {
        // 追加できなければ、メソッド終了
        print("Could not add video data output to the session")
        session.commitConfiguration()
        return
    }
    let captureConnection = videoDataOutput.connection(with: .video)
    // 常にフレームを処理する
    captureConnection?.isEnabled = true
    do {
        /* ここから、設定のためデバイスをロックする */
        try videoDevice!.lockForConfiguration()
        let dimensions = CMVideoFormatDescriptionGetDimensions((videoDevice?.activeFormat.formatDescription)!)
        bufferSize.width = CGFloat(dimensions.width)
        bufferSize.height = CGFloat(dimensions.height)
        videoDevice!.unlockForConfiguration()
        /* デバイスのロックを解除する */
    } catch {
        // ロックできなければエラー
        print(error)
    }
    session.commitConfiguration()
    /* セッションの設定はここまで */

startCaptureSession()メソッド

セッションを開始します。

    func startCaptureSession() {
        session.startRunning()
    }

teardownAVCapture()メソッド

AVCaptureのプレビューレイヤーをまっさらな状態にします。

キャプチャ設定をクリーンアップする
    func teardownAVCapture() {
        previewLayer.removeFromSuperlayer()
        previewLayer = nil
    }

captureOutput(_:didDrop:from:) {メソッド

    func captureOutput(_ captureOutput: AVCaptureOutput, didDrop didDropSampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        // print("frame dropped")
    }

exifOrientationFromDeviceOrientation() -> CGImagePropertyOrientationメソッド

デバイスの向きに基づいて、適切な画像方向を取得する
    public func exifOrientationFromDeviceOrientation() -> CGImagePropertyOrientation {
        let curDeviceOrientation = UIDevice.current.orientation
        let exifOrientation: CGImagePropertyOrientation

        switch curDeviceOrientation {
        case UIDeviceOrientation.portraitUpsideDown:
            exifOrientation = .left
        case UIDeviceOrientation.landscapeLeft:
            exifOrientation = .upMirrored
        case UIDeviceOrientation.landscapeRight:
            exifOrientation = .down
        case UIDeviceOrientation.portrait:
            exifOrientation = .up
        default:
            exifOrientation = .up
        }
        return exifOrientation
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vision・Core ML・iOS・Swift】リアルタイム映像のオブジェクトを識別する

この記事は何か?

Apple Developerのサンプルコード「Recognizing Objects in Live Capture」を独自に解説するものです。

環境

macOS 10.15.7
Xcode 12.1
Swift5.3
iOS 14.2

概要

Visionフレームワークを使用すると、ライブキャプチャしたオブジェクトを識別できます。
Core MLモデルを使ったVisionのリクエストは、キャプチャしたシーンで見つかったオブジェクトを識別した結果をVNRecognizedObjectObservationオブジェクトとして返します。

このサンプルアプリでは、以下の方法を紹介しています。
- カメラをライブキャプチャ用に設定する
- Core MLモデルをVisionに組み込む
- 結果を解析して、オブジェクトを分類する

ライブキャプチャを設定する

AVライブキャプチャの実装は、他のキャプチャアプリと似ていますが、Visionアルゴリズムで最適に動作するようにカメラを設定するには、いくつかの微妙な違いがあります。

キャプチャに使用するカメラを設定する

AVFoundationフレームワークが出力するカメラ映像をメインビューコントローラに送信します。
まずは、AVCaptureSessionを設定するために、セッションのインスタンスを作成します。

private let session = AVCaptureSession()

重要なのは、アプリに最適な解像度を選択することです。
特に要件がない場合でも、最高解像度を安易に選択しないでください。
解像度が低ければ、Visionはより効率的に結果を処理できます。
Xcodeのモデルパラメータを見て、「アプリが640x480ピクセル以下の解像度を必要とするか」を確認します。

以下のコードは、カメラの解像度を「モデルで使用する画像の解像度以上で、最も近い解像度」に設定します。

デバイスとセッションの解像度を設定する
//「背面にある広角のビデオカメラ」のインスタンスを作成
let videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first
do {
    deviceInput = try AVCaptureDeviceInput(device: videoDevice!) // カメラからの入力
} catch {
    print("Could not create video device input: \(error)")
    return
}

session.beginConfiguration()        // セッションの設定を開始する
session.sessionPreset = .vga640x480 // モデル画像はより小さくする

スケーリング手続きは、Visionが行います。

以下のコードは、カメラをデバイスとして追加して、セッションにビデオ入力を追加します。

セッションにカメラデバイスからの入力を追加する
guard session.canAddInput(deviceInput) else {
    print("Could not add video device input to the session")
    session.commitConfiguration()
    return
}
session.addInput(deviceInput)

以下のコードは、セッションにビデオ出力を追加し、ピクセルフォーマットを指定します。

if session.canAddOutput(videoDataOutput) {
    // ビデオ出力をセッションに追加できる場合
    session.addOutput(videoDataOutput)    // ビデオからの出力をセッションに追加する
    videoDataOutput.alwaysDiscardsLateVideoFrames = true
    videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
    videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
} else {
    // ビデオ出力をセッションに追加できない場合は、セッション設定を終わる
    print("Could not add video data output to the session")
    session.commitConfiguration()
    return
}

すべてのフレームを処理するが、一度に複数のVisionリクエストを保持しない
バッファキューが使用可能なメモリを超えると、カメラは動作を停止します。
バッファ管理を簡単にするために、キャプチャ出力では、前のリクエストが必要とする限り、Visionは呼び出しをブロックします。
その結果、AVFoundationは必要に応じてフレームをドロップすることがあります。
サンプルアプリでは、キューサイズを1にしています。
別のリクエストが利用可能になったときに、Visionリクエストが処理のためにすでにキューに入っている場合、余分なものを保持する代わりにそれをスキップします。

let captureConnection = videoDataOutput.connection(with: .video) // 全フレームを処理する
captureConnection?.isEnabled = true
do {
    try  videoDevice!.lockForConfiguration()
    let dimensions = CMVideoFormatDescriptionGetDimensions((videoDevice?.activeFormat.formatDescription)!)
    bufferSize.width = CGFloat(dimensions.width)
    bufferSize.height = CGFloat(dimensions.height)
    videoDevice!.unlockForConfiguration()
} catch {
    print(error)
}

以下のコードは、セッション設定を決定します。

セッション設定を決定する
session.commitConfiguration()

以下のコードは、ビューコントローラにプレビューレイヤーを設定して、カメラがそのフレームをアプリのUIに送信できるようにします。

previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
rootLayer = previewView.layer
previewLayer.frame = rootLayer.bounds
rootLayer.addSublayer(previewLayer)

デバイスの向きを指定する

カメラの向きは、デバイスの向きを使用して適切に入力する必要があります。
Visionアルゴリズムは方向に依存しないので、リクエストを行う際には、キャプチャデバイスの方向と相対的な方向を使用してください。

デバイスの向きに基づいて、画像方向を取得する
let curDeviceOrientation = UIDevice.current.orientation // 実際のデバイスの向き
let exifOrientation: CGImagePropertyOrientation         // 画像方向のメタ情報

switch curDeviceOrientation {
case UIDeviceOrientation.portraitUpsideDown:  // デバイスが縦向き(ホームボタンが上)
    exifOrientation = .left
case UIDeviceOrientation.landscapeLeft:       // デバイスが水平(ホームボタンが右)
    exifOrientation = .upMirrored
case UIDeviceOrientation.landscapeRight:      // デバイスが水平(ホームボタンが左
    exifOrientation = .down
case UIDeviceOrientation.portrait:            // デバイスが縦向き(ホームボタンが下)
    exifOrientation = .up
default:
    exifOrientation = .up
}

Core ML分類器を使って、ラベルを決定する

アプリに含めるCore MLモデルは、Visionのオブジェクト分類器に使用されるラベルを決定します。
このサンプルアプリのモデルは、Darknet YOLO(You Only Look Once)を使用してTuri Create 4.3.2でトレーニングされています。
Turi Createを使用して独自のモデルを生成する方法については、オブジェクト検出を参照してください。
Visionはこれらのモデルを解析し、オブザベーションをVNRecognizedObjectObservationオブジェクトとして返します。

以下のコードは、VNCoreMLModelを使用してモデルをロードします。

モデルを作成する
let visionModel = try VNCoreMLModel(for: MLModel(contentsOf: modelURL))

以下のコードは、導入したモデルからVNCoreMLRequestを作成します。

リクエストを作成する
let objectRecognition = VNCoreMLRequest(model: visionModel, completionHandler: { (request, error) in
    DispatchQueue.main.async(execute: {
        // UIの更新はメインキューで実行する
        if let results = request.results {
            self.drawVisionRequestResults(results)
        }
    })
})

完了ハンドラ自体はバックグラウンドキュー上で実行される可能性があります。
そのため、UIの更新をメインキュー上で実行して、即座に視覚的なフィードバックを提供します。
リクエストの完了ハンドラでは、requestsプロパティを介して結果にアクセスします。

オブザベーションを分析する

結果プロパティはオブザベーションの配列です。
配列の要素は、ラベルとバウンディング・ボックスのセットです。

以下のコードは、配列を反復して、これらのオブザベーションを解析します。

for observation in results where observation is VNRecognizedObjectObservation {
    guard let objectObservation = observation as? VNRecognizedObjectObservation else {
        continue
    }
    // 最も信頼度が高いラベルのみを選択する
    let topLabelObservation = objectObservation.labels[0]
    let objectBounds = VNImageRectForNormalizedRect(objectObservation.boundingBox, Int(bufferSize.width), Int(bufferSize.height))

    let shapeLayer = self.createRoundedRectLayerWithBounds(objectBounds)

    let textLayer = self.createTextSubLayerInBounds(objectBounds,
                                                    identifier: topLabelObservation.identifier,
                                                    confidence: topLabelObservation.confidence)
    shapeLayer.addSublayer(textLayer)
    detectionOverlay.addSublayer(shapeLayer)
}

labels配列は、信頼度の高いものから低いものへと順に、各分類識別子とその信頼度の値をリストアップします。
サンプルアプリでは、要素0にある最も信頼度の高い分類のみをメモし、この分類と信頼度をテキストのオーバーレイで表示します。

バウンディングボックスには、オブジェクトが観測された座標情報が含まれています。
サンプルでは、この座標情報を使用してオブジェクトの周囲にバウンディングボックスを描画します。

このサンプルは、上位の分類のみを返すことで分類を単純化し、配列は信頼度スコアの小さい順に並べられます。
信頼度スコアを分析して複数の分類を表示すれば、検出されたオブジェクトをより詳細に説明したり、競合する分類を表示したりすることもできます。

また、オブジェクトを識別した結果として得られるVNRecognizedObjectObservationを使用して、VNTrackObjectRequestのようなオブジェクトトラッカーを初期化することもできます。
オブジェクト・トラッキングの詳細については、Tracking Multiple Objects or Rectangles in Videoを参照してください。

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

UINibでDI

要約

UINib.OptionsKey.externalObjects を使ってDIします。

やり方

  • External Object用のクラスを用意する
  • External Object付きのXibファイルを作成する
  • instantiate(withOwner:options:)時にexternalObjectsキーでExternal Objectにオブジェクトを割り当てる

External Object用のクラスを用意する

今回は計算をさせたいので計算用オブジェクトを用意します。
これらのクラスは後ほど使います。

class CalcObject: NSObject {
    func calc(num1: Int, num2: Int) -> Int {
        fatalError()
    }

    var operaterText: String {
        fatalError()
    }
}

class AdditionObject: CalcObject {
    override func calc(num1: Int, num2: Int) -> Int {
        num1 + num2
    }

    override var operaterText: String {
        "+"
    }
}

class MultiplicationObject: CalcObject {
    override func calc(num1: Int, num2: Int) -> Int {
        num1 * num2
    }

    override var operaterText: String {
        "×"
    }
}

External Object付きのXibファイルを作成する

まず、xibファイルを作成し、Interface Builderを開き、Object Libraryより External Object を追加します。
スクリーンショット 2020-11-08 14.38.59.png

追加したらExternal Objectのインスペクタより設定を行います。
- Identifier → 今回はCalcObjectにします
- Class → 先ほど作成したクラスCalcObjectにします

スクリーンショット 2020-11-08 15.12.26.png

ついでにFile's Ownerも実装しておきます。
File's Ownerは、先ほどのExternal Objectと計算結果を表示するラベル、ボタンのアクションが認識できるよう、IBOutletやIBActionを設定しておきます。

class NibOwner: NSObject {
    @IBOutlet var calcObject: CalcObject!
    @IBOutlet weak var label: UILabel!
    @IBInspectable var num1: Int = 0
    @IBInspectable var num2: Int = 0

    @IBAction func calc() {
        label.text = "\(num1) \(calcObject.operaterText) \(num2) = \(calcObject.calc(num1: num1, num2: num2))"
    }
}

instantiate(withOwner:options:)時にexternalObjectsキーでExternal Objectにオブジェクトを割り当てる

先ほど作成したxibファイルをコードから使います。
xibを使うときに、instantiate(withOwner:options:)でインスタンス化しますが、その時のoptionsに. externalObjectsを付与します。
External ObjectのIdentifierをキーにして、実際にExternal Objectとして割り当てたいオブジェクトをvalueとします。
CalcObject() の部分をAdditionObjectMultiplicationObjectに差し替えると動作を変えることができます。
以下は画面にxibの内容を反映させるコードの例です。

class ViewController: UIViewController {
    @IBOutlet weak var stackView: UIStackView!
    let nibOwner = NibOwner()

    override func viewDidLoad() {
        super.viewDidLoad()
        let nib = UINib(nibName: "View", bundle: nil)
        let externalObjects = ["CalcObject" : CalcObject()] // ここのオブジェクトを差し替える
        let instance = nib.instantiate(withOwner: nibOwner,
                                       options: [.externalObjects : externalObjects])
        instance.compactMap { $0 as? UIView }.forEach {
            self.stackView.addArrangedSubview($0)
        }
    }
}

実際の動作

AdditionObjectの場合

Simulator Screen Shot - iPod touch (7th generation) - 2020-11-08 at 15.28.25.png

MultiplicationObjectの場合

Simulator Screen Shot - iPod touch (7th generation) - 2020-11-08 at 15.28.45.png

まとめ

つかおう!DI!

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

【Swift5】ARKit を始める 〜モーションキャプチャ〜

どうも、こんにちは。
ありきたりな、佐藤です。
そう、僕です。
最近急にARに興味を持ち出しました。
LIDARが触りたくて、でもLIDARが載ってる端末を所持していなくて。。
まずは、ARKitで、できることを学んでからだな!と思い、まずは勉強開始〜!
なぜ、モーションキャプチャからか、、、
動きある方が入り易いかな?って思うのでやってみる!


開発環境

  • XCode12.1 (12A7403)
  • Model: iPhone 11 Pro
  • iOS 14.1 (18A8395)

ARKit

Appleの公式に説明があるので、こちらを確認してみてください^^
Apple ARKit4

ARKit 4は、まったく新しいDepth APIを導入し、iPhone 12 Pro、iPhone 12 Pro Max、およびiPadProでLiDARスキャナーによって収集された詳細な深度情報にアクセスするための新しい方法を作成します。 Location Anchorsは、Apple Mapsの高解像度データを活用して、iPhoneおよびiPadアプリの世界の特定の場所にARエクスペリエンスを配置します。 また、顔追跡のサポートは、Apple Neural Engineと前面カメラを備えたすべてのデバイスに拡張されているため、さらに多くのユーザーが写真やビデオでARの楽しさを体験できます。


プロジェクトを作ろう!


新規作成

まずは、いつもの通り、Xcodeで
「Welcome to Xcode」の
「Create a new Xcode project」を選択
スクリーンショット 2020-11-08 10.43.41.png


テンプレート選択

「Augmented Reality App」を選択
スクリーンショット 2020-11-08 10.43.59.png


アプリケーション名を付ける

「MotionCapture」
今回はサンプルのモーションキャプチャのアプリなでこんな感じで作成して、「Next」を押し、
プロジェクトの保存先などは、任意の場所に。

*下記の3項目はデフォルトでこうなってると思います
- Content Technology:「SceneKit」
- Interface:「Storyboard」
- Language:「Swift」

スクリーンショット 2020-11-08 10.46.17.png


プロジェクトテンプレート

Xcodeが起動され、テンプレートのアプリが作成されましたね。
iPhoneをMacと接続し、「Run」してみましょう。
カメラへのアクセス許可を「OK」を選択し、iPhoneを動かし、ジェット機を探してみましょー!
発見したら、角度を変えるといろんな角度からジェット機が確認できますね。

長々と、説明をしましたね、、
念の為、必要な人がいるかもしれないので書いてみましたw


モーションキャプチャに突入

ARSCNViewDelegateもすでに入っている状態です。
ARSCVViewDelegateはARセッションにARAnchorが追加、更新、削除などの
イベントをハンドリングする為に用意されているものですが、
今回は動きをキャプチャしたいので、ARSessionDelegateを使用するので、
「ARSCNViewDelegate」を「ARSessionDelegate」に変更する。

ViewController.swift
class ViewController: UIViewController, ARSCNViewDelegate {

class ViewController: UIViewController, ARSessionDelegate {

テンプレートにすでにsceneViewが定義されていますね。
StroryBoardにももちろん用意されています。

ViewController.swift
    @IBOutlet var sceneView: ARSCNView!

sceneViewのdelegateを削除し、
sceneViewのsessitonのdelegateに、selfにセットし、ハンドリングできる様にする

ViewController.swift
    sceneView.delegate = self
    
    sceneView.session.delegate = self

sceneViewのsceneに空のSCNSSceneをセットする

ViewController.swift
    sceneView.scene = SCNScene()

こちらは今回不要なので削除でOK

ViewController.swift
        // Show statistics such as fps and timing information
        // デフォルトはfalseだが、ジェット機に出した時の画面の下部に出ていた情報の表示のon/offフラグ
        sceneView.showsStatistics = true

        // Create a new scene
        // ジェット機の3Dオブジェクトを読み込む
        let scene = SCNScene(named: "art.scnassets/ship.scn")!

        // Set the scene to the view
        // sceneViewにジェット機を表示させる
        sceneView.scene = scene

人の動きを追跡する設定に変更する

viewWillAppear内で、元々ある
「ARWorldTrackingConfiguration」を「ARBodyTrackingConfiguration」に変更して、
sceneViewにセットする。

*ARBodyTrackingConfigurationが使用できない端末の場合は、動作しない為isSupportedで判定し処理する

ARWorldTrackingConfiguration
ARBodyTrackingConfiguration

ViewController.swift
        guard ARBodyTrackingConfiguration.isSupported else {
            fatalError("This feature is only supported on devices with an A12 chip")
        }
        // Create a session configuration
        // let configuration = ARWorldTrackingConfiguration()
        // ↓
        let configuration = ARWorldTrackingConfiguration()

        // Run the view's session
        sceneView.session.run(configuration)

ARSessionDelegateのdelegateメソッドを実装する

今回使用するのはこの2つ
・ARAnchorが追加されたときに呼び出される、session(ARSession, didAdd: [ARAnchor])
・ARAnchorが更新されたときに呼び出される、session(ARSession, didUpdate: [ARAnchor])

//developer.apple.com/documentation/arkit/arsessiondelegate
func session(ARSession, didAdd: [ARAnchor])

func session(ARSession, didUpdate: [ARAnchor])

func session(ARSession, didAdd: [ARAnchor])を実装していく

anchorsのforループさせ、ARAnchorがARBodyAnchorか確認し、ARBodyAnchor以外は今回は処理しない為
returnする。

ViewController.swift
    func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
        for anchor in anchors {
            guard let bodyAnchor = anchor as? ARBodyAnchor else {
                return
            }
            // Next
        }
    }

func session(ARSession, didUpdate: [ARAnchor])も同じく、実装していく

didAddと同様にARAnchorを確認し対象だけ処理する様に実装する。

ViewController.swift
    func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
        for anchor in anchors {
            guard let bAnchor = anchor as? ARBodyAnchor else {
                return
            }
            // Next
        }
    }

ARBodyAnchorから情報を取り出し描画する

両方のdelegateメソッドから取得されたARBodyAnchorを使用し、画面に表示させよう。
ARBodyAnchorからskeletonを取り出す。
skeletonは3Dの身体情報です。

ViewController.swift
        let skeleton = anchor.skeleton

skeletonの、ARSkeletonDefinitionのjointNamesをforループでjointNameを取得する。
ARBodyAnchorの座標は「hips_joint」の位置が中心となっている。取得した、jointNameを使用し、modelTransformで、中心からの移動量と回転量を取得します。
modelTransformは、存在しないjointNameをセットするとnilが取得される。

ViewController.swift
        for jointName in skeleton.definition.jointNames {
            let jointType = ARSkeleton.JointName(rawValue: jointName)
            if let transform = skeleton.modelTransform(for: jointType) {

備考:jointName

jointNameをデバッガで取得してみた。91パーツ。もっとあるのかな、、?
- 0 : "root"
- 1 : "hips_joint"
- 2 : "left_upLeg_joint"
- 3 : "left_leg_joint"
- 4 : "left_foot_joint"
- 5 : "left_toes_joint"
- 6 : "left_toesEnd_joint"
- 7 : "right_upLeg_joint"
- 8 : "right_leg_joint"
- 9 : "right_foot_joint"
- 10 : "right_toes_joint"
- 11 : "right_toesEnd_joint"
- 12 : "spine_1_joint"
- 13 : "spine_2_joint"
- 14 : "spine_3_joint"
- 15 : "spine_4_joint"
- 16 : "spine_5_joint"
- 17 : "spine_6_joint"
- 18 : "spine_7_joint"
- 19 : "left_shoulder_1_joint"
- 20 : "left_arm_joint"
- 21 : "left_forearm_joint"
- 22 : "left_hand_joint"
- 23 : "left_handIndexStart_joint"
- 24 : "left_handIndex_1_joint"
- 25 : "left_handIndex_2_joint"
- 26 : "left_handIndex_3_joint"
- 27 : "left_handIndexEnd_joint"
- 28 : "left_handMidStart_joint"
- 29 : "left_handMid_1_joint"
- 30 : "left_handMid_2_joint"
- 31 : "left_handMid_3_joint"
- 32 : "left_handMidEnd_joint"
- 33 : "left_handPinkyStart_joint"
- 34 : "left_handPinky_1_joint"
- 35 : "left_handPinky_2_joint"
- 36 : "left_handPinky_3_joint"
- 37 : "left_handPinkyEnd_joint"
- 38 : "left_handRingStart_joint"
- 39 : "left_handRing_1_joint"
- 40 : "left_handRing_2_joint"
- 41 : "left_handRing_3_joint"
- 42 : "left_handRingEnd_joint"
- 43 : "left_handThumbStart_joint"
- 44 : "left_handThumb_1_joint"
- 45 : "left_handThumb_2_joint"
- 46 : "left_handThumbEnd_joint"
- 47 : "neck_1_joint"
- 48 : "neck_2_joint"
- 49 : "neck_3_joint"
- 50 : "neck_4_joint"
- 51 : "head_joint"
- 52 : "jaw_joint"
- 53 : "chin_joint"
- 54 : "left_eye_joint"
- 55 : "left_eyeLowerLid_joint"
- 56 : "left_eyeUpperLid_joint"
- 57 : "left_eyeball_joint"
- 58 : "nose_joint"
- 59 : "right_eye_joint"
- 60 : "right_eyeLowerLid_joint"
- 61 : "right_eyeUpperLid_joint"
- 62 : "right_eyeball_joint"
- 63 : "right_shoulder_1_joint"
- 64 : "right_arm_joint"
- 65 : "right_forearm_joint"
- 66 : "right_hand_joint"
- 67 : "right_handIndexStart_joint"
- 68 : "right_handIndex_1_joint"
- 69 : "right_handIndex_2_joint"
- 70 : "right_handIndex_3_joint"
- 71 : "right_handIndexEnd_joint"
- 72 : "right_handMidStart_joint"
- 73 : "right_handMid_1_joint"
- 74 : "right_handMid_2_joint"
- 75 : "right_handMid_3_joint"
- 76 : "right_handMidEnd_joint"
- 77 : "right_handPinkyStart_joint"
- 78 : "right_handPinky_1_joint"
- 79 : "right_handPinky_2_joint"
- 80 : "right_handPinky_3_joint"
- 81 : "right_handPinkyEnd_joint"
- 82 : "right_handRingStart_joint"
- 83 : "right_handRing_1_joint"
- 84 : "right_handRing_2_joint"
- 85 : "right_handRing_3_joint"
- 86 : "right_handRingEnd_joint"
- 87 : "right_handThumbStart_joint"
- 88 : "right_handThumb_1_joint"
- 89 : "right_handThumb_2_joint"
- 90 : "right_handThumbEnd_joint"


取得した、パーツの情報とARBodyAnchorの中心点を乗算し
3D空間上での、パーツの位置・回転を取得する。

ViewController.swift
        /// jointTypeの位置・回転をキャスト
        let partsPoint = SCNMatrix4(transform)
        /// 基準点 hipの位置・回転をキャスト
        let hipPoint = SCNMatrix4(anchor.transform)
        /// func SCNMatrix4Mult(_ a: SCNMatrix4, _ b: SCNMatrix4) -> SCNMatrix4で行列を合成するときは、左のaが後にやる方、右のbが先にやる方、という風に考えて合成します。
        let matrix = SCNMatrix4Mult(partsPoint, hipPoint)
        let position = SCNVector3(matrix.m41, matrix.m42, matrix.m43)

すでにSceaneViewに同じものがあるかチェックし、あれば更新、なければ追加する

sceneViewから、nodeの名前をKeyに検索し、
存在していたら、nodeのpositionをセットして更新する。
存在していない場合は、「SCNSphere(radius: 0.02)」で球体を作成し
SCNNodeを作成、position、検索用のnameをセットし
作成した、SCNNodeを、sceneView.scene.rootNodeにaddChildNodeをする。

ViewController.swift
        if let nodeToUpdate = sceneView.scene.rootNode.childNode(withName: jointName, recursively: false) {
            /// 既に追加されているので、位置の更新のみ行う
            nodeToUpdate.isHidden = false
            nodeToUpdate.position = position
        } else {
            // GeoSphere
            // Radius 球の半径で初期値は 1。
            let sphereGeometry = SCNSphere(radius: 0.02)
            // チェックすると三角ポリゴンを均等に面が構成される。 初期値はfalse
            sphereGeometry.isGeodesic = true
            // 球体Color
            sphereGeometry.firstMaterial?.diffuse.contents = UIColor.green
            // ノードに球体ジオメトリを設定
            let sphereNode = SCNNode(geometry: sphereGeometry)
            // 表示位置設定
            sphereNode.position = position
            // ノードにname設定
            sphereNode.name = jointName
            // ルートノードに追加する
            sceneView.scene.rootNode.addChildNode(sphereNode)
        }

さあ完成です。

実行してみましょう。
人がいない場合、youtubeなどの歩いている人の動画をiPhoneの画面越しにみて
見て下さい。

全てのソースはGithubにPushしていますのでお取りくださいー。
https://github.com/Satoiosdevelop/ExampleMotionCapture

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

ARKit+SceneKit+Metal で 『君はラピュタ王の前にいるのだ』の再現

ARで壁や床から3Dモデルが現れる演出をよく見かけるので挑戦。
題材にしたのは『天空の城ラピュタ』。ラピュタの展望室でモウロ将軍の前にムスカとシータが天井から現れるシーン。(床に穴が空く仕掛けがある部屋を「展望室」?とは思うがwikiの表現に倣う)

(モデルがレッサーパンダなのはおいて、、)完成イメージ
demo.png demo.gif
再現にあたり困ったのがキャラクターと天井の境界面の薄黄色の部分(ラピュタの技術では境界面は薄黄色になったりならなかったりしているが、ここでは薄黄色に統一)。以下、再現方法を解説します。

再現方法

①ムスカとシータのノードを上下にアニメーションさせる
②天井境界の薄黄色面を作るための奥行き情報(以下、デプス)を作成する
 次の3つを作る。
 ・天井の境界平面のデプス
 ・キャラクターを cullMode = back で描画したときのデプス
 ・キャラクターの cullMode = front で描画したときのデプス
③②の情報から境界面とキャラクターの断面部分を判定し、①の画像に薄黄色を加える。

レンダリングパス(Xcodeの Capture GPU Frame)
renderpass.png
以下、個別にみていきましょう。

①ムスカとシータのノードを上下にアニメーションさせる

これは Xcode の Scene Editor で設定。
・キャラクターを並べる(キャラクターモデルはWWDC2017 SceneKit Demoから借用)。キャラクターは取りまとめノードchar_parentの下にぶら下げる。
・境界面ノードslice_planechar_parentと同列に配置。この境界面ノードはアニメーションしない。

 →色はほぼ透明にする。
slice_plane_1.png
 →Rendering Order の値を小さくして、キャラクターよりも先に描画することで、背景にキャラクターが描画されないようにする。
slice_plane_2.png
 ここでCategory bit mask を設定している。後でデプスを生成する際に、境界面とキャラクターを区別するのに使う。境界面は4、キャラクターは2を設定する。

 →アニメーションの設定をする
scene_animation.png

②天井境界の薄黄色面を作るための奥行き情報を作成する

キャラクターの正面側(手前側)のデプス情報と、キャラクターの背面側(見えない側)のデプス情報を取得し、その差分でキャラクターの実体を得る。
1) キャラクターの背面部分のデプスを取得
 →背面側のみ描画は cullMode=front という指定で行う(後述)
  キャラクターの断面は、ここで得られたデプスよりも大きい値となる。
2) キャラクターの正面部分のデプスを取得
 →正面側のみ描画は cullMode=back という指定で行う(後述)
  キャラクターの断面は、ここで得られたデプスよりも小さい値となる。
3) 境界面(天井平面)のデプスを取得
 上記1)2)のデプスでキャラクターの断面となるのは、この境界面のデプスの範囲とする。

上記3つ各々のデプス情報を SCNTechnique によるマルチパスレンダリングで生成。マルチパスレンダリングの定義は次の通り。

tequnique.json
{
    "targets" : {
        "color_scene"     : { "type" : "color" },
        "depth_slice"     : { "type" : "depth" },
        "depth_cullback"  : { "type" : "depth" },
        "depth_cullfront" : { "type" : "depth" }
    },
    "passes" : {
        "pass_scene" : {
            "draw"    : "DRAW_SCENE",
            "outputs" : {
                "color" : "color_scene"
            }
        },
        "pass_slice" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 4,
            "outputs" : {
                "depth" : "depth_slice"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },
        "pass_cullback" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 2,
            "cullMode"            : "back",
            "outputs" : {
                "depth" : "depth_cullback"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },
        "pass_cullfront" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 2,
            "cullMode"            : "front",
            "outputs" : {
                "depth" : "depth_cullfront"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },
        "pass_mix" : {
            "draw"   : "DRAW_QUAD",
            "inputs" : {
                "colorScene"     : "color_scene",
                "depthSlice"     : "depth_slice",
                "depthCullBack"  : "depth_cullback",
                "depthCullFront" : "depth_cullfront"
            },
            "metalVertexShader"   : "mix_vertex",
            "metalFragmentShader" : "mix_fragment",
            "outputs" : {
                "color" : "COLOR"
            },
            "colorStates" : {
                "clear"      : "true",
                "clearColor" : "0.0 0.0 0.0 0.0"
            }
        }
    },
    "sequence" : [
        "pass_scene",
        "pass_slice",
        "pass_cullback",
        "pass_cullfront",
        "pass_mix"
    ]
}

少しずつみていきましょう。

        "pass_scene" : {
            "draw"    : "DRAW_SCENE",
            "outputs" : {
                "color" : "color_scene"
            }
        },

これは、シーン全体の描画の定義。drawDRAW_SCENE を指定することでカメラキャプチャ画像+キャラクターの描画を行なっている。描画結果は色情報のみで、color_scene という名前のバッファに格納。

        "pass_slice" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 4,
            "outputs" : {
                "depth" : "depth_slice"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },

これは天井境界面の描画。includeCategoryMask4を指定しており、境界平面のみ描画する設定としている。この描画では色情報は不要でデプスのみ depth_slice という名前のバッファに格納。

        "pass_cullback" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 2,
            "cullMode"            : "back",
            "outputs" : {
                "depth" : "depth_cullback"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },

これは、正面からみたときのキャラクターのデプス情報を取得するための定義。includeCategoryMask2を指定しており、キャラクターのみ描画する設定としている。cullMode には back を指定しており、見えている部分を描画し、見えていない(背面)は描画しないようにしている(デフォルトはback)。この描画では色情報は不要でデプスのみ depth_cullback という名前のバッファに格納。

        "pass_cullfront" : {
            "draw"                : "DRAW_NODE",
            "includeCategoryMask" : 2,
            "cullMode"            : "front",
            "outputs" : {
                "depth" : "depth_cullfront"
            },
            "depthStates" : {
                "clear" : true,
                "func" : "less"
            }
        },

これは、キャラクターの背面側のデプス情報を取得するための定義。"pass_cullback"と同様だが、cullMode には front を指定している。

        "pass_mix" : {
            "draw"   : "DRAW_QUAD",
            "inputs" : {
                "colorScene"     : "color_scene",
                "depthSlice"     : "depth_slice",
                "depthCullBack"  : "depth_cullback",
                "depthCullFront" : "depth_cullfront"
            },
            "metalVertexShader"   : "mix_vertex",
            "metalFragmentShader" : "mix_fragment",
            "outputs" : {
                "color" : "COLOR"
            },
            "colorStates" : {
                "clear"      : "true",
                "clearColor" : "0.0 0.0 0.0 0.0"
            }
        }

これは、最終的にカメラキャプチャ+キャラクター+キャラクター断面を表示させる定義。
inputs に指定された各描画パスの出力結果(色情報、デプス)をmetalFragmentShader で指定されたmix_fragmentフラグメントシェーダー(後述)で合成して、最終画像としている。outputscolorCOLOR と指定することで画面に描画される。

③②の情報から境界面とキャラクターの断面部分を判定し、①の画像に薄黄色を加える

これは前述のmix_fragmentシェーダーで行う。
処理内容はソース内のコメントにある通りで、デプス情報で薄い黄色を表示するかどうかを判定し、シーン全体の色に加算している。

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> depthSlice [[texture(1)]],
                            depth2d<float,   access::sample> depthCullBack [[texture(2)]],
                            depth2d<float,   access::sample> depthCullFront [[texture(3)]])
{
    // 天井境界面のデプス
    float ds = depthSlice.sample(s, vert.uv);
    // 視点から見て手前を向いているポリゴンのデプス
    float db = depthCullBack.sample(s, vert.uv);
    // 視点から見て反対を向いたポリゴンのデプス
    float df = depthCullFront.sample(s, vert.uv);

    float4 sliceColor = float4(0.0, 0.0, 0.0, 0.0);
    if (df < ds) {
        // キャラクターの背面側より境界面が手前
        if (ds < db) {
            // さらに、キャラクターの前面側より境界面が後ろ
            sliceColor = float4(0.5, 0.5, 0.0, 0.0);    // 薄い黄色
        }
    }
    // カメラキャプチャ画像を含むシーン全体の画像に境界面の色を足す
    float4 fragment_color = colorScene.sample(s, fract(vert.uv));
    fragment_color += sliceColor;   // ざつな処理だと思うが、色の取り扱いに詳しくないので機会があれば見直す。

    return half4(fragment_color);
}

ここまでで説明は終わり。

ジオメトリとジオメトリの接触断面に色をつける方法はググっても見つからなかった。
今回、試行錯誤してなんとなく見れるようになったと思うが、この方法は境界面のデプスの他に、キャラクターの前面・背面の2つのデプス情報しか作っていないので、キャラクターの後ろに別のキャラクターがいる場合、後ろにいるキャラクターのデプスは手前のキャラクターのデプスで上書きされてしまい、境界面が描画されないという問題がある。
他に良い方法があると思うので、知っている方教えて欲しいです。
以下、試行錯誤過程で調べたり、試した内容です。

再現にあたり試した方法

1) 大量のヒットテストをして境界面でのキャラクターの形を探る
  境界面で SCNNodehitTestWithSegment(from:to:options:) を横に100並べて、それをキャラクターの前面→背面、背面→前面でヒットテストすることで、キャラクターの表面位置を求めて、断面図を作ってみた。
  →hitTestWithSegment の精度は期待するレベルではなくジオメトリの形とすこしずれた結果となり使えなかった。特に耳や足のように細かな箇所はヒット結果の位置が見た目と大きくずれた。本来の目的と全然異なる使い方なのでしょうがないとは思う。

2) 境界面のジオメトリをリアルタイムに作成
 ・キャラクターのジオメトリを境界面でまっ平にしてその部分を薄黄色にする。
  →例えば、足のようにジオメトリと境界面が複数箇所接しているときに、右足・左足のそれぞれでジオメトリを平にするのが相当面倒そう。試してはいない。
   また、ジオメトリがローポリの場合、ガタガタすると思われるので、テッセレーション(?)でジオメトリを分割しておくとか必要になると思われる。
 ・キャラクターのジオメトリと境界面に接する部分で新たに平面ジオメトリ を作って境界面に配置する。
  →これも、境界面の近くのジオメトリの頂点は取得できても、それから閉じた平面ジオメトリ を作るのが面倒と思われる(法線情報で頑張ればできるか??)。
  →「mesh slicing」でぐぐるといくつか方法が見つかるが難しそうで手が止まった。
   ・Algorithm or software for slicing a mesh
  →UE4だとリアルタイムでMesh Sliceができるっぽい。SceneKitにはなさそう。  
   ・https://unrealengine.hatenablog.com/entry/2016/09/12/002115

全体ソースコード

・swift

ViewController.swift
class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!

    private let device = MTLCreateSystemDefaultDevice()!
    private var charNode: SCNNode!
    private var isTouching = false      // タッチ検知中

    override func viewDidLoad() {
        super.viewDidLoad()
        // キャラクター読み込み。WWDC2017 SceneKit Demoを借用 https://developer.apple.com/videos/play/wwdc2017/604/
        guard let scene = SCNScene(named: "art.scnassets/scene.scn"),
              let charNode = scene.rootNode.childNode(withName: "char_node", recursively: true) else { return }
        self.charNode = charNode
        self.charNode.isHidden = true
        // Scene Technique セットアップ
        self.setupSCNTechnique()
        // AR Session 開始
        self.scnView.delegate = self
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal]
        self.scnView.session.run(configuration, options: [.removeExistingAnchors, .resetTracking])
    }
    //
    // フレームごとに呼び出される
    //
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime _: TimeInterval) {

        if isTouching {
            // 画面がタッチされた
            isTouching = false
            DispatchQueue.main.async {
                // 表示済みならスキップ
                guard self.charNode.isHidden else { return }

                let bounds = self.scnView.bounds
                let screenCenter = CGPoint(x: bounds.midX, y: bounds.midY)
                let results = self.scnView.hitTest(screenCenter, types: [.existingPlaneUsingGeometry])
                guard let existingPlaneUsingGeometryResult = results.first(where: { $0.type == .existingPlaneUsingGeometry }),
                      let _ = existingPlaneUsingGeometryResult.anchor as? ARPlaneAnchor else {
                    // 画面中央に平面がないので何もしない
                    return
                }
                // 画面中央にムスカとシータのノードを配置
                let position = existingPlaneUsingGeometryResult.worldTransform.columns.3
                self.scnView.scene.rootNode.addChildNode(self.charNode)
                self.charNode.simdPosition = SIMD3<Float>(position.x, position.y, position.z)
                self.charNode.isHidden = false
            }
        }
    }

    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
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let _ = touches.first else { return }
        isTouching = true
    }
}

・Metal

#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)]];   // 頂点座標
};

struct MixColorInOut {
    float4 position [[position]];
    float2 uv;
};

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,
                              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> depthSlice [[texture(1)]],
                            depth2d<float,   access::sample> depthCullBack [[texture(2)]],
                            depth2d<float,   access::sample> depthCullFront [[texture(3)]])
{
    // 天井境界面のデプス
    float ds = depthSlice.sample(s, vert.uv);
    // 視点から見て手前を向いているポリゴンのデプス
    float db = depthCullBack.sample(s, vert.uv);
    // 視点から見て反対を向いたポリゴンのデプス
    float df = depthCullFront.sample(s, vert.uv);

    float4 sliceColor = float4(0.0, 0.0, 0.0, 0.0);
    if (df < ds) {
        // キャラクターの背面側より境界面が手前
        if (ds < db) {
            // さらに、キャラクターの前面側より境界面が後ろ
            sliceColor = float4(0.5, 0.5, 0.0, 0.0);    // 薄い黄色
        }
    }
    // カメラキャプチャ画像を含むシーン全体の画像に境界面の色を足す
    float4 fragment_color = colorScene.sample(s, fract(vert.uv));
    fragment_color += sliceColor;   // ざつな処理だと思うが、色の取り扱いに詳しくないので機会があれば見直す。

    return half4(fragment_color);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift5]"lottie-ios"を使ってアニメーションを実装する方法

lottie-iosとは?

Lottie(ロッティー)は、Airbnbから登場したiOS、Android、React Native対応のアニメーションライブラリです。Adobe After Effectsで表示できるアニメーションをリアルタイムでレンダリングし、ネイティブアプリで静的なコンテンツを作るのと同じくらい簡単に面白い動きをするアニメーションを作成することができます。

▼詳しくはこちら
https://ferret-plus.com/6214

▼公式ページはこちら
https://airbnb.design/lottie/

実装環境

macOS Catalina 10.15.7
Xcode 12.1
Apple Swift version 5.3

今回はTestAppというプロジェクトで実装します。

導入の流れ

①CocoaPodsのインストールとインポート
②JSONファイルのダウンロード
③ダウンロードしたファイルを読み込んでコード記述

①CocoaPodsのインストールとインポート

必要とするPodsはpod 'lottie-ios'

Podfile.
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'TestApp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

pod 'IBMWatsonToneAnalyzerV3', '~> 3.6.0'

  # Pods for TestApp

  target 'TestAppTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'TestAppUITests' do
    # Pods for testing
  end

  //ここをインストール
  pod 'lottie-ios'

end

pod installが完了したらプロジェクトへimportします。

②JSONファイルのダウンロード

それではLottieのアニメーション検索ページへ移動してお気に入りのアニメーションをダウンロードしていきます。

▼検索ページ
https://lottiefiles.com/featured?page=1

お気に入りのアニメーションが決まったらクリックして下記のようなページを開きます。
image.png
右上にあるDownload JSONを選択し、Lottie JSONを選択してJSONファイルをダウンロードします。
image.png
ダウンロードが完了したらドラック&ドロップでプロジェクトファイル追に加します。
ドラックする場所はinfo.plistの上あたりでOKです。

③ダウンロードしたファイルを読み込んでコード記述

それではいよいよ実装です。

ViewController.swift
import UIKit
import Lottie //①でインポート済

class ViewController: UIViewController {

    //AnimationViewの宣言
    var animationView = AnimationView()

    override func viewDidLoad() {
        super.viewDidLoad()

        //アニメーションの呼び出し
        addAnimationView()
    }

    //アニメーションの準備
    func addAnimationView() {

        //アニメーションファイルの指定
        animationView = AnimationView(name: "38280-man-chilling-on-electric-scooter") //ここに先ほどダウンロードしたファイル名を記述(拡張子は必要なし)

        //アニメーションの位置指定(画面中央)
        animationView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)

        //アニメーションのアスペクト比を指定&ループで開始
        animationView.contentMode = .scaleAspectFit
        animationView.loopMode = .loop
        animationView.play()

        //ViewControllerに配置
        view.addSubview(animationView)
    }

動作確認

コードの記述を終えたら動作確認を行います。
下記画像のように画面中央にアニメーションが表示されていれば成功です!
image.png

最後に

今回行ったようにLottieを使えばクオリティの高いアニメーションを短いコードで実装できます。
他にもオシャレなアニメーションがたくさんあるので一度みてみても良いかもしれませんね!

最後までご覧いただきありがとうございます!
是非参考にしていただければと思います。

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

【Swift・駆け出し必見!】WebAPIの通信処理の実装を理解しよう

初学者がつまづく実装は大体決まっています。
・Delegate
・クロージャ
・通信周り
・エラーハンドリング
・アーキテクチャ

この辺りで「?」となる初学者は大勢いるのではないでしょうか?
そしてそのどれもが、開発をしていく上でとても重要なことです。

ここでは、API通信の処理を理解するための内容にしたいと思います。
どこの記事にも書いてあるような説明は省いたりしています。

Delegateについて理解したいなら
https://qiita.com/Sossiii/items/6c8916f96176991f19aa
こちらの記事を読んでいただきたいです。
Twitterでもなかなか好評でした。

未経験から実務につくためのスキルマップ(ロードマップ)も公開しています。
https://qiita.com/Sossiii/items/e2c846d12f2f5db7c426

API通信をするためのステップ

ステップ0:そもそもAPIとは何か

https://qiita.com/NagaokaKenichi/items/df4c8455ab527aeacf02
こちらの記事に詳細に書かれていますが、初学者だと難しいですね。
この記事を読み進む上でざっくり説明すると、
ある情報通信によって外部から受信するためのものです。
例えばアプリを開発していると、イベント情報を管理画面で操作して
アプリ側に送信したい時があります。
そのためにいちいちアプリのアップデートをかけるわけにはいきません。
API通信を利用すると、
「イベント情報があったらアプリ内で表示する」
といったロジックを実装することができます。

ステップ1:受け取るデータ構造を確認する

さて、APIについてざっくりと理解したところで、
次は利用するための手順を説明していきます。
まずやることは、どんなデータが送られてくるのかを確認することです。

大まかなAPI通信処理の流れは、
1・リクエストを投げる
2・データを受け取る
3・受け取ったデータをマッピングする
4・マッピングしたデータを実際に利用する

といったものなのですが、マッピングの段階で
データの型が違っていたり、受け取り側の構造が違うと
マッピングが失敗してしまいます。
そのため、まずはどんなデータが送られてくるのかを確認します。

確認する方法は、URLを検索するだけです。
https://covid19-japan-web-api.now.sh/api//v1/prefectures
例えばコロナウイルスの感染状況を公開しているこちらのURLに
アクセスしてみましょう。
スクリーンショット 2020-11-06 11.19.37.png
※chrome拡張機能のJsonViewで見やすくしています

ここで確認するのは「key」「valueとその型」「データ構造」です。
keyとは、画像で言うとid, name_ja, name_enなどにあたり
valueは1(Int), "北海道"(String), "Hokkaido"(String)にあたります
データ構造は、階層{ }や配列になっているか[ ]です。

アウトプットしないとなぜこれらを確認しないといけないのかわからないので、
あ〜そんなものがあるのか、程度に読み進めてください。

ステップ2:マッピングの準備、モデルを書く

送られてくるデータ構造を把握したら、マッピングするための
コードを書いていきます。マッピングにおいて、送られてくるデータから
利用したいデータを抜き取り、利用する側の言語で利用可能にします。

https://covid19-japan-web-api.now.sh/api//v1/prefectures
で送られてくるデータから、id, name_ja, pcrを利用する場合を考えます。
この場合、知りたい部分のkey, value, 構造に着目すると以下のようになります。

[ 
  { 
    id:1
    name_ja: "北海道",
    ...
    pcr: 85892,
  },
  { 
    id:2
    name_ja: "青森",
    ...
    pcr: 5350,
  },
...
]

それでは実際にマッピングするためのモデルを書いていきます。
Xcodeを開いたら、新しいファイルを作りましょう。
ファイル名はCovid.swiftとします。

//Covid.swift file
struct CovidInfo: Codable {
  let id: Int
  let nameJa: String
  let pcr: Int
  //2単語以上の変数をCodingKeyによってスネークケースに変換している
  //マッピングの際にはスネークケースが適用される
  enum CodingKeys: String, CodingKey {
    case nameJa = "name_ja"
  }
}

基本的に変数・定数名はローワーキャメルケース(https://wa3.i-3-i.info/word13956.html)
で書く必要があるので上記のように(nameJa)書いていますが、
返ってくるJSONデータではname_jaとなっていますね。

マッピングの際はここの書き方が違うと失敗するので、
同じ形にしてあげる必要があります。
そこで活躍するのがCodingKeyです。
enum CodingKey: String, CodingKey {}
で一致させているのですね。

さて、これでマッピングのための受け皿が完成したので、
次のステップで実際にリクエストを投げていきます。

ステップ3:リクエストを投げる関数を作成する

API系の処理は、APIファイルを新たに作成して共通化します。
API.swiftとでもしておきましょう。
AlamofireやMoyaなどのライブラリを使いたいところですが、
駆け出しの方向けのためここではライブラリを使わないで進めていきます。

struct CovidAPI {
    static func getPrefectures(completion: @escaping ([CovidInfo]) -> Void) {
        let url = URL(string: "https://covid19-japan-web-api.now.sh/api//v1/prefectures")
        let request = URLRequest(url: url!)
        URLSession.shared.dataTask(with: request) { (data, response, error) in

            if let data = data {
                let result = try! JSONDecoder().decode([CovidInfo].self, from: data)
                completion(result)
            }
        }.resume()
    }
}

上から順に解説していきます。

static func getPrefectures(completion: @escaping ([CovidInfo]) -> Void)
//CovidInfoに[]がついているのはStep1でデータ構造を確認した時に[ ]がついていた(配列になっていた)
//からです。複数のデータが飛んでくるので呼び出し処理では配列にしてあげます。
//staticをつけることで、CovidAPIをインスタンス化しなくても外部から
//呼び出せるようにしています。
//staticがある場合
CovidAPI.getPrefectures()
//staticがない場合
CovidAPI().getPrefectures()
//引数にクロージャを使っています。
//関数内のデータはデフォルトで外部に持ち越すことができません。
//@escapingをつけることでそれを可能にしています。
URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let data = data {
                let result = try! JSONDecoder().decode([CovidInfo].self, from: data)
                completion(result)
            }
        }.resume()
//URLSessionの行で実際にリクエストを投げていて、{()}内がレスポンスです。
//dataが私たちが欲しいデータになるので、if let文でdataが返ってきた場合にのみ
//マッピングしていますね。
//dataをCovidInfo型に合わせて(マッピング)定数resultに代入し、
//completionに渡しています。(呼び出し側にデータを返している)

ステップ4:実際に呼び出してみる

さて、これでAPI通信を利用する準備が整ったので、ViewControllerから呼び出してみましょう。

//ViewController.swift
CovidAPI.getPrefectures(completion: {(result: [CovidInfo]) -> Void in
    DispatchQueue.main.async {
        idLabel.text = "\(result.id)"
        nameLabel.text = "\(result.nameJa)"
        pcrLabel.text = "\(result.pcr)"
    }
})

idLabel, nameLabel, pcrLabelは自分でUILabel()を用意して画面に配置してください。
データが反映されたらこの記事のステップは完了です。

おわりに

通信周りの処理の流れは理解していただけたでしょうか。
1・リクエストを投げる
2・データを受け取る
3・受け取ったデータをマッピングする
4・マッピングしたデータを実際に利用する

の流れで行いますが、3のマッピングのために
1・返ってくるデータを確認(データ名とデータ構造)
2・受け取りたいデータを決めて受け皿を用意(型)

の2ステップが必要になります。
受け皿の型や変数名が違っているだけで通信処理に失敗するので、
ここの2ステップが最も重要と言っても過言ではありません。

駆け出しの方が苦戦する実装の1つなので、少しでも参考になれば幸いです。

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

画像の上に文字を表示する

画像の上に文字を表示したかったのですが、すごくすごく頼りになったURL↓
https://qiita.com/taku/items/2c246d92bec494a1df5e

上記URLをそのままコピーし他のですがSwift初心者のため、Swiftバージョンアップに伴う少しの変更で結構手間取ってしまったのでメモしておきます。。
こちらのソースで無事Swift12でも画像にテキストを表示できました。

  //画像の上にテキストを表示させる
    func drawText(image :UIImage) ->UIImage
    {
        let text = "Sample Text"

        let tmpImage = UIImage(named: "1")
        let font = UIFont.boldSystemFont(ofSize: 32)
        let imageRect = CGRect(x: 0, y: 0, width: tmpImage.size.width, height: tmpImage.size.height)
        UIGraphicsBeginImageContext(tmpImage.size);
        tmpImage.draw(in: imageRect)
        let textRect = CGRect(x: 5, y: 5, width: tmpImage.size.width - 5 , height: tmpImage.size.height - 5)
        let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
        let textFontAttributes = [
            NSAttributedString.Key.font: font,
            NSAttributedString.Key.foregroundColor: UIColor.black,
            NSAttributedString.Key.paragraphStyle: textStyle
        ] as [NSAttributedString.Key : Any]
        text.draw(in: textRect, withAttributes: textFontAttributes)

        let newImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext()
        return newImage!
    }

本当はAlamofire経由でJSONから取得したImageに動的にテキストを配置したいのですがそれはまだこれから・・・

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

【Swift】SCNNodeの色を変更する

SceneKit View上に描画をした際に、描画の色を変更したかったので調べました。

実行環境 バージョン
Swift 5
Xcode 12.0

例としてballのnodeを作成しています。

let ball = SCNSphere(radius: 0.01)
let node = SCNNode(geometry: ball)

//色変更のコード
ball.firstMaterial?.diffuse.contents = UIColor.red

UIColor.Redの部分にお好みの色を入れると色変更が出来ます。

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