gpt4 book ai didi

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

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

我正在尝试学习Combine,这对我来说是PITA。我从未学过RX Swift,所以这对我来说是全新的。我敢肯定我缺少这种简单的东西,但希望能有所帮助。

我正在尝试从API中获取一些JSON并将其加载到列表视图中。我有一个符合ObservableObject的视图模型,并更新了一个@Published属性,该属性是一个数组。我使用该VM加载列表,但是看起来该API返回之前视图加载的方式(列表显示为空白)。我希望这些属性包装器能够执行我认为应该做的事情,并在对象更改时重新渲染视图。

就像我说的,我确信我缺少一些简单的东西。如果您能找到它,我将很乐意为您提供帮助。谢谢!

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:)

这样的优势之一是您的代码朝着可测试的方向迈了一步。

可以使用replaceErrorcatch代替具有某些默认值的Empty(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协议的 DefaultHTTPClientHTTPClient

数据提取器

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/

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