使用 URLSession 和 ViewModel 检索数据
Retrieving data using URLSession and ViewModel
我正在尝试检索用户配置文件的数据,但我不确定如何在我的视图模型中初始化可观察对象。我看到很多示例将返回一组对象,但不是一个对象。
在尝试获取配置文件数据之前,如何使用空值初始化视图模型中的 profile
变量,或者我不应该使用此方法来检索此数据?
另外,最好的做法是编写一个通用的 class(如 RestManager class)来处理所有 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()
}
}
}
}
您可以尝试这样的操作,将“个人资料”设为可选:
(请注意,在您的“ProfileViewModel”中,我看不到您实际上在哪里为“配置文件”赋值。似乎您缺少 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)
}
}
一个可能的解决方案——为了避免可选——是静态的属性创建一个空的示例实例
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()
毫无意义。
而不是让视图尝试呈现不存在的内容(到底怎么能实现呢??)
明确渲染什么随时。
因此,与其尝试绘制值为 .none
的可选值,不如:
您的“视图状态”或其中的一部分:
enum Content {
case none(NoContent)
case some(Profile)
}
在您的视图模型中:
class ProfileViewModel: ObservableObject {
@Published var viewState: Content = .none(NoContent())
...
您的 NoContent
准确表示缺少配置文件值时要呈现的内容。这也使您能够为“没有可用的配置文件(尚未)”呈现一个很好的表示,因为您可以呈现 any 视图,您(或 UX)想要在您即将加载时查看配置文件 - 或发生错误时,或 ...
我正在尝试检索用户配置文件的数据,但我不确定如何在我的视图模型中初始化可观察对象。我看到很多示例将返回一组对象,但不是一个对象。
在尝试获取配置文件数据之前,如何使用空值初始化视图模型中的 profile
变量,或者我不应该使用此方法来检索此数据?
另外,最好的做法是编写一个通用的 class(如 RestManager class)来处理所有 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()
}
}
}
}
您可以尝试这样的操作,将“个人资料”设为可选:
(请注意,在您的“ProfileViewModel”中,我看不到您实际上在哪里为“配置文件”赋值。似乎您缺少 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)
}
}
一个可能的解决方案——为了避免可选——是静态的属性创建一个空的示例实例
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()
毫无意义。
而不是让视图尝试呈现不存在的内容(到底怎么能实现呢??)
明确渲染什么随时。
因此,与其尝试绘制值为 .none
的可选值,不如:
您的“视图状态”或其中的一部分:
enum Content {
case none(NoContent)
case some(Profile)
}
在您的视图模型中:
class ProfileViewModel: ObservableObject {
@Published var viewState: Content = .none(NoContent())
...
您的 NoContent
准确表示缺少配置文件值时要呈现的内容。这也使您能够为“没有可用的配置文件(尚未)”呈现一个很好的表示,因为您可以呈现 any 视图,您(或 UX)想要在您即将加载时查看配置文件 - 或发生错误时,或 ...