20201009のSwiftに関する記事は10件です。

【ReactNative】SwiftでNativeModuleを書く

ReactNative-0.63.3 XCode-12

はじめに

iOSのネイティブAPIにアクセスするために、ReactNatveにはNative ModuleというAPIが用意されています。
今回は、Swiftでネイティブモジュールを実装する方法をまとめます。

ReactNativeプロジェクトの作成

まだプロジェクトがない場合は、作成します。

npx react-native init myApp

XCodeでSwiftファイルを作成する

ios/myApp.xcodeprojにあるプロジェクトをXCodeで開きます。

NativeModulesフォルダを作成します。(任意)

NativeModulesフォルダ内に.swiftファイルを作成します。

スクリーンショット 2020-10-09 16.15.25.png

Objective-C Bridging Headerを作成する

.swiftファイルを作成すると、XCodeからObjective-C Bridging Headerを作成するかどうか聞かれるの、作成する。

このファイルは、名前の通りSwiftファイルとObjective-Cファイルをブリッジするものです。
ファイル名は変えてはいけません。

以下のように、Objective-C Bridging Headerに追記します。

myApp-Bridging-Header.h
// myApp-Bridging-Header.h

#import"React/RCTBridgeModule.h"

メソッドを実装する

手始めに、最も簡単なネイティブモジュールを実装してみましょう。
カウンターの値を変化させます。

Counter.swift
import Foundation@objc(Counter)
class Counter: NSObject {

  private var count = 0

  @objc
  func increment() {
    count += 1
    print("count is \(count)")
  }
}

メソッドをReactNativeから扱えるようにする

実装したメソッドをReactNativeから扱えるようにObjective-Cのファイルを作成します。

先ほど作成したSwiftファイルと同じ名前で、同ディレクトリに作成します。

スクリーンショット 2020-10-09 16.51.20.png

作成したファイルには以下を追記します。

Counter.m
@interface RCT_EXTERN_MODULE(Counter, NSObject)
  RCT_EXTERN_METHOD(increment)
@end

RCT_EXTERN_METHODについて

RCT_EXTERN_METHODに記述したメソッドは、ReactNativeで使用することができます。

メソッドに引数がない場合は、以下のように記述します。

RCT_EXTERN_METHOD(methodName)

引数がある場合は、以下のように記述します。

RCT_EXTERN_METHOD(
  methodName: (paramType1)internalParamName1
)

例えば、incrementメソッドに引数が必要な場合swiftファイルは以下のようになります。

@objc
func increment(_ num: Int) {
  ...
}

対応するRCT_EXTERN_METHODは以下のようになります。

RCT_EXTERN_METHOD(
  increment: (Int)num
)

ReactNativeから呼び出す

NativeModulesをインポートし、クラス名.メソッドで呼び出すことができます。

import { NativeModules } from 'react-native'

NativeModules.Counter.increment(1));

Event Emitterを扱う

iOSネイティブ側で起こるイベントをSunscribeしたい時には、RCTEventEmitterを使用します。

Objective-Cのファイルに、以下を記述します。

#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"

@interface RCT_EXTERN_MODULE(Counter, RCTEventEmitter)

さらに、ブリッジファイルにも追記します。

CounterApp-Bridging-Header.h
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"

Swiftファイルには以下を実装します。

  • sendEvent: イベント名と、その内容を記述
  • supportedEvents: ReactNative側でリスナーを貼る時のイベント名を記述
@objc(Counter)
class Counter: RCTEventEmitter {
  @objc
  func increment() {
    count += 1
    print("count is \(count)")
    sendEvent(withName: "onIncrement", body: ["count": count])
  }

  override func supportedEvents() -> [String]! {
    return ["onIncrement"]
  }
}

ReactNative側で、イベントリスナーで待ち受けます。

import {
  NativeModules,
  NativeEventEmitter
} from 'react-native'

const CounterEvents = new NativeEventEmitter(NativeModules.Counter);

CounterEvents.addListener(
  "onIncrement",
  (res) => console.log(res)
);

NativeModules.Counter.increment();

[警告] Module requires main queue setupへの対処

以下のような警告が出ることがあります。

スクリーンショット 2020-10-09 22.41.02.png

これは、モジュールの処理をメインスレッドで行うか、バックグラウンドで行うか設定しなさいという警告です。
以下のように記述し、警告を消すことができます。

@objc
static func requiresMainQueueSetup() -> Bool {
  return true
}
  • trueを返す: メインスレッドで処理
  • falseを返す: バックグラウンドで処理

まとめ

ネイティブモジュールをSwiftで書く方法をまとめました。

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

GroupedのUITableViewで一番上の余白を消したい(swift)

Xcode-12.0 iOS-14.0 Swift-5.3

はじめに

下記の画像のように stylegroupedUITableView で条件によって一番上の余白の表示・非表示を切り替えたいときに試行錯誤したのでやり方をメモ。

余白表示 余白非表示
success_1 success_2

今回は画像のようにヘッダー表示時は余白をなくして、ヘッダー非表示の場合は余白ありにしたかった。。。

だめだったパターン

とりあえずだめだったパターン。。。下記のように isHeaderShowntrue のときにセクション0にヘッダーを設定して高さを設定してみた。

extension TableViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        guard section == 0 else {
            return UITableView.automaticDimension
        }
        return isHeaderShown ? 50 : UITableView.automaticDimension
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        guard section == 0 else {
            return nil
        }
        return isHeaderShown ? HeaderView() : nil
    }
}

結果

failure

初回表示だけいけてるけど2回目以降がおかしい。。。:frowning2:

