【Swift】エラーハンドリングと文字列
iOS開発でエラーハンドリング時のエラーメッセージの実装が間違えているような気がしたのでその調査メモです.
恥ずかしながら今まではCustomStringConvertibleやコンバータークラスを作成していました.
間違い等ありましたら指摘してくださると嬉しいです.
ErrorとNSError
Foundation/UIKit等を利用していると稀にNSErrorというものを見かけます.
SwiftのErrorとの違いは次の点が違います.
Error
Errorはdo-catch構文やResult型で補足することができる抽象型のプロトコルです.
Swiftで定義されているErrorプロトコルに準拠したオブジェクトを例外として投げることが可能で,クラスや共用型含む列挙型等に準拠させることができます.
NSError
Foundationで定義されたクラスで,エラーの発生とそのエラー情報を伝える使い方をします.
クラスはNSObjectを継承したクラスですが,SwiftのErrorプロトコルにも準拠しているためErrorと同様の使い方ができます.
以上がErrorとNSErrorの違いですが,一言で表すと
Errorはエラーの発生を知らせる事が可能.NSErrorはエラーの発生とその情報もセットで知らせることが可能.
という認識をしておけば大丈夫です.
NSError エラー情報
NSErrorはエラーの情報も格納できると上述しました.
エラーの情報は次のプロパティで取得することが可能です.
class NSError: NSObject { /// ローカライズされたエラーを説明する文字列 var localizedDescription: String { get } /// ローカライズされたアラートの選択肢として表示する文字列 var localizedRecoveryOptions: [String]? { get } /// ローカライズされたエラーへの対処方法を表す文字列 var localizedRecoverySuggestion: String? { get } /// ローカライズされたエラーの発生原因を表す文字列 var localizedFailureReason: String? { get } }
ローカライズされているということは,UIへの表示を前提にされたものだということが推測できます.
API呼び出しなどではなくNSErrorのインスタンスを生成したい場合はuserInfoにローカライズされた文字列を格納します.
let nsError = NSError(domain: "nserror", code: 1, userInfo: [ NSLocalizedDescriptionKey: "localized description", NSLocalizedRecoveryOptionsErrorKey: ["localized recovery options"], NSLocalizedRecoverySuggestionErrorKey: "localized recovery suggestion", NSLocalizedFailureReasonErrorKey: "localized failure reason" ]) print(nsError.localizedDescription) print(nsError.localizedRecoveryOptions) print(nsError.localizedRecoverySuggestion) print(nsError.localizedFailureReason)
macOS向けネイティブアプリ開発に用いられるCocoaでは アラートを表示するAPIの引数にNSErrorを渡すことでラベル等に適切に表示してくれるものもあります.
let nsError = ... NSAlert(error: nsError).runModal()
LocalizedError
FoundationのNSErrorのようにエラー情報を保持可能で,SwiftのプロトコルとしてErrorを利用したい場合はLocalizedErrorというFoundationで定義されたプロトコルが利用可能です.
LocalizedErrorは次のプロパティが定義された,Errorを継承したプロトコルです.
public protocol LocalizedError : Error { var errorDescription: String? { get } var failureReason: String? { get } var recoverySuggestion: String? { get } var helpAnchor: String? { get } }
先程の例として載せたNSErrorのプロパティと雰囲気が似ていると思います.
実際にプロトコルに準拠させた列挙型を定義して実際に利用してみると次のようになります.
enum MyError: LocalizedError { case a case b var errorDescription: String? { "error description" } var failureReason: String? { "failure reason" } var recoverySuggestion: String? { "recovery suggestion" } var helpAnchor: String? { nil } } do { throw MyError.b } catch { // MyError.errorDescription let myError = error as! MyError print(myError.errorDescription) // Error.localizedDescription print(error.localizedDescription) } // 両方とも"error description"が出力
定義したMyErrorのlocalizedDescription
はどのケースでも"error description"としています.
例を見ると引数の異なるprint()
が2回呼び出されていますが,その出力は両方とも同じです.
FoundationのextensionでErrorプロトコルにlocalizedDescription
というプロパティが追加されており,エラーの実装がNSErrorでもErrorでも意図した文字列(ローカライズされたエラーの説明)を返すという動作をします.
localizedDescription
がNSErrorとErrorの違いを吸収してくれるので,利用者はキャスト等の処理を挟まなくて済みます.
NSErrorと同じくCocoaAPIのAlertにMyError.bを渡してあげると,意図した形でアラートが表示されたことが確認できました.
NSAlert(error: MyError.a).runModal()
まとめ
Swiftのエラーハンドリングで型を定義する際にはLocalizedErrorを使いましょう.
エラーメッセージはError.localizedDescription
で定義しておくと,NSErrorとの違いを吸収してくれるので積極的に利用したいです.
参照
【Swift】 Combine 基礎
はじめに
Combineは2019年に発表されたフレームワークですが「アプリに組み込めるようになったのは最近」という人は多いのではないでしょうか.
私もそのうちの一人なのですが,備忘録としてまとめておきます.
間違っている箇所ありましたらご指摘いただけると助かります.
Combineとは
CombineはApple WWDC19で発表されたフレームワークです.
UIKitは処理の結果を元に各オブジェクト内の状態を変化させていくような手続き型プログラミングが主流でした.
SwiftUIを始めとした,オブジェクトが状態の変化を検知して適切な処理へ更新するような宣言型プログラミングスタイルを実現させるためのフレームワークとして登場しました.
iOS 13.0.macOS 10.15から利用可能です.
登場人物
Combineのコアとなるのは以下の3つのプロトコルです.
- Publisher
- Subscriber
- Operator
Publisherとは
Publisherは"発行者"と訳されます.
次に説明するSubscriberに値/エラーを通知することができます.
1対1の関係のDelegateパターンとは異なり,1つのPublisherに複数のSubscriberが登録(subscribe)することが可能です.この特徴が宣言型プログラミングスタイルを実現可能としています.
Subscriberとは
Subscriberは"登録者"と訳され,Publisherからの通知を受け取る役割があります.
Operatorとは
OperatorはPublisherとSubscriberの間に加入し,Subscriberが通知した値を加工しSubscriberへと渡す役割があります.
値の加工とはIntからLongへのキャストや,不要な値の破棄なども行うことができます.
Publisher
WWDC19よりPublisherとはプロトコルで以下のように定義されています.
protocol Publisher { associatedtype Output associatedtype Failure: Error func subscribe<S: Subscriber>(_ subsctiber: S) where S.Input == Output, S.Failure == Failure }
通知を行うために出力するための型,異常を通知するError型を指定するassociatedtype
.
Subscriberが通知の送付先として登録するためのsubscribe(_:Subscriber)
があります.
Subscriber
WWDC19よりSubscriberもプロトコルで以下のように定義されています.
protocol Subscriber { associatedtype Input associatedtype Failure: Error func receive(subscription: Subscription) func receive(_ input: Input) -> Subscribers.Demand func receive(completion: Subscribers.Completion<Failure>) }
通知の入力として受け取る型と異常時のError型を指定します.
Publisherに登録する場合はPublisherのOutputとSubscriberのInputの型は一致している必要があります.
静的型付けを行うことによって実行時のキャストエラーを防ぎ,外部APIとして公開する場合も扱いやすくなります.
Combine組込みSubscriberとしてAssign
とSink
が用意されています.
AssignはPublisherから受け取った値を,指定した変数に代入するSubscriber.
SinkはPublisherから値を受け取ったら,初期化時に指定したクロージャを呼び出すSubscriberです.
Operator
Operatorはプロトコルではありません.Publisherからの出力を受け取り,値のキャスト等を行いSubscriberへと出力するものです.
Combineの組込みOperatorだけでもかなりの数のものが用意されており,様々なことが実現可能です.
Operatorの一つとして提供されているMapは以下のような定義です.
extension Publishers { struct Map<Upstream: Publisher, Output>: Publisher { typealias Failure = Upstream.Failure let upstream: Upstream let transform: (Upstream.Output) -> Output } }
組込みOperatorはPublishersという名前空間に定義されています.詳しく知りたい方はドキュメントを参照してください.
Subject
SubjectはPublisherを継承したプロトコルです.
Publisherの実装に加えてsend()
メソッドの実装を要求します.
組込みSubjectとしてCurrentValueSubjectとPassthroughSubjectが提供されており,既存の命令型コードと共存させる場合に力を発揮します.
ライフサイクル
PublisherとSubscriberは以下の流れで処理を行います.
Combineを利用するにあたってOperatorは必須ではありませんのでここでは割愛します.
- Subscriberは通知を受け取りたいPublisherの
subscribe(_:Subscriber)
に自身を引数として渡す. - Publisherは登録要求で渡されたSubscriberの
receive(_:Subscription)
を呼び出して接続を確率します. - Subscriberは
receive(_:Subscription)
で渡されたSubscriptionオブジェクトのrequest(_:Demand)
メソッドを用いてN回もしくは無限回の通知を受け取ることをPublisherへと伝える. - Publisherは通知イベントをSubscriberへ送るために
Subscriber.receive(_:Input)
を呼び出します.このイベントは接続が確率されている間は複数回送ることが可能です. - Subscriberは
.cancel()
メソッドを用いてPublisherとの接続をキャンセルすることが可能です..cancel()
呼び出し以降はPublisherからの通知はされなくなります. - Publisherは
Subscriber.receive(_:Completion)
を呼び出し,以降の通知は発生しないことを伝えることができます.
実践 Publisher Subscriber
Foundationの拡張でCombineを利用することができます.
今回はKVO(KeyValueObserve)をCombineで利用してみます.
import Foundation import Combine class User: NSObject { @objc dynamic var age: Int = 0 } let user = User() // ① User.ageの値の変更を通知するPublisher let agePublisher = user.publisher(for: \.age, options: .new) // ② 通知を受け取り,都度クロージャを呼び出す組込みSubscriber let ageSubscriber = Subscribers.Sink<Int, Never>(receiveCompletion: { _ in // 完了/失敗時に呼ばれる }, receiveValue: { value in // 値が更新されたときに呼ばれる print(value) }) // ③ Publisherと接続する agePublisher.subscribe(ageSubscriber) user.age = 40 user.age = 30 // ④ キャンセル ageSubscriber.cancel() // ⑤ この変更は通知されない user.age = 10
上記のコードを実行してみるとコンソールには
40 30
が出力されます.
④ キャンセル以降のイベントは通知されないことが見て取れます.
また,④の.cancel()
を実行した場合は②のreceiveCompletion
のクロージャは実行されません.
強引ですが,以下のように完了通知を呼び出してあげるとreceiveCompletion
が呼び出されて.cancel()
と同じ動きをすることが確認できました.
〜 // ③ Publisherと接続する agePublisher.subscribe(ageSubscriber) user.age = 40 user.age = 30 ageSubscriber.receive(completion: .finished)
Chained Publisher
Combineの例としてよく見る書き方.
Chained Publisherと呼ばれており以下のような書き方で,先程の例と同じ動きをします.
class User: NSObject { @objc dynamic var age: Int = 0 } let user = User() let cancellable = user.publisher( for: \.age, options: .new).sink(receiveCompletion: { _ in // 完了/失敗時に呼ばれる }, receiveValue: { value in // 値が更新されたときに呼ばれる print(value) }) user.age = 40 user.age = 30 cancellable.cancel() user.age = 10
Publisher.sink
メソッドで先程の例と同じ動きをしますが,返り値として渡されるのはAnyCancellableという型のオブジェクトです.
AnyCancellableはSubscriberをラップし.cancel()
メソッドのみを呼び出し可能としたものです.
ラップされたSubscriberは他のメソッドを呼び出される考慮をしなくて良くなる(先程の強引にreceive(_:Completion)
を呼び出す等)ので外部APIとして公開する場合に有用です.
Publisher.sink
で返ってくるオブジェクトを保持しない場合はラップされているSubscriberも破棄されるため意図した動作をしませんので注意が必要です.
実践 Subject
組込みSubjectを用いて,Subjectをsubscribeする例を示します.
組込みSubjectはCurrentValueSubjectとPassthroughSubjectの2つがあります.2つの違いは
CurrentValueSubjectはSubscriberからのsubscribe時に,Subjectが最後に受け取った(保持している)値を渡します.
PassthroughSubjectはsubscribeが完了した後に,send()
にて受け取った値をSubscriberに渡すという動作になります.
let subject = CurrentValueSubject<String, Never>("hello") let cancellable = subject.sink(receiveValue: { str in print(str) }) subject.send("world")
コンソールの出力は
hello world
となります.
今までの例ではsubscribeをするときにエラー時の処理も指定していましたが,Publisher/SubjectのFailureがNever(エラーが発生しないことを表す)の場合は,subscribe処理の(receiveCompletion:)
を省略することが可能です.
おわりに
Combineというフレームワークを調べてみると,基本的なコンセプトとしては小さなものでした.
CombineはSwiftUIを支えている技術の一つでもあるため,その基礎を抑えておいて損はないかと思います.
使いこなせれば良い道具となりそうなので,これからも活用方法を探っていきたいと思いました.
参考
Combine | Apple Developer Documentation
Introducing Combine - WWDC19 - Videos - Apple Developer
Combine in Practice - WWDC19 - Videos - Apple Developer
Using Combine