【问题标题】:Retrieving data using URLSession and ViewModel使用 URLSession 和 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()
            }
        }
    }
}

【问题讨论】:

    标签: ios swift xcode mvvm swiftui


    【解决方案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 视图 - 或发生错误时,或...

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-23
      • 1970-01-01
      相关资源
      最近更新 更多