【问题标题】:Using URLSession to load JSON data for SwiftUI Views使用 URLSession 为 SwiftUI 视图加载 JSON 数据
【发布时间】:2020-09-03 10:30:44
【问题描述】:

构建 SwiftUI 应用程序的网络层的行之有效的方法是什么?具体来说,您如何使用 URLSession 构建 JSON 数据以在 SwiftUI 视图中显示并处理所有可能正确发生的不同状态?

【问题讨论】:

    标签: ios swiftui urlsession


    【解决方案1】:

    这是我在上一个项目中的想法:

    • 将加载过程表示为ObservableObject模型类
    • 使用URLSession.dataTaskPublisher进行加载
    • 使用CodableJSONDecoder 对使用Combine support for decoding 的Swift 类型的响应进行解码
    • 作为@Published 属性跟踪模型中的状态,以便视图可以显示加载/错误状态。
    • 在单独的属性中以@Published 属性的形式跟踪加载的结果,以便在 SwiftUI 中轻松使用(您也可以使用 View#onReceive 在 SwiftUI 中直接订阅发布者,但将发布者封装在模型类中似乎更多整体干净)
    • 如果尚未加载,请使用 SwiftUI .onAppear 修饰符触发加载。
    • 使用.overlay 修饰符可以方便地根据状态显示进度/错误视图
    • 为重复发生的任务提取可重用组件(这里是一个示例:EndpointModel

    该方法的独立示例代码(也可在我的SwiftUIPlayground 中找到):

    // SwiftUIPlayground
    // https://github.com/ralfebert/SwiftUIPlayground/
    
    import Combine
    import SwiftUI
    
    struct TypiTodo: Codable, Identifiable {
        var id: Int
        var title: String
    }
    
    class TodosModel: ObservableObject {
    
        @Published var todos = [TypiTodo]()
        @Published var state = State.ready
    
        enum State {
            case ready
            case loading(Cancellable)
            case loaded
            case error(Error)
        }
    
        let url = URL(string: "https://jsonplaceholder.typicode.com/todos/")!
        let urlSession = URLSession.shared
    
        var dataTask: AnyPublisher<[TypiTodo], Error> {
            self.urlSession
                .dataTaskPublisher(for: self.url)
                .map { $0.data }
                .decode(type: [TypiTodo].self, decoder: JSONDecoder())
                .receive(on: RunLoop.main)
                .eraseToAnyPublisher()
        }
    
        func load() {
            assert(Thread.isMainThread)
            self.state = .loading(self.dataTask.sink(
                receiveCompletion: { completion in
                    switch completion {
                    case .finished:
                        break
                    case let .failure(error):
                        self.state = .error(error)
                    }
                },
                receiveValue: { value in
                    self.state = .loaded
                    self.todos = value
                }
            ))
        }
    
        func loadIfNeeded() {
            assert(Thread.isMainThread)
            guard case .ready = self.state else { return }
            self.load()
        }
    }
    
    struct TodosURLSessionExampleView: View {
    
        @ObservedObject var model = TodosModel()
    
        var body: some View {
            List(model.todos) { todo in
                Text(todo.title)
            }
            .overlay(StatusOverlay(model: model))
            .onAppear { self.model.loadIfNeeded() }
        }
    }
    
    struct StatusOverlay: View {
    
        @ObservedObject var model: TodosModel
    
        var body: some View {
            switch model.state {
            case .ready:
                return AnyView(EmptyView())
            case .loading:
                return AnyView(ActivityIndicatorView(isAnimating: .constant(true), style: .large))
            case .loaded:
                return AnyView(EmptyView())
            case let .error(error):
                return AnyView(
                    VStack(spacing: 10) {
                        Text(error.localizedDescription)
                            .frame(maxWidth: 300)
                        Button("Retry") {
                            self.model.load()
                        }
                    }
                    .padding()
                    .background(Color.yellow)
                )
            }
        }
    
    }
    
    struct TodosURLSessionExampleView_Previews: PreviewProvider {
        static var previews: some View {
            Group {
                TodosURLSessionExampleView(model: TodosModel())
                TodosURLSessionExampleView(model: self.exampleLoadedModel)
                TodosURLSessionExampleView(model: self.exampleLoadingModel)
                TodosURLSessionExampleView(model: self.exampleErrorModel)
            }
        }
    
        static var exampleLoadedModel: TodosModel {
            let todosModel = TodosModel()
            todosModel.todos = [TypiTodo(id: 1, title: "Drink water"), TypiTodo(id: 2, title: "Enjoy the sun")]
            todosModel.state = .loaded
            return todosModel
        }
    
        static var exampleLoadingModel: TodosModel {
            let todosModel = TodosModel()
            todosModel.state = .loading(ExampleCancellable())
            return todosModel
        }
    
        static var exampleErrorModel: TodosModel {
            let todosModel = TodosModel()
            todosModel.state = .error(ExampleError.exampleError)
            return todosModel
        }
    
        enum ExampleError: Error {
            case exampleError
        }
    
        struct ExampleCancellable: Cancellable {
            func cancel() {}
        }
    
    }
    

    【讨论】:

    • 这是一个很好的示例,但仅适用于使用 1 个模型的单个/简单用例。您如何着手重构此代码,以便 Loader/StatusOverlay 组件在多个 Codable 结构上是通用的?
    【解决方案2】:

    在 View Struct 之外将状态/数据/网络拆分成一个单独的 @ObservableObject 类绝对是要走的路。有太多的 SwiftUI “Hello World”示例将它们全部填充到 View 结构中。

    作为最佳实践,您可以将 @ObservableObject 命名与 MVVM 内联标准化,并将该“模型”类称为 ViewModel,如下所示:

    @StateObject var viewModel = TodosViewModel()
    

    其中的大部分代码用于处理视图的覆盖状态、onAppear 事件和显示问题。

    创建一个新的 TodosModel 类并在 ViewModel 中引用它:

    @ObservedObject var model = TodosModel()
    

    然后使用 ViewModel 调用的一种方法将所有网络/api/JSON 代码移动到该类中:

    public func getList() -> AnyPublisher<[TypiTodo], Error>
    

    View-ViewModel-Model 现在被拆分了,与 Paul D 的评论有关,ViewModel 可以组合 1 个或多个模型来返回视图需要的任何内容。而且,更重要的是,TodoModel 实体对 View 一无所知,可以专注于 http/JSON/CRUD。

    以下是使用组合 / HTTP / JSON 解码的好例子。您可以看到它如何使用 tryMap、mapError 进一步将网络与解码错误分开。 https://gist.github.com/stinger/e8b706ab846a098783d68e5c3a4f0ea5

    请参阅这篇文章中对@StateObject 和@ObservedObject 之间区别的非常简短和清晰的解释: https://levelup.gitconnected.com/state-vs-stateobject-vs-observedobject-vs-environmentobject-in-swiftui-81e2913d63f9

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-04-15
      • 1970-01-01
      • 2020-07-21
      • 2021-09-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-06-22
      相关资源
      最近更新 更多