gpt4 book ai didi

swift - 结合 SwiftUI 远程获取数据 - ObjectBinding 不更新 View

转载 作者:行者123 更新时间:2023-12-02 23:04:28 24 4
gpt4 key购买 nike

我正在尝试学习Combine,它对我来说是一个PITA。我从未学过 RX Swift,所以这对我来说是全新的。我确信我错过了一些简单的事情,但希望得到一些帮助。

我正在尝试从 API 获取一些 JSON 并将其加载到 ListView 中。我有一个符合 ObservableObject 的 View 模型,并更新一个 @Published 属性,它是一个数组。我使用该虚拟机加载我的列表,但看起来 View 在该 API 返回之前加载(列表显示为空白)。我希望这些属性包装器能够执行我认为它们应该执行的操作,并在对象发生更改时重新渲染 View 。

就像我说的,我确信我错过了一些简单的东西。如果您能发现它,我会很乐意提供帮助。谢谢!

class PhotosViewModel: ObservableObject {

var cancellable: AnyCancellable?

@Published var photos = Photos()

func load(user collection: String) {
guard let url = URL(string: "https://api.unsplash.com/users/\(collection)/collections?client_id=\(Keys.unsplashAPIKey)") else {
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Photos.self, decoder: JSONDecoder())
.replaceError(with: defaultPhotosObject)
.receive(on: RunLoop.main)
.assign(to: \.photos, on: self)
}

}
struct PhotoListView: View {
@EnvironmentObject var photosViewModel: PhotosViewModel
var body: some View {
NavigationView {
List(photosViewModel.photos) { photo in
NavigationLink(destination: PhotoDetailView(photo)) {
PhotoRow(photo)
}
}.navigationBarTitle("Photos")
}
}
}
struct PhotoRow: View {
var photo: Photo
init(_ photo: Photo) {
self.photo = photo
}
var body: some View {
HStack {
ThumbnailImageLoadingView(photo.coverPhoto.urls.thumb)
VStack(alignment: .leading) {
Text(photo.title)
.font(.headline)
Text(photo.user.firstName)
.font(.body)
}
.padding(.leading, 5)
}
.padding(5)
}
}

最佳答案

根据您更新的解决方案,这里有一些改进建议(不适合评论)。

PhotosViewModel改进建议

我可以建议更改您的 load返回 Void 的函数(即不返回任何内容),返回 AnyPublisher<Photos, Never>并跳过最后一步.assign(to:on:)

这样做的一个优点是您的代码向可测试迈出了一步。

而不是 replaceError对于一些默认值,您可以使用 catchEmpty(completeImmediately: <TRUE/FALSE>) 一起。因为总是可以得出任何相关的默认值吗?也许在这种情况下?也许是“空照片”?如果是这样,那么您可以制作 Photos符合ExpressibleByArrayLiteral并使用replaceError(with: [])或者您可以创建一个名为 empty 的静态变量,允许 replaceError(with: .empty) .

在代码块中总结我的建议:

public class PhotosViewModel: ObservableObject {

@Published var photos = Photos()

// var cancellable: AnyCancellable? -> change to Set<AnyCancellable>
private var cancellables = Set<AnyCancellable>()
private let urlSession: URLSession

public init(urlSession: URLSession = .init()) {
self.urlSession = urlSession
}
}

private extension PhotosViewModel {}
func populatePhotoCollection(named nameOfPhotoCollection: String) {
fetchPhotoCollection(named: nameOfPhotoCollection)
.assign(to: \.photos, on: self)
.store(in: &cancellables)
}

func fetchPhotoCollection(named nameOfPhotoCollection: String) -> AnyPublisher<Photos, Never> {
func emptyPublisher(completeImmediately: Bool = true) -> AnyPublisher<Photos, Never> {
Empty<Photos, Never>(completeImmediately: completeImmediately).eraseToAnyPublisher()
}

// This really ought to be moved to some APIClient
guard let url = URL(string: "https://api.unsplash.com/users/\(collection)/collections?client_id=\(Keys.unsplashAPIKey)") else {
return emptyPublisher()
}

return urlSession.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Photos.self, decoder: JSONDecoder())
.catch { error -> AnyPublisher<Photos, Never> in
print("☣️ error decoding: \(error)")
return emptyPublisher()
}
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}

*Client建议

您可能想要编写某种 HTTPClient/APIClient/RESTClient 并查看 HTTP 状态代码。

这是一个高度模块化(有人可能会说 - 过度设计)的解决方案,使用 DataFetcher和一个 DefaultHTTPClient符合HTTPClient协议(protocol):

数据 getter

