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 value
try Auth.auth().signOut()
self.appState.session.user = nil // サインアウト時にユーザ情報を剥がしている
まとめ
今回はサインイン/サインアウトを例に取り上げましたが、モバイルアプリを作る上で複数のViewが同じ値を参照していることが多々あると思います。
参照しているViewが多ければ多いほど、予期せぬnilを受け取る可能性があり、アプリのクラッシュを引き起こしてしまいますが、Optional型を活用することで、それらを未然に防ぐことができます。
何でもかんでもOptionalにするべきではありませんが、nilの代入が想定されるプロパティや変数に関しては、Optionalを活用しましょう!
参考
iOS で Firebase Authentication を使ってみる
SwiftのOptional型を極める
詳解Swift 第5版 Chapter4 Optional