いけてそうなパターン1

上の方法 + 下記のようにテーブル更新時に tableHeaderView をいじってみました。

tableView.tableHeaderView = isHeaderShown ? UIView() : nil
tableView.reloadData()

結果

success

いけてそう:tada:

いけてそうなパターン2

上の方法でもいけてそうですがそもそも一番上にしかヘッダー設定しないなら sectionHeader に View を設定する必要はないと思い最終的には下記のようにしました。

extension TableViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        guard section == 0 else {
            return UITableView.automaticDimension
        }
        return isHeaderShown ? CGFloat.leastNormalMagnitude : UITableView.automaticDimension
    }
}

// 更新時の処理
tableView.tableHeaderView = isHeaderShown ? HeaderView(frame: .init(origin: .zero, size: .init(width: 0, height: 50))) : nil
tableView.reloadData()

結果はパターン1と同じだったのでたぶんいけてそう:clap:

おわりに

こんなレイアウトにしたくなるのはレアだと思いますがどなたかの参考になれば幸いです。他にいい方法ご存知でしたらぜひ教えて下さい:pray:

今後は UITableView より UICollectionView 使っていった方がいいよって言うのも聞くしもうあんまりテーブルをごちゃごちゃするのはよくないのかもしれない。。。:thinking:

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

SwiftのOptionalを存分に活用してみる

Optionalについて軽くおさらい

Optionalとは

Swiftではオプショナル型という特殊な型が用意されています。
オプショナル型とは、通常は持っている型とは別に例外的にnilとなってしまう値に対しても定義できる型のことを示します。

例えば、通常は文字列を持っている変数optionalNameが、ある特殊なケースにおいてnilが代入される場合がある時、変数optionalNameString?と表現することが出来ます。

var optionalName: String? = "Brian"

optionalName = nil //OptionalなString型であるため、nilも代入可能 

nilの代入が許容されることで、例外的に変数にnilが入った場合でも条件分岐などでnilチェックを行うことでアプリケーションのクラッシュ等を防ぐことが出来ます。

var optionalName: String? = "Brian"

optionalName = nil //OptionalなString型であるため、nilも代入可能 

if optionalName == nil {
    print("名前にnilが代入されています。")
} else {
    print( optionalName! ) // Optionalな値は、nilでない場合は!をつけて値を開示する必要がある
}

オプショナル束縛構文

上記のように、if文でOptional型がnilかどうかを判別するやり方もありますが、Swiftではオプショナル束縛構文というのも用意されています。

var optionalName: String? = "Brian"

if let name = optionalName {
    print( name )
}

この書き方をすることで、optionalNameがnilでなければnameに値が代入され、条件分岐に入ることが出来ます。
またこの時、nameに代入されている時点でOptionalな値が開示されているため、nameを開示する必要はありません。

より詳しいOptionalの解説については、是非こちらをご覧ください。
SwiftのOptional型を極める
詳解Swift 第5版 Chapter4 Optional

サインイン/サインアウト処理を例に

ではこれがSwiftではどういう時に恩恵が得られるか、アプリのサインイン/サインアウトの処理を例に見てみましょう。

雑ですが、今回はサインインをするための「SigninView」とサインイン後の「ContentView」、そしてサインインしているユーザ情報を保持する「AppState」を以下のように組み合わせたアプリを想定します。
※サインインには、FirebaseAuthenticationを使います。これについての詳しい説明は割愛します。

struct1.png

サンプルコード

まずはAppStateです。
こちらは、サインインするユーザの情報を保持しており、各Viewから参照されることを想定しているクラスです。

Sessionという構造体は、EnvironmentObjectとして定義しています。

/// ユーザ情報を格納するリポジトリクラス
class User: Identifiable {
    public var uid: String
    public var email: String?

    init(uid: String, email: String) {
        self.uid = uid
        self.email = email
    }
}

class AppState: ObservableObject {

    // 認証状態を管理するための構造体
    struct Session {
        var user: User?
        var handler: AuthStateDidChangeListenerHandle? //サインインしているユーザ情報を保持するオブジェクト
    }

    @Published public var session = Session()
}

次にSessionに関する処理(サインイン/サインアウト)を担うInteractorクラスです。

import Firebase

class SessionInteractor {

    public var appState: AppState

    /// ViewコンポーネントよりEnvironmentObjectを取得し、クラスプロパティに格納する
    init(appState: AppState) {
        self.appState = appState
    }

    /// ユーザのログイン状態が変わるたびに呼び出される処理
    public func listen() {
        self.appState.session.handler = Auth.auth().addStateDidChangeListener { (auth, user) in
            // guard let文を利用して早期リターン
            guard let user = user else {
                self.appState.session.user = nil
                return
            }            
            // Session型のuserプロパティに追加
            self.appState.session.user = User(
                uid: user.uid,
                email: user.email!
            )
        }
    }

    /// サインイン処理
    public func signIn() {
       // FirebaseAuthenticationを使ったサインイン処理
       // FIRAuthオブジェクトのアタッチとかを行っていますが詳細な処理は割愛        
        Auth.auth().signIn(/* [中略]FirebaseAuthenticationを使ったサインイン */)
    }

    /// サインアウト処理
    public func signOut() -> Bool {
        do {
            try Auth.auth().signOut()
            self.appState.session.user = nil
            return true
        } catch {
            return false
        }
    }
}

次に各Viewのコードです。

import SwiftUI

struct SignInView: View {
    @EnvironmentObject var appState: AppState

