Combine examples

This commit is contained in:
John Holdsworth
2019-07-10 00:37:46 +01:00
parent f5f4d0b3d5
commit 8018d0c2a0
13 changed files with 274 additions and 91 deletions

View File

@@ -1,34 +1,57 @@
import SwiftUI
import Combine
enum RequestError: Error {
case request(code: Int, error: Error?)
case unknown
enum URLSessionError: Error {
case invalidResponse
case serverErrorMessage(statusCode: Int, data: Data)
case urlError(URLError)
}
extension URLSession {
func send(request: URLRequest) -> AnyPublisher<(data: Data, response: HTTPURLResponse), RequestError> {
AnyPublisher<(data: Data, response: HTTPURLResponse), RequestError> { subscriber in
let task = self.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
let httpReponse = response as? HTTPURLResponse
if let data = data, let httpReponse = httpReponse, 200..<300 ~= httpReponse.statusCode {
_ = subscriber.receive((data, httpReponse))
subscriber.receive(completion: .finished)
}
else if let httpReponse = httpReponse {
subscriber.receive(completion: .failure(.request(code: httpReponse.statusCode, error: error)))
}
else {
subscriber.receive(completion: .failure(.unknown))
}
}
}
func send(request: URLRequest) -> AnyPublisher<Data, URLSessionError> {
subscriber.receive(subscription: AnySubscription(task.cancel))
task.resume()
}
dataTaskPublisher(for: request)
.mapError { URLSessionError.urlError($0) }
.flatMap { data, response -> AnyPublisher<Data, URLSessionError> in
guard let response = response as? HTTPURLResponse else {
return .fail(.invalidResponse)
}
guard 200..<300 ~= response.statusCode else {
return .fail(.serverErrorMessage(statusCode: response.statusCode,
data: data))
}
return .just(data)
}.eraseToAnyPublisher()
}
}
extension JSONDecoder: TopLevelDecoder {}
extension Publisher {
/// - seealso: https://twitter.com/peres/status/1136132104020881408
func flatMapLatest<T: Publisher>(_ transform: @escaping (Self.Output) -> T) -> Publishers.SwitchToLatest<T, Publishers.Map<Self, T>> where T.Failure == Self.Failure {
map(transform).switchToLatest()
}
}
extension Publisher {
static func empty() -> AnyPublisher<Output, Failure> {
return Publishers.Empty()
.eraseToAnyPublisher()
}
static func just(_ output: Output) -> AnyPublisher<Output, Failure> {
return Just(output)
.catch { _ in AnyPublisher<Output, Failure>.empty() }
.eraseToAnyPublisher()
}
static func fail(_ error: Failure) -> AnyPublisher<Output, Failure> {
return Publishers.Fail(error: error)
.eraseToAnyPublisher()
}
}

View File

@@ -11,5 +11,11 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
)
self.window = window
window.makeKeyAndVisible()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: SearchUserView().environmentObject(SearchUserViewModel()))
self.window = window
window.makeKeyAndVisible()
}
}
}

View File

@@ -36,10 +36,10 @@ final class SearchUserViewModel: BindableObject {
cancellable = assign
URLSession.shared.send(request: request)
.map { $0.data }
.decode(type: SearchUserResponse.self, decoder: JSONDecoder())
.map { $0.items }
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.receive(subscriber: assign)
}
@@ -50,10 +50,11 @@ final class SearchUserViewModel: BindableObject {
let request = URLRequest(url: user.avatar_url)
URLSession.shared.send(request: request)
.map { UIImage(data: $0.data) }
.map { UIImage(data: $0) }
.replaceError(with: nil)
.eraseToAnyPublisher()
.receive(subscriber: Subscribers.Sink<AnyPublisher<UIImage?, Never>> { [weak self] image in
.receive(on: DispatchQueue.main)
.receive(subscriber: Subscribers.Sink<UIImage?, Never> { [weak self] image in
self?.userImages[user] = image
})
}