使用 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)想要在您即将加载时查看配置文件 - 或发生错误时,或 ...