    var body: some View {
        VStack() {
            TextField("メールアドレス", text: $email)
            SecureField("パスワード", text: $password)
            Button(action: self.signIn) {
                Text("サインイン")
            }
        }
    }

    /// 実際にサインインを行うメソッド
    private func signIn() {
       let sessionInteractor = SessionInteractor(appState: self.appState)
        let result = sessionInteractor.signIn()
    }
}

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var appState: AppState

    var body: some View {
        VStack() {
            if let user = self.appState.session.user {
                Text("こんにちは、\(user.email!)")
                Button(action: self.signOut) {
                  Text("サインアウト")
                }
            }
        }.onAppear(perform: self.loadUserInfo)
    }

    // サインアウトの処理を呼び出す
    private func signOut() {
        let sessionInteractor = SessionInteractor(appState: self.appState)
        let result = sessionInteractor.signOut()
    }
}

Optionalの定義と開示

AppStateが持っている、Userオブジェクトが今回の肝となります。

// 認証状態を管理するための構造体
struct Session {
    var user: User? //記述は省略しますがユーザ情報をプロパティに持つ構造体
    var handler: AuthStateDidChangeListenerHandle? //サインインしているユーザ情報を保持するオブジェクト
}

これらはOptionalで定義されていますが、これはサインイン前やサインアウト後などはサインインしているユーザのオブジェクトがnilになるためです。
サインインが完了し、Userオブジェクトにユーザ情報が入った後は、Viewの中でそれらの情報を開示しています。

var body: some View {
    VStack() {
        if let user = self.appState.session.user {
            Text("こんにちは、\(user.email!)") // !で開示して表示
            Button(action: self.signOut) {
              Text("サインアウト")
            }
        }
    }.onAppear(perform: self.loadUserInfo)
}

ここで注目したいのが、オプショナル束縛構文を使って分岐をしている部分です。

if let user = self.appState.session.user {

これを記述することで、ユーザがサインアウトした後にUserオブジェクトがnilになっても問題ありません。
逆にこれを記述しないと、View側はサインアウト時に代入されるnilを受け取り、アプリがクラッシュしてしまいます。

Fatal error: Unexpectedly found nil while unwrapping an Optional value
try Auth.auth().signOut()
self.appState.session.user = nil // サインアウト時にユーザ情報を剥がしている

struct2.png

まとめ

今回はサインイン/サインアウトを例に取り上げましたが、モバイルアプリを作る上で複数のViewが同じ値を参照していることが多々あると思います。
参照しているViewが多ければ多いほど、予期せぬnilを受け取る可能性があり、アプリのクラッシュを引き起こしてしまいますが、Optional型を活用することで、それらを未然に防ぐことができます。
何でもかんでもOptionalにするべきではありませんが、nilの代入が想定されるプロパティや変数に関しては、Optionalを活用しましょう!

参考

iOS で Firebase Authentication を使ってみる
SwiftのOptional型を極める
詳解Swift 第5版 Chapter4 Optional

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

SwiftのOptionalを活用してログイン機能を作る

Optionalについて軽くおさらい

Optionalとは

Swiftではオプショナル型という特殊な型が用意されています。
オプショナル型とは、通常は持っている型とは別に例外的にnilとなってしまう値に対しても定義できる型のことを示します。

例えば、通常は文字列を持っている変数optionalNameが、ある特殊なケースにおいてnilが代入される場合がある時、変数optionalNameString?と表現することが出来ます。

var optionalName: String? = "Brian"

optionalName = nil //OptionalなString型であるため、nilも代入可能 

nilの代入が許容されることで、例外的に変数にnilが入った場合でも条件分岐などでnilチェックを行うことでアプリケーションのクラッシュ等を防ぐことが出来ます。

var optionalName: String? = "Brian"

optionalName = nil //OptionalなString型であるため、nilも代入可能 

if optionalName == nil {
    print("名前にnilが代入されています。")
} else {
    print( optionalName! ) // Optionalな値は、nilでない場合は!をつけて値を開示する必要がある
}

オプショナル束縛構文

上記のように、if文でOptional型がnilかどうかを判別するやり方もありますが、Swiftではオプショナル束縛構文というのも用意されています。

var optionalName: String? = "Brian"

if let name = optionalName {
    print( name )
}

この書き方をすることで、optionalNameがnilでなければnameに値が代入され、条件分岐に入ることが出来ます。
またこの時、nameに代入されている時点でOptionalな値が開示されているため、nameを開示する必要はありません。

より詳しいOptionalの解説については、是非こちらをご覧ください。
SwiftのOptional型を極める
詳解Swift 第5版 Chapter4 Optional

サインイン/サインアウト処理を例に

ではこれがSwiftではどういう時に恩恵が得られるか、アプリのサインイン/サインアウトの処理を例に見てみましょう。

雑ですが、今回はサインインをするための「SigninView」とサインイン後の「ContentView」、そしてサインインしているユーザ情報を保持する「AppState」を以下のように組み合わせたアプリを想定します。
※サインインには、FirebaseAuthenticationを使います。これについての詳しい説明は割愛します。

struct1.png

サンプルコード

まずはAppStateです。
こちらは、サインインするユーザの情報を保持しており、各Viewから参照されることを想定しているクラスです。

Sessionという構造体は、EnvironmentObjectとして定義しています。

/// ユーザ情報を格納するリポジトリクラス
class User: Identifiable {
    public var uid: String
    public var email: String?

    init(uid: String, email: String) {
        self.uid = uid
        self.email = email
    }
}

class AppState: ObservableObject {

    // 認証状態を管理するための構造体
    struct Session {
        var user: User?
        var handler: AuthStateDidChangeListenerHandle? //サインインしているユーザ情報を保持するオブジェクト
    }

    @Published public var session = Session()
}

次にSessionに関する処理(サインイン/サインアウト)を担うInteractorクラスです。

import Firebase

class SessionInteractor {

    public var appState: AppState

    /// ViewコンポーネントよりEnvironmentObjectを取得し、クラスプロパティに格納する
    init(appState: AppState) {
        self.appState = appState
    }

    /// ユーザのログイン状態が変わるたびに呼び出される処理
    public func listen() {
        self.appState.session.handler = Auth.auth().addStateDidChangeListener { (auth, user) in
            // guard let文を利用して早期リターン
            guard let user = user else {
                self.appState.session.user = nil
                return
            }            
            // Session型のuserプロパティに追加
            self.appState.session.user = User(
                uid: user.uid,
                email: user.email!
            )
        }
    }

    /// サインイン処理
    public func signIn() {
       // FirebaseAuthenticationを使ったサインイン処理
       // FIRAuthオブジェクトのアタッチとかを行っていますが詳細な処理は割愛        
        Auth.auth().signIn(/* [中略]FirebaseAuthenticationを使ったサインイン */)
    }

    /// サインアウト処理
    public func signOut() -> Bool {
        do {
            try Auth.auth().signOut()
            self.appState.session.user = nil
            return true
        } catch {
            return false
        }
    }
}

次に各Viewのコードです。

import SwiftUI

struct SignInView: View {
    @EnvironmentObject var appState: AppState

    var body: some View {
        VStack() {
            TextField("メールアドレス", text: $email)
            SecureField("パスワード", text: $password)
            Button(action: self.signIn) {
                Text("サインイン")
            }
        }
    }

    /// 実際にサインインを行うメソッド
    private func signIn() {
       let sessionInteractor = SessionInteractor(appState: self.appState)
        let result = sessionInteractor.signIn()
    }
}

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var appState: AppState

    var body: some View {
        VStack() {
            if let user = self.appState.session.user {
                Text("こんにちは、\(user.email!)")
                Button(action: self.signOut) {
                  Text("サインアウト")
                }
            }
        }.onAppear(perform: self.loadUserInfo)
    }

    // サインアウトの処理を呼び出す
    private func signOut() {
        let sessionInteractor = SessionInteractor(appState: self.appState)
        let result = sessionInteractor.signOut()
    }
}

