使用 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 的远程通知

如何在 Swift 3 中使用带有代理的 URLSession

如何同步 URLSession 任务的串行队列?

swift--使用URLSession异步加载图片

tableview 是空的,但我看到操场上的数据来自 urlsession