ios - 使用 URLSession 为 SwiftUI View 加载 JSON 数据

标签 ios swiftui urlsession

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

最佳答案

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

  • 将加载过程表示为 ObservableObject模型类
  • 使用 URLSession.dataTaskPublisher用于装载
  • 使用 CodableJSONDecoder使用 Combine support for decoding 解码对 Swift 类型的响应
  • 跟踪模型中的状态作为@Published 属性,以便 View 可以显示加载/错误状态。
  • 将加载的结果作为 @Published 属性记录在单独的属性中,以便在 SwiftUI 中使用(您也可以使用 View#onReceive 直接在 SwiftUI 中订阅发布者,但将发布者封装在模型类中总体上看起来更干净)
  • 使用 SwiftUI .onAppear如果尚未加载,则触发加载的修饰符。
  • 使用 .overlay修饰符可以方便地根据状态显示进度/错误 View
  • 为重复发生的任务提取可重用的组件(这里是一个例子: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() {}
        }
    
    }
    

    关于ios - 使用 URLSession 为 SwiftUI View 加载 JSON 数据,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61855811/

    相关文章:

    使用 Sqlite 和 GRDB 的 SwiftUI FileDocument

    ios - 如何模拟 URLSession.DataTaskPublisher

    ios - 专注于输入字段和方向变化会扰乱 iOS 6 Safari 上的缩放级别

    ios - 如何从 iOS 应用程序创建成员(member)条形码存折通行证?

    ios - Actionscript 3 是否有等效的 Reachability 类?

    iphone - 从应用商店下载多个应用

    swiftui - 如何在 swiftui 的 vstack 下以不同方式对齐 TextView ?

    ios - 使用 SwiftUI 从 Wireframe 创建一个新的 VIPER 模块

    arrays - 在 Swift 4 中使用 decodable 访问嵌入式 JSON

    ios - URLSession 和 JSONSerialization Swift 3