- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
我正在尝试学习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:)
。
这样的优势之一是您的代码朝着可测试的方向迈了一步。
可以使用replaceError
和catch
代替具有某些默认值的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
建议
DataFetcher
协议的
DefaultHTTPClient
和
HTTPClient
:
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() }
}
}
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/
SwiftUI 具有 .animate() 的隐式动画,以及使用 .withAnimation() 的显式.但是,我不知道如何为图像更改设置动画: struct ImageViewWidget : V
我有一个 SwiftUI View ,它有一个 @ObjectBinding progression: Progressable 像这样: struct ProgressBarComponent :
当我们有 @Binding 属性包装器时,@EnvironmentObject 的目的是什么?我无法弄清楚为什么需要@EnvironmentObject。我查看了这些帖子,但它并不是我想要的 Envi
我想删除一个标记为 @ObjectBinding 的对象,例如为了清理一些 TextFields。 我试图将对象引用设置为 nil,但没有成功。 import SwiftUI import Combi
我正在尝试学习Combine,它对我来说是一个PITA。我从未学过 RX Swift,所以这对我来说是全新的。我确信我错过了一些简单的事情,但希望得到一些帮助。 我正在尝试从 API 获取一些 JSO
我正在尝试在 SwiftUI 中设置一个基本列表,但我遇到了一个对我来说意义不大的编译时错误。这是错误: Cannot convert value of type 'Text' to closure
我有一个简单的 bean,它有一些相互关联的属性。例如,这个 bean 有一个名为 discountRate 的属性和另一个名为 discountValue 的属性。 discountRate 是应用
在 SwiftUI 中,您可以将 Publisher 的实例直接用作 @ObjectBinding 属性,还是必须将其包装在实现 BindableObject 的类中? let subject = P
我正在测试 Combine 框架并使用 BindableObject 作为通知中心,以便在 SwiftUI ContentView 的多个 View 之间传递数据。 其中一个 View 是一个表。我单
我是一名优秀的程序员,十分优秀!