search
LoginSignup
2

More than 1 year has passed since last update.

posted at

updated at

[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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2