Optionalの定義と開示

AppStateが持っている、Userオブジェクトが今回の肝となります。

// 認証状態を管理するための構造体
struct Session {
    var user: User? //記述は省略しますがユーザ情報をプロパティに持つ構造体
    var handler: AuthStateDidChangeListenerHandle? //サインインしているユーザ情報を保持するオブジェクト
}

これらはOptionalで定義されていますが、これはサインイン前やサインアウト後などはサインインしているユーザのオブジェクトがnilになるためです。
サインインが完了し、Userオブジェクトにユーザ情報が入った後は、Viewの中でそれらの情報を開示しています。

var body: some View {
    VStack() {
        if let user = self.appState.session.user {
            Text("こんにちは、\(user.email!)") // !で開示して表示
            Button(action: self.signOut) {
              Text("サインアウト")
            }
        }
    }.onAppear(perform: self.loadUserInfo)
}

ここで注目したいのが、オプショナル束縛構文を使って分岐をしている部分です。

if let user = self.appState.session.user {

これを記述することで、ユーザがサインアウトした後にUserオブジェクトがnilになっても問題ありません。
逆にこれを記述しないと、View側はサインアウト時に代入されるnilを受け取り、アプリがクラッシュしてしまいます。

Fatal error: Unexpectedly found nil while unwrapping an Optional value
try Auth.auth().signOut()
self.appState.session.user = nil // サインアウト時にユーザ情報を剥がしている

struct2.png

まとめ

今回はサインイン/サインアウトを例に取り上げましたが、モバイルアプリを作る上で複数のViewが同じ値を参照していることが多々あると思います。
参照しているViewが多ければ多いほど、予期せぬnilを受け取る可能性があり、アプリのクラッシュを引き起こしてしまいますが、Optional型を活用することで、それらを未然に防ぐことができます。
何でもかんでもOptionalにするべきではありませんが、nilの代入が想定されるプロパティや変数に関しては、Optionalを活用しましょう!

参考

iOS で Firebase Authentication を使ってみる
SwiftのOptional型を極める
詳解Swift 第5版 Chapter4 Optional

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

SwiftのOptional、何が嬉しいのか

Optionalとは

Swiftではオプショナル型という特殊な型が用意されています。
オプショナル型とは、通常は持っている型とは別に例外的にnilとなってしまう値に対しても定義できる型のことを示します。

例えば、通常は文字列を持っている変数optionalNameが、ある特殊なケースにおいてnilが代入される場合がある時、変数optionalNameString?と表現することが出来ます。

var optionalName: String? = "Brian"

optionalName = nil //OptionalなString型であるため、nilも代入可能 

nilの代入が許容されることで、例外的に変数にnilが入った場合でも条件分岐などでnilチェックを行うことでアプリケーションのクラッシュ等を防ぐことが出来ます。

var optionalName: String? = "Brian"

optionalName = nil //OptionalなString型であるため、nilも代入可能 

if optionalName == nil {
    print("名前にnilが代入されています。")
} else {
    print( optionalName! ) // Optionalな値は、nilでない場合は!をつけて値を開示する必要がある
}

オプショナル束縛構文

上記のように、if文でOptional型がnilかどうかを判別するやり方もありますが、Swiftではオプショナル束縛構文というのも用意されています。

var optionalName: String? = "Brian"

if let name = optionalName {
    print( name )
}

この書き方をすることで、optionalNameがnilでなければnameに値が代入され、条件分岐に入ることが出来ます。
またこの時、nameに代入されている時点でOptionalな値が開示されているため、nameを開示する必要はありません。

より詳しいOptionalの解説については、是非こちらをご覧ください。
SwiftのOptional型を極める
詳解Swift 第5版 Chapter4 Optional

実際にどう使われるか

ではこれがSwiftではどういう時に恩恵が得られるか、アプリのサインイン/サインアウトの処理を例に見てみましょう。

雑ですが、今回はサインインをするための「SigninView」とサインイン後の「ContentView」、そしてサインインしているユーザ情報を保持する「AppState」を以下のように組み合わせたアプリを想定します。
※サインインには、FirebaseAuthenticationを使います。これについての詳しい説明は割愛します。

struct1.png

サンプルコード

まずはAppStateです。
こちらは、サインインするユーザの情報を保持しており、各Viewから参照されることを想定しているクラスです。

Sessionという構造体は、EnvironmentObjectとして定義しています。

/// ユーザ情報を格納するリポジトリクラス
class User: Identifiable {
    public var uid: String
    public var email: String?

    init(uid: String, email: String) {
        self.uid = uid
        self.email = email
    }
}

class AppState: ObservableObject {

    // 認証状態を管理するための構造体
    struct Session {
        var user: User?
        var handler: AuthStateDidChangeListenerHandle? //サインインしているユーザ情報を保持するオブジェクト
    }

    @Published public var session = Session()
}

次にSessionに関する処理(サインイン/サインアウト)を担うInteractorクラスです。

import Firebase

class SessionInteractor {

    public var appState: AppState

    /// ViewコンポーネントよりEnvironmentObjectを取得し、クラスプロパティに格納する
    init(appState: AppState) {
        self.appState = appState
    }

    /// ユーザのログイン状態が変わるたびに呼び出される処理
    public func listen() {
        self.appState.session.handler = Auth.auth().addStateDidChangeListener { (auth, user) in
            if user != nil {
                // Session型のuserプロパティに追加
                self.appState.session.user = User(
                    uid: user!.uid,
                    email: user!.email!
                )
            } else {
                self.appState.session.user = nil
            }
        }
    }

    /// サインイン処理
    public func signIn() {
       // FirebaseAuthenticationを使ったサインイン処理
       // FIRAuthオブジェクトのアタッチとかを行っていますが詳細な処理は割愛        
        Auth.auth().signIn(/* [中略]FirebaseAuthenticationを使ったサインイン */)
    }

    /// サインアウト処理
    public func signOut() -> Bool {
        do {
            try Auth.auth().signOut()
            self.appState.session.user = nil
            return true
        } catch {
            return false
        }
    }
}

次に各Viewのコードです。

import SwiftUI

struct SignInView: View {
    @EnvironmentObject var appState: AppState

    var body: some View {
        VStack() {
            TextField("メールアドレス", text: $email)
            SecureField("パスワード", text: $password)
            Button(action: self.signIn) {
                Text("サインイン")
            }
        }
    }

    /// 実際にサインインを行うメソッド
    private func signIn() {
       let sessionInteractor = SessionInteractor(appState: self.appState)
        let result = sessionInteractor.signIn()
    }
}

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var appState: AppState

    var body: some View {
        VStack() {
            if let user = self.appState.session.user {
                Text("こんにちは、\(user.email!)")
                Button(action: self.signOut) {
                  Text("サインアウト")
                }
            }
        }.onAppear(perform: self.loadUserInfo)
    }

    // サインアウトの処理を呼び出す
    private func signOut() {
        let sessionInteractor = SessionInteractor(appState: self.appState)
        let result = sessionInteractor.signOut()
    }
}

