【发布时间】:2020-09-03 10:30:44
【问题描述】:
构建 SwiftUI 应用程序的网络层的行之有效的方法是什么?具体来说,您如何使用 URLSession 构建 JSON 数据以在 SwiftUI 视图中显示并处理所有可能正确发生的不同状态?
【问题讨论】:
标签: ios swiftui urlsession
构建 SwiftUI 应用程序的网络层的行之有效的方法是什么?具体来说,您如何使用 URLSession 构建 JSON 数据以在 SwiftUI 视图中显示并处理所有可能正确发生的不同状态?
【问题讨论】:
标签: ios swiftui urlsession
这是我在上一个项目中的想法:
View#onReceive 在 SwiftUI 中直接订阅发布者,但将发布者封装在模型类中似乎更多整体干净).onAppear 修饰符触发加载。.overlay 修饰符可以方便地根据状态显示进度/错误视图该方法的独立示例代码(也可在我的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() {}
}
}
【讨论】:
在 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
【讨论】: