使用 URLSession 和 ViewModel 检索数据
Posted
技术标签:
【中文标题】使用 URLSession 和 ViewModel 检索数据【英文标题】:Retrieving data using URLSession and ViewModel 【发布时间】:2021-07-13 02:08:04 【问题描述】:我正在尝试检索用户配置文件的数据,但我不确定如何在我的视图模型中初始化可观察对象。我看到了很多关于将返回一组对象的示例,但不是一个对象。
在尝试获取配置文件数据之前,如何使用 null 值初始化视图模型中的 profile
变量,还是不应该使用此方法来检索此数据?
另外,最好的做法是编写一个通用类(如 RestManager 类)来处理所有 API 请求并拥有一个调用 URLSession dataTaskPublisher 方法的方法?
型号:
struct Profile: Codable, Hashable, Identifiable
var id: UUID
var address: String
var city: String
var emailAddress: String
var firstName: String
var lastName: String
var smsNumber: String
var state: String
var userBio: String
var username: String
var zipCode: Int
视图模型:
class ProfileViewModel: ObservableObject
// Cannot create Profile() as it needs data to be instantiated
@Published var profile: Profile = Profile()
init()
fetchProfile()
func fetchProfile()
guard let url = URL(string: "https://path/to/api/v1/profiles") else
print("Failed to create URL")
return
let requestData = [
"username": "testuser"
]
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "GET"
request.httpBody = try? JSONEncoder().encode(requestData)
URLSession.shared.dataTaskPublisher(for: request)
.tryMap output in
guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else
throw HTTPError.statusCode
return output.data
.decode(type: Profile.self, decoder: JSONDecoder())
.retry(3)
.replaceError(with: Profile()) // Same issue here - requires data to be instantiated
.eraseToAnyPublisher()
.receive(on: DispatchQueue.main)
.assign(to: &$profile)
查看:
struct ProfileSummary: View
@ObservedObject var viewModel: ProfileViewModel
var body: some View
NavigationView
VStack
Text(viewModel.profile.firstName)
Text(viewModel.profile.lastName)
.navigationTitle("Profile")
.onAppear
viewModel.fetchProfile()
【问题讨论】:
【参考方案1】:一个可能的解决方案——避免使用选项——是一个静态属性,创建一个空的示例实例
struct Profile: Codable, Hashable, Identifiable
var id: UUID
var address: String
var city: String
var emailAddress: String
var firstName: String
var lastName: String
var smsNumber: String
var state: String
var userBio: String
var username: String
var zipCode: Int
static let sample = Profile(id: UUID(), address: "", city: "", emailAddress: "", firstName: "", lastName: "", smsNumber: "", state: "", userBio: "", username: "", zipCode: 0)
并使用它
@Published var profile = Profile.sample
也可以在replaceError
操作符行中
.replaceError(with: Profile.sample)
或者例如为所有可能的状态创建一个枚举
enum State
case undetermined
case isLoading
case loaded(Profile)
case error(Error)
@Published var state : State = .undetermined
在视图switch
上的状态并渲染相应的UI
旁注:
如果结构成员永远不会被修改,请将它们声明为常量 (let
)
如果管道在同一范围内结束,.eraseToAnyPublisher()
将毫无意义。
【讨论】:
空样本似乎更容易,但我觉得这就是为什么发明 Optionals 的原因,以避免编写不正确的数据。最好有“nil = 不存在”。在 UI 中,您可能也有类似的东西。 “文本(profile.firstName).frame(宽度:111).border(.red)”。这将显示一个空的红色框。 根据您的设计,空实例应该是占位符。如果您期望不存在,则应不替换错误并正确处理。例如,您可以返回Result<Profile,Error>
类型。或者使用具有多个状态的枚举。在 SwiftUI 中,我们鼓励您尽可能避免使用可选项,而不是在 Objective-C 桥接 UIKit 中。
在查看了所有答案并进行了一些测试后,我能够使用这种方法使其正常工作。我发现这个链接也补充了这篇文章:swiftbysundell.com/articles/handling-loading-states-in-swiftui。感谢所有回复的人!【参考方案2】:
您可以尝试这样的方法,将“个人资料”设为可选:
(请注意,在您的“ProfileViewModel”中,我看不到您实际为“profile”分配值的位置。似乎您缺少 sink/receiveValue)
class ProfileViewModel: ObservableObject
@Published var profile: Profile: Profile? // <--- here
...
struct ProfileSummary: View
@ObservedObject var viewModel: ProfileViewModel
var body: some View
NavigationView
VStack
if let profile = viewModel.profile // <--- here
Text(profile.firstName)
Text(profile.lastName)
.navigationTitle("Profile")
.onAppear
viewModel.fetchProfile()
编辑:要解决“ProfileViewModel”中的其他问题,您可以尝试:
class ProfileViewModel: ObservableObject
@Published var profile: Profile?
var cancellable = Set<AnyCancellable>()
func fetchProfile()
guard let url = URL(string: "https://path/to/api/v1/profiles") else
print("Failed to create URL")
return
let requestData = ["username": "testuser"]
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "GET"
request.httpBody = try? JSONEncoder().encode(requestData)
URLSession.shared.dataTaskPublisher(for: URLRequest(url: url))
.tryMap output in
guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else
throw HTTPError.statusCode
return output.data
.decode(type: Profile.self, decoder: JSONDecoder())
.retry(3)
.eraseToAnyPublisher()
.receive(on: DispatchQueue.main)
.sink completion in
print("-----> completion: \(completion)")
receiveValue: profile in
self.profile = profile
.store(in: &self.cancellable)
【讨论】:
错过了那部分。我正在重写此代码并忘记添加它。它现在更新为我之前所做的。看起来我仍然对 .replaceError 和 .assign 有疑问。使用 .assign,它不允许我使用可选项。 我不太熟悉“replaceError”和“assign()”,也许你可以使用:“.replaceError(with: nil)”并替换“.assign(to: &$profile )" with ".sink error in // 处理错误 receiveValue: profile in self.profile = profile " 更新了答案。这对你有用吗?【参考方案3】:而不是让视图尝试呈现不存在的内容(这到底是怎么实现的??)
明确在任何时候渲染什么。
因此,与其尝试绘制值为.none
的可选项:
您的“视图状态”或其一部分:
enum Content
case none(NoContent)
case some(Profile)
在您的视图模型中:
class ProfileViewModel: ObservableObject
@Published var viewState: Content = .none(NoContent())
...
您的NoContent
准确地代表了当配置文件值不存在时要呈现的内容。这也使您能够为“没有可用的配置文件(还)”呈现一个很好的表示,因为您可以在您即将加载配置文件时呈现您(或 UX)想要查看的 any 视图 - 或发生错误时,或...
【讨论】:
以上是关于使用 URLSession 和 ViewModel 检索数据的主要内容,如果未能解决你的问题,请参考以下文章
使用 URLSession 和 Codable 解析 JSON 数据
使用 URLSession 和后台获取以及使用 firebase 的远程通知