Optionalの定義と開示

AppStateが持っている、Userオブジェクトが今回の肝となります。

// 認証状態を管理するための構造体
struct Session {
    var user: User? //記述は省略しますがユーザ情報をプロパティに持つ構造体
    var handler: AuthStateDidChangeListenerHandle? //サインインしているユーザ情報を保持するオブジェクト
}

これらはOptionalで定義されていますが、これはサインイン前やサインアウト後などはサインインしているユーザのオブジェクトがnilになるためです。
サインインが完了し、Userオブジェクトにユーザ情報が入った後は、Viewの中でそれらの情報を開示しています。

var body: some View {
    VStack() {
        if let user = self.appState.session.user {
            Text("こんにちは、\(user.email!)") // !で開示して表示
            Button(action: self.signOut) {
              Text("サインアウト")
            }
        }
    }.onAppear(perform: self.loadUserInfo)
}

ここで注目したいのが、オプショナル束縛構文を使って分岐をしている部分です。

if let user = self.appState.session.user {

これを記述することで、ユーザがサインアウトした後にUserオブジェクトがnilになっても問題ありません。
逆にこれを記述しないと、View側はサインアウト時に代入されるnilを受け取り、アプリがクラッシュしてしまいます。

Fatal error: Unexpectedly found nil while unwrapping an Optional value
try Auth.auth().signOut()
self.appState.session.user = nil // サインアウト時にユーザ情報を剥がしている

struct2.png

まとめ

今回はサインイン/サインアウトを例に取り上げましたが、モバイルアプリを作る上で複数のViewが同じ値を参照していることが多々あると思います。
参照しているViewが多ければ多いほど、予期せぬnilを受け取る可能性があり、アプリのクラッシュを引き起こしてしまいますが、Optional型を活用することで、それらを未然に防ぐことができます。
何でもかんでもOptionalにするべきではありませんが、nilの代入が想定されるプロパティや変数に関しては、Optionalを活用しましょう!

参考

iOS で Firebase Authentication を使ってみる
SwiftのOptional型を極める
詳解Swift 第5版 Chapter4 Optional

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

Array(Element)型を理解しよう!

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

Array(Element)型とは?

Array(Element)型を一言でいうと、配列を表す型のことです。

例えば
Array(Element)型は[1,2,3]のように配列リテラルを用いて表現することができます

qiita.rbvar
1.let a = [a,b,c]
2.let b = [1,2,3]

Array(Element)型を用いることで、便利なのが
Array(Element)型の値に対して、要素の更新、追加、結合、削除を行えることです。

それでは一つずつ深堀りしていきましょう!

要素の更新

qiita.rbvar
               //0 1 2 
1.var numbers = [1,2,3]
2.numbers[1] = [4] // 1番目である2の更新を行っている
numbers// [1,4,3]

要素の追加

末尾に要素を追加するにはappend(_:)メソッドを使用します
次の例は、[String]型の"d"を追加していいます

qiita.rbvar
1.let strings  = ["a","b","c"]
2.strings append(d)//["a","b","c","d"]

また、任意の場所に追加することができます。
任意の場所に要素を追加するには、insert(_: at:1)、メソッドを使用します。

次の例では、nsert(_: at:1)***、メソッドをし使用し、2番目に"b"を追加しています。

qiita.rbvar
1.let strings  = ["a","c","d"]
2.strings insert("b",at1)//["a","b","c","d"]

要素の結合

要素の結合は、+演算子でArray(Element)型の値を結合することができます。

qiita.rbvar
1.let strings1 = ["あ","い","う"]//[String型]
2.let c =["え","お"]//[String型]

let result = strings1+strings1//["あ","い","う","え","お"]

要素の削除

要素の削除には、任意の場所を削除するremove(at:)、最後の要素を削除するremoveLast()、全ての要素を削除するremoveAll()メソッドの3つが用意されています。

qiita.rbvar
var strings = ["あ","い","う","え","お"]

strings.remove(at:2)
strings//["あ","い","え","お"]//"う"が削除

strings.removeLast()
strings//["あ","い","え"]//末尾の"お"が削除

strings.removeAll()
strings//()

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

簡単にiOSアプリのアイコンを設定する方法

アイコンの設定

アプリが完成した!と思ったらアイコンの設定をし忘れてた!とやる気を削がれたことはありませんか?この記事では、アプリのアイコンを一瞬で設定する方法をまとめたいと思います。
有名な内容なので、そんなの知ってるよ!と思われた方はそっとブラウザバックをしてください。

アイコンを作ろう!

この記事ではアイコンの作り方自体は書きませんが、僕が普段使っているおすすめのツールを紹介したいと思います。
有料のものばかりになってしまいますが、許して、、、

Sketch
モックアップを作る際やアイコンを作るとき、ロゴを作る時など基本的にSketchを使って作業しています。

Adobe CC
かの有名なAdobe Creative Cloudです。Sketchでは厳しいことなどはイラストレーターやフォトショップを使って作業しています。

それでは本題

ここでようやく本題のアプリのアイコンを一瞬で設定する方法を説明したいと思います。
まず、作成したアプリのアイコンを1024*1024のpngやjpgなどに書き出してください。

こちらのサイトにアクセスして作成した写真を以下の部分にドラックアンドドロップしてください。
スクリーンショット 2020-10-09 13.42.39.png

該当するアプリが対応するプラットフォーム(iPhone、iPadなど)にチェックが入っていることを確認してGenerateを押してダウンロードしてダウンロードされたZipを解凍してください。

解凍できたら、中身がこのようになっていることを確認してください。

スクリーンショット 2020-10-09 13.46.41.png

Xcodeプロジェクトに実際に設定していく

まず、Xcodeプロジェクトを開いて左側にあるファイル一覧からAssets.xcassetsを右クリックしてShow in Finderを選択して下さい。
この時点で一度Xcodeを終了しておくと無難です。

スクリーンショット 2020-10-09 13.48.17.png

このような画面は表示されましたか?

スクリーンショット 2020-10-09 13.49.35.png

このような画面が表示されたら、AppIcon.appiconsetをフォルダーごと削除しましょう!(⌘+Delete)
削除ができたら、先ほどダウンロードしたフォルダーの中にあるAssets.xcassetsの中のAppIcon.appiconsetを今削除したフォルダーがあったAssets.xcassetsにドラック&ドロップしましょう!

スクリーンショット 2020-10-09 13.54.42.png

ここまで完了したら、Xcodeのプロジェクトを再度開いてアイコンが設定されているか確認しましょう!

以上で完了です!お疲れ様でした!

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

Optional(Wrapped)型を理解しよう!

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

Optional(Wrapped)型とは?

Optional(Wrapped)型を一言でいうと、値があるか空かのいずれを表す型です。
基本的に、swiftの変数や定数は基本的にnilを許容しないのですが、そのnilを用いる場合には
Optional(Wrapped)型を使用します!

例えば

qiita.rbvar
1.var n: Int
2.
3.print(n)//エラー

上記のように、存在していない値を出力しようとするとエラーが起こります。
そこで登場するのが、Optional(Wrapped)型です

qiita.rbvar
1.var n: Optional<Int>
2.
3.print(n)//nil

上記のように、Optional(Wrapped)型を使うとエラーにならず、nilが出力がされます。

このように、「『Optional型』でデータをラップしておくと値が存在しない場合は『nil』を返すようになる」という挙動になるためエラーを回避することができます。これが『Optional型』の基本的な使い方になります.

Optional(Wrapped)型のアンラップとは?

Optional(Wrapped)型は値を持っていない可能性があるため、Wrapped型の変数や定数と同じように扱うことができない。
例えば、Int?型どうしの四則演算はエラーになります。

qiita.rbvar
1.let a: Int? = 1
2.let b: Int? = 1
3.a+b//エラー

このエラーを回避するために、アンラップを行います。
アンラップの方法は以下の3つです。

○ オプショナルバインディング
○ ??演算子
○ 強制アンラップ

一つずつ深堀りしていきましょう!

オプショナルバインディングとは?

qiita.rbvar
if let 定数名 = Optional(Wrapped){

//値が存在する場合に実行される文

}

上記の文のように、if-let文を用いてWrapped型の値をもつ場合は{}の文が実行されます。
次の例では、定数Aに値が存在するため、String型の定数aに値が代入され、実行文が実行されます!

qiita.rbvar
1.let A: Optional("1") //Int型
2.if let A {
print(type(of:1))

//実行結果:Int

}

 ??演算子とは?

次の例では、??演算子の左辺にString型の値"a"を持った、String型?の定数optionalStringを右辺にString型の値"b"を指定し、結果として左辺の値"a"を取得しています。

qiita.rbvar
1.let optionalString:String? = "a"
2.if String = optionalString ?? "b"

//実行結果 a

 強制アンラップとは?

強制アンラップは、Optional(Wrapped)型からWrapped型の値を強制的に取り出す方法です。
強制アンラップを行うには、!演算子を使用します。
上記では、Wrapped型の変数や定数と同じように扱うことができないと述べましたが、強制アンラップ使うことによって取り出すことができます。

qiita.rbvar
let a : Int? = 1
let b : Int? = 1
a!+b! = //2
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

複数プロトコル指定つきオプショナル型の変数定義

複数プロトコルを適用したOptional型の定義に戸惑ったのでメモ書きレベルです

複数プロトコルの適用した宣言

class Hoge {
    var fuga: SomeClass & HogeProtocol & FugaProtocol 
}

複数プロトコルの適用したOptionalの宣言

書き方はいろいろあった。

カッコ

class Hoge {
    weak var delegate: (SomeClass & SomeProtocol & FugaProtocol)?
}

シンタックスシュガーなし

class Hoge {
    weak var delegate: Optional<SomeClass & SomeProtocol & FugaProtocol>
}

typealiasの利用。

typealias HogeDelegate = SomeClass & SomeProtocol & FugaProtocol
class Hoge {
    weak var delegate: HogeDelegate?
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ARKit+SceneKit+Metalで光学迷彩①

光学迷彩っぽいことをやってみた

やりたいこと

『GHOST IN THE SHELL/攻殻機動隊』のあれ。

 完成イメージ
demo1.gif

光学迷彩っぽくする方法

光学迷彩にする3Dモデルを透明(=背景色)で出力することで実現する。
このとき、3Dモデルの背景色を法線方向にちょっとずらして取得・出力することで、3Dモデルの形状にそって背景が歪むため、それっぽく見える。

参考にしたサイト:wgld.org

マルチパスレンダリング

ARにてオブジェクトの背景を歪ませるには次のステップが必要(きちんとした方法は他にもたくさんあると思うが一例として)。
①背景(=カメラのキャプチャ+光学迷彩させない3Dモデル)画像を用意する
②光学迷彩させる3Dモデルの法線情報を用意する
③①と②を組み合わせる。
 ・②の光学迷彩対象部分は①の背景を歪めて出力
 ・それ以外の部分は①の背景をそのまま出力

ここで、①②③のいずれもレンダリングを行うが、最終的に画面に出力すのは③の結果である。①②の出力結果は内部的にメモリに保持する。SceneKitでこれを実現するのがSCNTechniqueでレンダリングを複数行い、それらを組み合わせることができる。

このSCNTechniqueについては情報が少なく、なんだかんだで SCNTechnique | Apple Developer Document に情報が一番ある(と思う)。

今回試したマルチパスレンダリングの定義がこちら。

technique.json
{
    "targets" : {
        "color_scene" : { "type" : "color" },
        "depth_scene" : { "type" : "depth" },
        "color_node"  : { "type" : "color" },
        "depth_node"  : { "type" : "depth" }
    },
    "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_mix" : {
            "draw" : "DRAW_QUAD",
            "inputs" : {
                "colorScene" : "color_scene",
                "depthScene" : "depth_scene",
                "colorNode"  : "color_node",
                "depthNode"  : "depth_node",
            },
            "metalVertexShader" : "mix_vertex",
            "metalFragmentShader" : "mix_fragment",
            "outputs" : {
                "color" : "COLOR"
            },
            "colorStates" : {
                "clear" : "true"
            }
        }
    },
    "sequence" : [
        "pass_scene",
        "pass_node",
        "pass_mix"
    ]
}

少しづつ見ていきましょう。

①背景(=カメラのキャプチャ+光学迷彩させない3Dモデル)画像を用意する

        "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_scene" が ①背景の描画の定義。
"draw" : "DRAW_SCENE" はシーン全体の描画指示で、出力結果を"outputs"にある、"color" : "color_scene""depth" : "depth_scene" に出力するようにしている。後述するが、"color" : "COLOR" とすると、最終的に画面に出力される。
で、"color_scene"は何かというと、色情報を保存しておくバッファ。ここに保存しておくことで、後々、この情報を使った処理が可能になる。
次に、"depth_scene"は何かというと、描画した3Dオブジェクトの奥行きの情報を保存しておくバッファ。後で、光学迷彩対象の3Dモデルと画像を組み合わせるときに、背景の3Dモデルとどちらが手前にあるのか判定するのに使用する。
あと、光学迷彩対象は描画をしないように "excludeCategoryMask" : 2 としている。光学迷彩対象の SCNNodecategoryBitMask に2を設定しておく。

②光学迷彩させる3Dモデルの法線情報を用意する

        "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_node" が ②光学迷彩させる3Dモデルの法線情報の描画の定義。
"draw" : "DRAW_NODE" は特定のノードを描画するという意味で、ここでは"includeCategoryMask"に指定した Category Bit Maskを持つノードを対象にしている(逆に、光学迷彩対象としない3Dモデルは includeCategoryMask のビットとは異なる値にする)。
"outputs""color_node""depth_node" で、それぞれノードの色と奥行きの情報を格納するバッファ。
"node_vertex""node_fragment" はノードの法線の情報を色情報として "color_node" に出力するためのシェーダー(後述)。

③①と②を組み合わせる。

        "pass_mix" : {
            "draw" : "DRAW_QUAD",
            "inputs" : {
                "colorScene" : "color_scene",
                "depthScene" : "depth_scene",
                "colorNode"  : "color_node",
                "depthNode"  : "depth_node",
            },
            "metalVertexShader" : "mix_vertex",
            "metalFragmentShader" : "mix_fragment",
            "outputs" : {
                "color" : "COLOR"
            },
            "colorStates" : {
                "clear" : "true"
            }
        }

"pass_mix" が ①と②を組み合わせる描画の定義。
"draw" : "DRAW_QUAD" は他で作られた色や奥行きの情報を処理するときに使う。"inputs"には前述した描画定義の出力バッファを指定しており、これをインプットとして、"mix_vertex""mix_fragment" で加工し、"outputs" : { "color" : "COLOR"} を指定することで最終的な画像を出力させる。

シェーダー

次にSCNTechniqueで指定したシェーダーについて。

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

// ノード用頂点シェーダー
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]],
                            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)]])
{
    float ds = depthScene.sample(s, vert.uv);    // シーン全体描画時のデプス
    float dn = depthNode.sample(s, vert.uv);     // ノード描画時のデプス

    float4 fragment_color;
    if (dn > ds) {
        // 光学迷彩対象のオブジェクトがシーンで描画したオブジェクトの手前にあるので、光学迷彩効果を出す
        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);
}