public final class DataFetcher {

private let dataFromRequest: (URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError>
public init(dataFromRequest: @escaping (URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError>) {
self.dataFromRequest = dataFromRequest
}
}

public extension DataFetcher {
func fetchData(request: URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError> {
dataFromRequest(request)
}
}

// MARK: Convenience init
public extension DataFetcher {

static func urlResponse(
errorMessageFromDataMapper: ErrorMessageFromDataMapper,
headerInterceptor: (([AnyHashable: Any]) -> Void)?,
badStatusCodeInterceptor: ((UInt) -> Void)?,
_ dataAndUrlResponsePublisher: @escaping (URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError>
) -> DataFetcher {

DataFetcher { request in
dataAndUrlResponsePublisher(request)
.mapError { HTTPError.NetworkingError.urlError($0) }
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw HTTPError.NetworkingError.invalidServerResponse(response)
}

headerInterceptor?(httpResponse.allHeaderFields)

guard case 200...299 = httpResponse.statusCode else {

badStatusCodeInterceptor?(UInt(httpResponse.statusCode))

let dataAsErrorMessage = errorMessageFromDataMapper.errorMessage(from: data) ?? "Failed to decode error from data"
print("⚠️ bad status code, error message: <\(dataAsErrorMessage)>, httpResponse: `\(httpResponse.debugDescription)`")
throw HTTPError.NetworkingError.invalidServerStatusCode(httpResponse.statusCode)
}
return data
}
.mapError { castOrKill(instance: $0, toType: HTTPError.NetworkingError.self) }
.eraseToAnyPublisher()

}
}

// MARK: From URLSession
static func usingURLSession(
errorMessageFromDataMapper: ErrorMessageFromDataMapper,
headerInterceptor: (([AnyHashable: Any]) -> Void)?,
badStatusCodeInterceptor: ((UInt) -> Void)?,
urlSession: URLSession = .shared
) -> DataFetcher {

.urlResponse(
errorMessageFromDataMapper: errorMessageFromDataMapper,
headerInterceptor: headerInterceptor,
badStatusCodeInterceptor: badStatusCodeInterceptor
) { urlSession.dataTaskPublisher(for: $0).eraseToAnyPublisher() }
}
}

HTTP客户端

public final class DefaultHTTPClient {
public typealias Error = HTTPError

public let baseUrl: URL

private let jsonDecoder: JSONDecoder
private let dataFetcher: DataFetcher

private var cancellables = Set<AnyCancellable>()

public init(
baseURL: URL,
dataFetcher: DataFetcher,
jsonDecoder: JSONDecoder = .init()
) {
self.baseUrl = baseURL
self.dataFetcher = dataFetcher
self.jsonDecoder = jsonDecoder
}
}

// MARK: HTTPClient
public extension DefaultHTTPClient {

func perform(absoluteUrlRequest urlRequest: URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError> {
return Combine.Deferred {
return Future<Data, HTTPError.NetworkingError> { [weak self] promise in

guard let self = self else {
promise(.failure(.clientWasDeinitialized))
return
}

self.dataFetcher.fetchData(request: urlRequest)

.sink(
receiveCompletion: { completion in
guard case .failure(let error) = completion else { return }
promise(.failure(error))
},
receiveValue: { data in
promise(.success(data))
}
).store(in: &self.cancellables)
}
}.eraseToAnyPublisher()
}

func performRequest(pathRelativeToBase path: String) -> AnyPublisher<Data, HTTPError.NetworkingError> {
let url = URL(string: path, relativeTo: baseUrl)!
let urlRequest = URLRequest(url: url)
return perform(absoluteUrlRequest: urlRequest)
}

func fetch<D>(urlRequest: URLRequest, decodeAs: D.Type) -> AnyPublisher<D, HTTPError> where D: Decodable {
return perform(absoluteUrlRequest: urlRequest)
.mapError { print("☢️ got networking error: \($0)"); return castOrKill(instance: $0, toType: HTTPError.NetworkingError.self) }
.mapError { HTTPError.networkingError($0) }
.decode(type: D.self, decoder: self.jsonDecoder)
.mapError { print("☢️ 🚨 got decoding error: \($0)"); return castOrKill(instance: $0, toType: DecodingError.self) }
.mapError { Error.serializationError(.decodingError($0)) }
.eraseToAnyPublisher()
}

}

助手

public protocol ErrorMessageFromDataMapper {
func errorMessage(from data: Data) -> String?
}


public enum HTTPError: Swift.Error {
case failedToCreateRequest(String)
case networkingError(NetworkingError)
case serializationError(SerializationError)
}

public extension HTTPError {
enum NetworkingError: Swift.Error {
case urlError(URLError)
case invalidServerResponse(URLResponse)
case invalidServerStatusCode(Int)
case clientWasDeinitialized
}

enum SerializationError: Swift.Error {
case decodingError(DecodingError)
case inputDataNilOrZeroLength
case stringSerializationFailed(encoding: String.Encoding)
}
}

internal func castOrKill<T>(
instance anyInstance: Any,
toType expectedType: T.Type,
_ file: String = #file,
_ line: Int = #line
) -> T {

guard let instance = anyInstance as? T else {
let incorrectTypeString = String(describing: Mirror(reflecting: anyInstance).subjectType)
fatalError("Expected variable '\(anyInstance)' (type: '\(incorrectTypeString)') to be of type `\(expectedType)`, file: \(file), line:\(line)")
}
return instance
}

关于swift - 结合 SwiftUI 远程获取数据 - ObjectBinding 不更新 View ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59505048/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com