[Swift][SwiftUI] Result 活用のすすめ

SwiftUI

     
⌛️ 3 min.
Swift が標準的に用意している Result の使い方を改めて説明します。

Result とは何か?

Result は、実行結果について、結果の状態(成功/失敗)と 結果(計算結果)をまとめて扱うことができる enum です。

Swift の enum は、その中に値を保つことができるので、結果状態だけではなく、結果の値を保持することができます。

Result<Success,Error>とは何か

Result<Success, Error> と書くと、正常実行された際の結果 Success と エラーが発生した時の Error を保持するような Result という意味になります。

結果を保持するので、実行結果の状態に応じて、計算結果、エラー情報を取得することができます。

具体的には、Result<Int, Error>という方の Result に対して、以下のような switch 文を使って、結果をチェックすることができます。

Result の switch


testFunc(value: 1) { result in
  switch result {
    case .success(let value):      // value は、Int 型
      print(value)
    case .failure(let error):      // error は、Error 型
      print(error)
  }
}

Result を返す側では、以下のような書き方になります。

Result 生成コード


let result: Result
if value == 1 {
  result = Result.success(value)    // .success を返すときには、結果の値をセットする(この場合は、Int)
} else {
  result = Result.failure(.MoreThanOne)   // .failure を返すときには、Error の値をセットする(この場合は、MyCustomStringConvertibleError)
}

Result を使う利点とは?

実行結果状態だけではなく、結果も合わせて保持できることが1つ目です。

これまでの非同期処理では、引数に正常実行結果と合わせて、エラー情報を渡されて、その2つをチェックすることが多かったと思いますが、この情報を合わせた1つの Result が渡されることで、不整合を防ぎやすくなります。

また、Result の実装に、enum が使用されていることで、タイプチェックしやすくなるとともに、switch 等で判定するときにモレヌケが防止できます。

Playground で使ってみる

Playground 上で以下のコードを実行すると、感じがつかめる気がします。

example code


import Foundation
import Combine

enum MyCustomStringConvertibleError: String, Error, CustomStringConvertible {
  case NoError = "No Error"
  case NotOne = "invalid Value(not 1)"

  var description: String {
    return rawValue
  }
}

func testFunc(value: Int, completion: (Result) -> Void) {
  if value == 1 {
    completion(.success(value))    // 計算成功として、.success と合わせて値を返す
  } else {
    completion(.failure(.NotOne))  // エラー発生で、.failure と合わせて、エラー詳細を返す
  }
}

testFunc(value: 1) { result in
  switch result {
    case .success(let value):
      print(value)
    case .failure(let error):
      print(error)
  }
}
// ->    1 が表示される

testFunc(value: 2) { result in
  switch result {
    case .success(let value):
      print(value)
    case .failure(let error):
      print(error)
  }
}
// ->    invalid Value(not1) が表示される

Result 応用編:非同期処理 の UI に使うケース

UI として、処理がうまくいったとき と うまくいかなかった時で 表示を切り替えることはよくあります。

Result で結果を保持することで、Result の .success, .failure の状態を表示の切り替えに使うことができます。

さらには、非同期の処理が使われていると、処理が正常終了した、処理が異常終了した 以外に、計算途中という状態も持ち得ます。

Result を ?(optional) という形で保持することで、計算途中という状態を持たせることも可能となります。

SwiftUI での Result 使用例


//
//  ContentView.swift
//  Result
//
//  Created by Tomoaki Yagishita on 2020/10/17.
//

import SwiftUI

// (1)
enum MyCustomStringConvertibleError: String, Error, CustomStringConvertible {
  case NoError = "No Error"
  case MoreThanOne = "more than  One"
  case LessThanOne = "less than One"
  var description: String {
    return rawValue
  }
}

// (2)
struct ContentView: View {
  @State private var intValue = 5
  
  var body: some View {
    NavigationView{
      VStack {
        Stepper("value: \(intValue)", value: $intValue)
        // (3)
        NavigationLink("calc (but it will finish after a while)  !", destination: ChildView(intValue: intValue))  
      }
    }
  }
}

//(4)
struct ChildView: View {
  var intValue: Int
  // (5)
  @State private var calcResult: Result?

  var body: some View {
    // (6)
    switch calcResult {
      case .success(let int):
        // (7)
        Text("Value is \(int)")
      case .failure(let error):
        // (8)
        Text("Error happened: \(error.description)")
      case nil:
        // (9)
        ProgressView()
          .onAppear(perform: {
              testFunc(value: intValue) { result in
                calcResult = result
              }})
    }
  }

  func testFunc(value: Int, completion: @escaping (Result) -> Void) {
    // (10)
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
      let result: Result
      // (11)
      if value == 1 {
        result = Result.success(value)
      } else if value > 1 {
        result = .failure(.MoreThanOne)
      } else {
        result = .failure(.LessThanOne)
      }
      completion(result)
    }
  }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

(1) エラー原因情報を含む MyuCustomStringConvertibleError を定義
(2) 起動時の画面を定義。値設定のスライダーと下位ビューへ遷移するためのボタンを表示
(3) 押下されるとステッパーで指定された値を下位ビューへ渡して遷移する
(4) NavigationLink で遷移する下位ビュー
(5) 非同期を想定した関数の実行結果を保持する Result の定義
(6) 保持している Result の状態によって、表示を切り替える switch 文
(7) Result が .success の状態であれば、結果を Text ビューで表示
(8) Result が .failure の状態であれば、エラーの詳細情報を Text ビューで表示
(9) Result が nil であれば、まだ計算が終了していないので、ProgressView を表示
ChildView へ遷移したときには、Result は nil なので、.onAppear を使用して、ChildView 遷移時に関数実行開始
(10) 実行される関数。非同期を想定しているので、3秒待ってから completion を呼ぶ
(11) 渡された値が 1 であれば、成功として 1 を返し、そうでなければ、”1 より大なのでエラー”、”1 より小なのでエラー”という情報を付与してエラーを返す

Work without error

「Work without error」

Work with Error

「Work with Error」

まとめ

Result の使い方
  • Result を使うと、実行状態(成功/失敗)と合わせて実行結果を渡すことができる
  • Result を生成するときには、.success と合わせて実行結果値、.failure と合わせて Error をセットする
  • 受け取った Result をチェックするときには、.success, .failure をチェックする。抜けもれは、Swift 側でチェックしてくれる
  • SwiftUI と組み合わせるときには、Result? としてオプショナルにすると、非同期処理と相性が良い

Swift 学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

Swift ポケットリファレンス

Swift を学んでも、プログラミング言語の文法を全て記憶しておくことは無理なので、ちょっとした文法の確認をするために、リファレンス本を手元に置いておくと便利です。

注意

Swift4 までしか対応していないので、相違点を理解して参照する必要があります。

説明は以上です。
不明な点やおかしな点ありましたら、ご連絡いただけるとありがたいです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です