- 投稿日:2020-10-09T22:45:25+09:00
【ReactNative】SwiftでNativeModuleを書く
はじめに
iOSのネイティブAPIにアクセスするために、ReactNatveには
Native Module
というAPIが用意されています。
今回は、Swift
でネイティブモジュールを実装する方法をまとめます。ReactNativeプロジェクトの作成
まだプロジェクトがない場合は、作成します。
npx react-native init myAppXCodeでSwiftファイルを作成する
ios/myApp.xcodeproj
にあるプロジェクトをXCodeで開きます。
NativeModules
フォルダを作成します。(任意)
NativeModules
フォルダ内に.swift
ファイルを作成します。
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.swiftimport Foundation@objc(Counter) class Counter: NSObject { private var count = 0 @objc func increment() { count += 1 print("count is \(count)") } }メソッドをReactNativeから扱えるようにする
実装したメソッドをReactNativeから扱えるように
Objective-C
のファイルを作成します。先ほど作成したSwiftファイルと同じ名前で、同ディレクトリに作成します。
作成したファイルには以下を追記します。
Counter.m@interface RCT_EXTERN_MODULE(Counter, NSObject) RCT_EXTERN_METHOD(increment) @endRCT_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への対処
以下のような警告が出ることがあります。
これは、モジュールの処理をメインスレッドで行うか、バックグラウンドで行うか設定しなさいという警告です。
以下のように記述し、警告を消すことができます。@objc static func requiresMainQueueSetup() -> Bool { return true }
true
を返す: メインスレッドで処理false
を返す: バックグラウンドで処理まとめ
ネイティブモジュールをSwiftで書く方法をまとめました。
- 投稿日:2020-10-09T22:09:39+09:00
GroupedのUITableViewで一番上の余白を消したい(swift)
はじめに
下記の画像のように
style
がgrouped
のUITableView
で条件によって一番上の余白の表示・非表示を切り替えたいときに試行錯誤したのでやり方をメモ。
余白表示 余白非表示 今回は画像のようにヘッダー表示時は余白をなくして、ヘッダー非表示の場合は余白ありにしたかった。。。
だめだったパターン
とりあえずだめだったパターン。。。下記のように
isHeaderShown
がtrue
のときにセクション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 } }結果
初回表示だけいけてるけど2回目以降がおかしい。。。
いけてそうなパターン1
上の方法 + 下記のようにテーブル更新時に
tableHeaderView
をいじってみました。tableView.tableHeaderView = isHeaderShown ? UIView() : nil tableView.reloadData()結果
いけてそう
いけてそうなパターン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と同じだったのでたぶんいけてそう
おわりに
こんなレイアウトにしたくなるのはレアだと思いますがどなたかの参考になれば幸いです。他にいい方法ご存知でしたらぜひ教えて下さい
今後は
UITableView
よりUICollectionView
使っていった方がいいよって言うのも聞くしもうあんまりテーブルをごちゃごちゃするのはよくないのかもしれない。。。
- 投稿日:2020-10-09T20:14:18+09:00
SwiftのOptionalを存分に活用してみる
Optionalについて軽くおさらい
Optionalとは
Swiftではオプショナル型という特殊な型が用意されています。
オプショナル型とは、通常は持っている型とは別に例外的にnilとなってしまう値に対しても定義できる型のことを示します。例えば、通常は文字列を持っている変数
optionalName
が、ある特殊なケースにおいてnilが代入される場合がある時、変数optionalName
はString?
と表現することが出来ます。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を使います。これについての詳しい説明は割愛します。サンプルコード
まずは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 valuetry Auth.auth().signOut() self.appState.session.user = nil // サインアウト時にユーザ情報を剥がしているまとめ
今回はサインイン/サインアウトを例に取り上げましたが、モバイルアプリを作る上で複数のViewが同じ値を参照していることが多々あると思います。
参照しているViewが多ければ多いほど、予期せぬnilを受け取る可能性があり、アプリのクラッシュを引き起こしてしまいますが、Optional型を活用することで、それらを未然に防ぐことができます。
何でもかんでもOptionalにするべきではありませんが、nilの代入が想定されるプロパティや変数に関しては、Optionalを活用しましょう!参考
iOS で Firebase Authentication を使ってみる
SwiftのOptional型を極める
詳解Swift 第5版 Chapter4 Optional
- 投稿日:2020-10-09T20:14:18+09:00
SwiftのOptionalを活用してログイン機能を作る
Optionalについて軽くおさらい
Optionalとは
Swiftではオプショナル型という特殊な型が用意されています。
オプショナル型とは、通常は持っている型とは別に例外的にnilとなってしまう値に対しても定義できる型のことを示します。例えば、通常は文字列を持っている変数
optionalName
が、ある特殊なケースにおいてnilが代入される場合がある時、変数optionalName
はString?
と表現することが出来ます。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を使います。これについての詳しい説明は割愛します。サンプルコード
まずは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 valuetry Auth.auth().signOut() self.appState.session.user = nil // サインアウト時にユーザ情報を剥がしているまとめ
今回はサインイン/サインアウトを例に取り上げましたが、モバイルアプリを作る上で複数のViewが同じ値を参照していることが多々あると思います。
参照しているViewが多ければ多いほど、予期せぬnilを受け取る可能性があり、アプリのクラッシュを引き起こしてしまいますが、Optional型を活用することで、それらを未然に防ぐことができます。
何でもかんでもOptionalにするべきではありませんが、nilの代入が想定されるプロパティや変数に関しては、Optionalを活用しましょう!参考
iOS で Firebase Authentication を使ってみる
SwiftのOptional型を極める
詳解Swift 第5版 Chapter4 Optional
- 投稿日:2020-10-09T20:14:18+09:00
SwiftのOptional、何が嬉しいのか
Optionalとは
Swiftではオプショナル型という特殊な型が用意されています。
オプショナル型とは、通常は持っている型とは別に例外的にnilとなってしまう値に対しても定義できる型のことを示します。例えば、通常は文字列を持っている変数
optionalName
が、ある特殊なケースにおいてnilが代入される場合がある時、変数optionalName
はString?
と表現することが出来ます。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を使います。これについての詳しい説明は割愛します。サンプルコード
まずは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 valuetry Auth.auth().signOut() self.appState.session.user = nil // サインアウト時にユーザ情報を剥がしているまとめ
今回はサインイン/サインアウトを例に取り上げましたが、モバイルアプリを作る上で複数のViewが同じ値を参照していることが多々あると思います。
参照しているViewが多ければ多いほど、予期せぬnilを受け取る可能性があり、アプリのクラッシュを引き起こしてしまいますが、Optional型を活用することで、それらを未然に防ぐことができます。
何でもかんでもOptionalにするべきではありませんが、nilの代入が想定されるプロパティや変数に関しては、Optionalを活用しましょう!参考
iOS で Firebase Authentication を使ってみる
SwiftのOptional型を極める
詳解Swift 第5版 Chapter4 Optional
- 投稿日:2020-10-09T15:10:23+09:00
Array(Element)型を理解しよう!
Array(Element)型について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承くださいArray(Element)型とは?
Array(Element)型を一言でいうと、配列を表す型のことです。
例えば
Array(Element)型は[1,2,3]のように配列リテラルを用いて表現することができますqiita.rbvar1.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.rbvar1.let strings = ["a","b","c"] 2.strings append(d)//["a","b","c","d"]また、任意の場所に追加することができます。
任意の場所に要素を追加するには、insert(_: at:1)、メソッドを使用します。次の例では、nsert(_: at:1)***、メソッドをし使用し、2番目に"b"を追加しています。
qiita.rbvar1.let strings = ["a","c","d"] 2.strings insert("b",at1)//["a","b","c","d"]要素の結合
要素の結合は、+演算子でArray(Element)型の値を結合することができます。
qiita.rbvar1.let strings1 = ["あ","い","う"]//[String型] 2.let c =["え","お"]//[String型] let result = strings1+strings1//["あ","い","う","え","お"]要素の削除
要素の削除には、任意の場所を削除するremove(at:)、最後の要素を削除するremoveLast()、全ての要素を削除するremoveAll()メソッドの3つが用意されています。
qiita.rbvarvar strings = ["あ","い","う","え","お"] strings.remove(at:2) strings//["あ","い","え","お"]//"う"が削除 strings.removeLast() strings//["あ","い","え"]//末尾の"お"が削除 strings.removeAll() strings//()
- 投稿日:2020-10-09T13:56:36+09:00
簡単にiOSアプリのアイコンを設定する方法
アイコンの設定
アプリが完成した!と思ったらアイコンの設定をし忘れてた!とやる気を削がれたことはありませんか?この記事では、アプリのアイコンを一瞬で設定する方法をまとめたいと思います。
有名な内容なので、そんなの知ってるよ!と思われた方はそっとブラウザバックをしてください。アイコンを作ろう!
この記事ではアイコンの作り方自体は書きませんが、僕が普段使っているおすすめのツールを紹介したいと思います。
有料のものばかりになってしまいますが、許して、、、Sketch
モックアップを作る際やアイコンを作るとき、ロゴを作る時など基本的にSketchを使って作業しています。Adobe CC
かの有名なAdobe Creative Cloudです。Sketchでは厳しいことなどはイラストレーターやフォトショップを使って作業しています。それでは本題
ここでようやく本題のアプリのアイコンを一瞬で設定する方法を説明したいと思います。
まず、作成したアプリのアイコンを1024*1024のpngやjpgなどに書き出してください。こちらのサイトにアクセスして作成した写真を以下の部分にドラックアンドドロップしてください。
該当するアプリが対応するプラットフォーム(iPhone、iPadなど)にチェックが入っていることを確認して
Generate
を押してダウンロードしてダウンロードされたZipを解凍してください。解凍できたら、中身がこのようになっていることを確認してください。
Xcodeプロジェクトに実際に設定していく
まず、Xcodeプロジェクトを開いて左側にあるファイル一覧から
Assets.xcassets
を右クリックしてShow in Finder
を選択して下さい。
この時点で一度Xcodeを終了しておくと無難です。このような画面は表示されましたか?
このような画面が表示されたら、
AppIcon.appiconset
をフォルダーごと削除しましょう!(⌘+Delete)
削除ができたら、先ほどダウンロードしたフォルダーの中にあるAssets.xcassets
の中のAppIcon.appiconset
を今削除したフォルダーがあったAssets.xcassets
にドラック&ドロップしましょう!ここまで完了したら、Xcodeのプロジェクトを再度開いてアイコンが設定されているか確認しましょう!
以上で完了です!お疲れ様でした!
- 投稿日:2020-10-09T12:51:39+09:00
Optional(Wrapped)型を理解しよう!
Optional型について学習したので、アウトプットしていきます
※以下の内容は、学習内容のアウトプット用のため、誤りがある場合があります。予めご了承くださいOptional(Wrapped)型とは?
Optional(Wrapped)型を一言でいうと、値があるか空かのいずれを表す型です。
基本的に、swiftの変数や定数は基本的にnilを許容しないのですが、そのnilを用いる場合には
Optional(Wrapped)型を使用します!例えば
qiita.rbvar1.var n: Int 2. 3.print(n)//エラー上記のように、存在していない値を出力しようとするとエラーが起こります。
そこで登場するのが、Optional(Wrapped)型ですqiita.rbvar1.var n: Optional<Int> 2. 3.print(n)//nil上記のように、Optional(Wrapped)型を使うとエラーにならず、nilが出力がされます。
このように、「『Optional型』でデータをラップしておくと値が存在しない場合は『nil』を返すようになる」という挙動になるためエラーを回避することができます。これが『Optional型』の基本的な使い方になります.
Optional(Wrapped)型のアンラップとは?
Optional(Wrapped)型は値を持っていない可能性があるため、Wrapped型の変数や定数と同じように扱うことができない。
例えば、Int?型どうしの四則演算はエラーになります。qiita.rbvar1.let a: Int? = 1 2.let b: Int? = 1 3.a+b//エラーこのエラーを回避するために、アンラップを行います。
アンラップの方法は以下の3つです。○ オプショナルバインディング
○ ??演算子
○ 強制アンラップ一つずつ深堀りしていきましょう!
オプショナルバインディングとは?
qiita.rbvarif let 定数名 = Optional(Wrapped)型{ //値が存在する場合に実行される文 }上記の文のように、if-let文を用いてWrapped型の値をもつ場合は{}の文が実行されます。
次の例では、定数Aに値が存在するため、String型の定数aに値が代入され、実行文が実行されます!qiita.rbvar1.let A: Optional("1") //Int型 2.if let A { print(type(of:1)) //実行結果:Int }??演算子とは?
次の例では、??演算子の左辺にString型の値"a"を持った、String型?の定数optionalStringを右辺にString型の値"b"を指定し、結果として左辺の値"a"を取得しています。
qiita.rbvar1.let optionalString:String? = "a" 2.if String = optionalString ?? "b" //実行結果 a強制アンラップとは?
強制アンラップは、Optional(Wrapped)型からWrapped型の値を強制的に取り出す方法です。
強制アンラップを行うには、!演算子を使用します。
上記では、Wrapped型の変数や定数と同じように扱うことができないと述べましたが、強制アンラップ使うことによって取り出すことができます。qiita.rbvarlet a : Int? = 1 let b : Int? = 1 a!+b! = //2
- 投稿日:2020-10-09T10:01:56+09:00
複数プロトコル指定つきオプショナル型の変数定義
複数プロトコルを適用した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? }
- 投稿日:2020-10-09T08:48:26+09:00
ARKit+SceneKit+Metalで光学迷彩①
光学迷彩っぽいことをやってみた
やりたいこと
『GHOST IN THE SHELL/攻殻機動隊』のあれ。
光学迷彩っぽくする方法
光学迷彩にする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
としている。光学迷彩対象のSCNNode
のcategoryBitMask
に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
が大きければ、背景を歪めることで光学迷彩効果を出し、小さければ背景色をそのまま表示する。
ちょっとわかりづらいが↓を見ると、光学迷彩対象のレッサーパンダが奥にあるときには手前のレッサーパンダは歪んでいないことがわかる。
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.swiftimport 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/