🪶

Swift Result<S, F>

2024/07/18に公開

how to result?

https://developer.apple.com/documentation/swift/result

Result

A value that represents either a success or a failure, including an associated value in each case.

結果

成功または失敗を表す値で、それぞれの場合に関連する値を含む。

Result.success(_:)
A success, storing a Success value.

Result.success(_:)
成功、Success値を格納する。

こちらの動画が参考になった!
https://www.youtube.com/watch?v=8aPjqLqUEKA

https://www.youtube.com/watch?v=RBZFCp3kSLM

Swift5で導入された結果タイプ。古いネットワークを呼び出し、クリーンアップ。

これは、2つのケースを含む列挙型。失敗ケースの中に成功ケースがあり、ネットワークの呼び出しでおこることは、2つだけ。ネットワークが成功を呼び出して必要なデータを取得するか、何が怒って失敗し、それに応じて行動するかのどちらか。

ここから長い解説と完成したコードを書き換えて、解説が続く💦

Github Searchで使ってみた!

全体のコード
import SwiftUI

struct APIResponse: Codable {
    var items: [User]
}

struct User: Codable, Identifiable {
    let id = UUID()
    var login: String
    var url: String
    var avatar_url: String
    var html_url: String
}

class UserViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false
    @Published var errorMessage: String?

    func getUsers(searchText: String) {
        let trimmedSearchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
        
        guard !trimmedSearchText.isEmpty else {
            return
        }
        
        isLoading = true
        errorMessage = nil
        
        guard let apiURL = URL(string: "https://api.github.com/search/users?q=\(trimmedSearchText)") else {
            isLoading = false
            errorMessage = "Invalid URL"
            return
        }
        
        var request = URLRequest(url: apiURL)
        request.httpMethod = "GET"
        
        URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
            DispatchQueue.main.async {
                self?.isLoading = false
                let result: Result<[User], Error> = {
                    if let error = error {
                        return .failure(error)
                    }
                    guard let data = data else {
                        return .failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"]))
                    }
                    do {
                        let apiResponse = try JSONDecoder().decode(APIResponse.self, from: data)
                        return .success(apiResponse.items)
                    } catch {
                        return .failure(error)
                    }
                }()
                
                switch result {
                case .success(let fetchedUsers):
                    self?.users = fetchedUsers
                case .failure(let error):
                    self?.errorMessage = error.localizedDescription
                }
            }
        }.resume()
    }
}

struct ContentView: View {
    @StateObject private var viewModel = UserViewModel()
    @State private var searchText = ""
    
    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading {
                    LoadingView()
                } else if let errorMessage = viewModel.errorMessage {
                    ErrorView(message: errorMessage)
                } else {
                    UserListView(users: viewModel.users)
                }
            }
            .navigationTitle("GitHub Users")
        }
        .searchable(text: $searchText)
        .onSubmit(of: .search) {
            viewModel.getUsers(searchText: searchText)
        }
    }
}

struct LoadingView: View {
    var body: some View {
        VStack {
            ProgressView().padding()
            Text("Fetching Users...")
        }
    }
}

struct ErrorView: View {
    let message: String
    
    var body: some View {
        Text("Error: \(message)")
            .foregroundColor(.red)
    }
}

struct UserListView: View {
    let users: [User]
    
    var body: some View {
        List(users) { user in
            Link(destination: URL(string: user.html_url)!) {
                HStack {
                    AsyncImage(url: URL(string: user.avatar_url)) { phase in
                        switch phase {
                        case .success(let image):
                            image.resizable().frame(width: 50, height: 50)
                        default:
                            Image(systemName: "nosign")
                        }
                    }
                    VStack(alignment: .leading) {
                        Text(user.login)
                        Text(user.url)
                            .font(.system(size: 11))
                            .foregroundColor(Color.gray)
                    }
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

思いつきで作りましたが、成功したら、User型を返す、失敗したら、Error型を返すResult<[User], Error>を定義しました。

let result: Result<[User], Error> = {
                    if let error = error {
                        return .failure(error)
                    }
                    guard let data = data else {
                        return .failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"]))
                    }
                    do {
                        let apiResponse = try JSONDecoder().decode(APIResponse.self, from: data)
                        return .success(apiResponse.items)
                    } catch {
                        return .failure(error)
                    }
                }()
                
                switch result {
                case .success(let fetchedUsers):
                    self?.users = fetchedUsers
                case .failure(let error):
                    self?.errorMessage = error.localizedDescription
                }
            }
        }.resume()

成功するとこんな感じで、Githubに登録されているユーザーの情報が表示されます。

Lastly

今回は、Swift5から追加されていた、Result Type を使ってみました。やってることは単純で、成功と失敗の値を返すときに使います。

Discussion