ポイントになるところは3箇所。

1. 光学迷彩対象のノードの法線情報の出力

// ノード用フラグメントシェーダー
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);        // 法線を色情報として出力。この情報で光学迷彩対象の背景を歪める
}

ノードのフラグメントシェーダーで、法線情報(xyz)をそのまま色(rgb)情報として出力している。

2.光学迷彩対象のオブジェクトと対象外のオブジェクトとの前後比較

// シーン全体とノード法線の合成用フラグメントシェーダー
fragment half4 mix_fragment(MixColorInOut vert [[stage_in]],
    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)]])
{
    float ds = depthScene.sample(s, vert.uv);    // シーン全体描画時のデプス
    float dn = depthNode.sample(s, vert.uv);     // ノード描画時のデプス

    float4 fragment_color;
    if (dn > ds) {
        // 光学迷彩対象のオブジェクトがシーンで描画したオブジェクトの手前にあるので、光学迷彩効果を出す
    } else {
        // 光学迷彩対象のオブジェクトがシーンで描画したオブジェクトの奥にあるので、シーン側の色をそのまま採用
    }

シーン全体を描画したときの奥行き情報dsと光学迷彩対象オブジェクトの奥行き情報dnを比較して、dnが大きければ、背景を歪めることで光学迷彩効果を出し、小さければ背景色をそのまま表示する。
ちょっとわかりづらいが↓を見ると、光学迷彩対象のレッサーパンダが奥にあるときには手前のレッサーパンダは歪んでいないことがわかる。
demo2.gif

3.背景を歪める

// 光学迷彩対象のオブジェクトがシーンで描画したオブジェクトの手前にあるので、光学迷彩効果を出す
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;

今回の光学迷彩は背景を透明に出力しつつも、背景の色を3Dモデルの法線の方向の離れた場所から取得して歪んだ感じを出している。
「②光学迷彩させる3Dモデルの法線情報を用意する」で出力した法線情報を使って、背景画像の座標を移動し、その場所の色を3Dモデル上の色としている。

SCNTechniqueのセットアップ

SCNTechnique をセットアップするSwift側のコードはこんな感じ。

ViewController.swift
import ARKit
import SceneKit

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!

    private var rootNode: SCNNode!

    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

        // 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(_: 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)
        }
    }

    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
    }
}

注意事項

いろいろ試していたら気が付いたのだが、ピープルオクルージョンを有効にすると SCNTechnique が無効化される!!
(ピープルオクルージョン自体、マルチパスレンダリングっぽい仕組みなので、そちらが優先されるということ?)

let configuration = ARWorldTrackingConfiguration()
configuration.frameSemantics = [.personSegmentation]  // これを指定するとSCNTechniqueは動かない!

TODO

  • 迷彩っぷりが綺麗すぎてそれっぽくない(つるつるな)ので、ノイズをのせる
  • 顔とかジオメトリの一部は迷彩にしない https://wirelesswire.jp/2016/02/50064/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む