mirror of
https://github.com/ivanvorobei/SwiftUI.git
synced 2026-01-17 14:36:46 +01:00
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
BB3DC0CE22D54717006F0587 /* AnySubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3DC0CD22D54717006F0587 /* AnySubscription.swift */; };
|
||||
BB3DC0D022D54737006F0587 /* ErrorResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3DC0CF22D54737006F0587 /* ErrorResponse.swift */; };
|
||||
BB3DC0D222D54826006F0587 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3DC0D122D54826006F0587 /* WebView.swift */; };
|
||||
BB3DC0D422D54927006F0587 /* Publisher.Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3DC0D322D54927006F0587 /* Publisher.Extension.swift */; };
|
||||
ED73146222A6F228004F8DA0 /* RepositoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED73146122A6F228004F8DA0 /* RepositoryListView.swift */; };
|
||||
EDDADAB122A808D500FEDC01 /* Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDDADAB022A808D500FEDC01 /* Combine.swift */; };
|
||||
EDDADAB322A809E600FEDC01 /* URLSession.Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDDADAB222A809E600FEDC01 /* URLSession.Combine.swift */; };
|
||||
@@ -24,6 +28,10 @@
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
BB3DC0CD22D54717006F0587 /* AnySubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySubscription.swift; sourceTree = "<group>"; };
|
||||
BB3DC0CF22D54737006F0587 /* ErrorResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorResponse.swift; sourceTree = "<group>"; };
|
||||
BB3DC0D122D54826006F0587 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
|
||||
BB3DC0D322D54927006F0587 /* Publisher.Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.Extension.swift; sourceTree = "<group>"; };
|
||||
ED73146122A6F228004F8DA0 /* RepositoryListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryListView.swift; sourceTree = "<group>"; };
|
||||
EDDADAB022A808D500FEDC01 /* Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Combine.swift; sourceTree = "<group>"; };
|
||||
EDDADAB222A809E600FEDC01 /* URLSession.Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.Combine.swift; sourceTree = "<group>"; };
|
||||
@@ -59,6 +67,7 @@
|
||||
EDDADAB022A808D500FEDC01 /* Combine.swift */,
|
||||
EDDADAB222A809E600FEDC01 /* URLSession.Combine.swift */,
|
||||
EDDADABA22A82C2500FEDC01 /* JSONDecoder.Extension.swift */,
|
||||
BB3DC0D322D54927006F0587 /* Publisher.Extension.swift */,
|
||||
);
|
||||
path = Extension;
|
||||
sourceTree = "<group>";
|
||||
@@ -109,6 +118,7 @@
|
||||
ED73146122A6F228004F8DA0 /* RepositoryListView.swift */,
|
||||
EDDF06B622A6CD8300B23D44 /* RepositoryView.swift */,
|
||||
EDDADAB822A82C0400FEDC01 /* RepositoryListViewModel.swift */,
|
||||
BB3DC0D122D54826006F0587 /* WebView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
@@ -119,6 +129,8 @@
|
||||
EDDF06B922A6D78200B23D44 /* Repository.swift */,
|
||||
EDDADAB422A821CD00FEDC01 /* ItemResponse.swift */,
|
||||
EDDADAB622A829A300FEDC01 /* RepositoryAPI.swift */,
|
||||
BB3DC0CD22D54717006F0587 /* AnySubscription.swift */,
|
||||
BB3DC0CF22D54737006F0587 /* ErrorResponse.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
@@ -195,8 +207,10 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
ED73146222A6F228004F8DA0 /* RepositoryListView.swift in Sources */,
|
||||
BB3DC0CE22D54717006F0587 /* AnySubscription.swift in Sources */,
|
||||
EDDADAB722A829A300FEDC01 /* RepositoryAPI.swift in Sources */,
|
||||
EDDF06A222A6C61C00B23D44 /* AppDelegate.swift in Sources */,
|
||||
BB3DC0D422D54927006F0587 /* Publisher.Extension.swift in Sources */,
|
||||
EDDADAB522A821CD00FEDC01 /* ItemResponse.swift in Sources */,
|
||||
EDDF06A422A6C61C00B23D44 /* SceneDelegate.swift in Sources */,
|
||||
EDDADAB922A82C0400FEDC01 /* RepositoryListViewModel.swift in Sources */,
|
||||
@@ -205,6 +219,8 @@
|
||||
EDDADAB322A809E600FEDC01 /* URLSession.Combine.swift in Sources */,
|
||||
EDDADABB22A82C2500FEDC01 /* JSONDecoder.Extension.swift in Sources */,
|
||||
EDDADAB122A808D500FEDC01 /* Combine.swift in Sources */,
|
||||
BB3DC0D022D54737006F0587 /* ErrorResponse.swift in Sources */,
|
||||
BB3DC0D222D54826006F0587 /* WebView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Publisher.Extension.swift
|
||||
// GitHubSearchWithSwiftUI
|
||||
//
|
||||
// Created by John Holdsworth on 09/07/2019.
|
||||
// Copyright © 2019 jp.marty-suzuki. All rights reserved.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -15,44 +15,26 @@ extension CombineExtension where Base == URLSession {
|
||||
|
||||
func send(request: URLRequest) -> AnyPublisher<Data, URLSessionError> {
|
||||
|
||||
AnyPublisher<Data, URLSessionError> { [base] subscriber in
|
||||
|
||||
let task = base.dataTask(with: request) { data, response, error in
|
||||
|
||||
base.dataTaskPublisher(for: request)
|
||||
.mapError { URLSessionError.urlError($0) }
|
||||
.flatMap { data, response -> AnyPublisher<Data, URLSessionError> in
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
subscriber.receive(completion: .failure(.invalidResponse))
|
||||
return
|
||||
return .fail(.invalidResponse)
|
||||
}
|
||||
|
||||
guard 200..<300 ~= response.statusCode else {
|
||||
let e = URLSessionError.serverError(statusCode: response.statusCode,
|
||||
error: error)
|
||||
subscriber.receive(completion: .failure(e))
|
||||
return
|
||||
return .fail(.serverErrorMessage(statusCode: response.statusCode,
|
||||
data: data))
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
subscriber.receive(completion: .failure(.noData))
|
||||
return
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
subscriber.receive(completion: .failure(.unknown(error)))
|
||||
} else {
|
||||
_ = subscriber.receive(data)
|
||||
subscriber.receive(completion: .finished)
|
||||
}
|
||||
return .just(data)
|
||||
}
|
||||
|
||||
// TODO: cancel task when subscriber cancelled
|
||||
task.resume()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
enum URLSessionError: Error {
|
||||
case invalidResponse
|
||||
case noData
|
||||
case serverError(statusCode: Int, error: Error?)
|
||||
case unknown(Error)
|
||||
case serverErrorMessage(statusCode: Int, data: Data)
|
||||
case urlError(URLError)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// AnySubscription.swift
|
||||
// GitHubSearchWithSwiftUI
|
||||
//
|
||||
// Created by John Holdsworth on 09/07/2019.
|
||||
// Copyright © 2019 jp.marty-suzuki. All rights reserved.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
/// - seealso: https://twitter.com/peres/status/1135970931153821696
|
||||
final class AnySubscription: Subscription {
|
||||
|
||||
private let cancellable: AnyCancellable
|
||||
|
||||
init(_ cancel: @escaping () -> Void) {
|
||||
self.cancellable = AnyCancellable(cancel)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {}
|
||||
|
||||
func cancel() {
|
||||
cancellable.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// ErrorResponse.swift
|
||||
// GitHubSearchWithSwiftUI
|
||||
//
|
||||
// Created by John Holdsworth on 09/07/2019.
|
||||
// Copyright © 2019 jp.marty-suzuki. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ErrorResponse: Decodable, Error {
|
||||
let message: String
|
||||
}
|
||||
@@ -10,27 +10,42 @@ import Combine
|
||||
import Foundation
|
||||
|
||||
enum RepositoryAPI {
|
||||
typealias SearchResponse = AnyPublisher<Result<[Repository], ErrorResponse>, Never>
|
||||
typealias SendRequest = (URLRequest) -> AnyPublisher<Data, URLSessionError>
|
||||
|
||||
static func search(query: String) -> AnyPublisher<[Repository], Error> {
|
||||
static func search(query: String) -> SearchResponse {
|
||||
search(query: query, sendRequest: URLSession.shared.combine.send)
|
||||
}
|
||||
|
||||
static func search(query: String, sendRequest: SendRequest) -> SearchResponse {
|
||||
|
||||
guard var components = URLComponents(string: "https://api.github.com/search/repositories") else {
|
||||
return Publishers.Empty<[Repository], Error>().eraseToAnyPublisher()
|
||||
return .empty()
|
||||
}
|
||||
components.queryItems = [URLQueryItem(name: "q", value: query)]
|
||||
|
||||
guard let url = components.url else {
|
||||
return Publishers.Empty<[Repository], Error>().eraseToAnyPublisher()
|
||||
return .empty()
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
return URLSession.shared.combine.send(request: request)
|
||||
return sendRequest(request)
|
||||
.decode(type: ItemResponse<Repository>.self, decoder: decoder)
|
||||
.map { $0.items }
|
||||
.handleEvents(receiveOutput: { print($0) },
|
||||
receiveCompletion: { print($0)})
|
||||
.map { Result<[Repository], ErrorResponse>.success($0.items) }
|
||||
.catch { error -> SearchResponse in
|
||||
guard case let .serverErrorMessage(_, data)? = error as? URLSessionError else {
|
||||
return .just(.success([]))
|
||||
}
|
||||
do {
|
||||
let response = try JSONDecoder().decode(ErrorResponse.self, from: data)
|
||||
return .just(.failure(response))
|
||||
} catch _ {
|
||||
return .just(.success([]))
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||
|
||||
// Use a UIHostingController as window root view controller
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.rootViewController = UIHostingController(rootView:
|
||||
RepositoryListView().environmentObject(RepositoryListViewModel())
|
||||
)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.rootViewController = UIHostingController(rootView: RepositoryListView(viewModel: RepositoryListViewModel(mainScheduler: DispatchQueue.main)))
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
|
||||
@@ -10,27 +10,51 @@ import SwiftUI
|
||||
|
||||
struct RepositoryListView : View {
|
||||
|
||||
@EnvironmentObject private var viewModel: RepositoryListViewModel
|
||||
@State private var text: String = ""
|
||||
@ObjectBinding
|
||||
private(set) var viewModel: RepositoryListViewModel
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationView {
|
||||
|
||||
TextField($text,
|
||||
placeholder: Text("Search reposipories..."),
|
||||
onCommit: { self.viewModel.search(query: self.text) })
|
||||
VStack {
|
||||
HStack {
|
||||
|
||||
TextField($viewModel.text,
|
||||
placeholder: Text("Search reposipories..."),
|
||||
onCommit: { self.viewModel.search() })
|
||||
.frame(height: 40)
|
||||
.padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8))
|
||||
.border(Color.gray, cornerRadius: 8)
|
||||
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||
|
||||
Button(action: { self.viewModel.search() }) {
|
||||
Text("Search")
|
||||
}
|
||||
.frame(height: 40)
|
||||
.padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8))
|
||||
.border(Color.gray, cornerRadius: 8)
|
||||
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||
.border(Color.blue, cornerRadius: 8)
|
||||
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16))
|
||||
}
|
||||
|
||||
List {
|
||||
|
||||
viewModel.errorMessage.map(Text.init)?
|
||||
.lineLimit(nil)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
ForEach(viewModel.repositories.identified(by: \.id)) { repository in
|
||||
RepositoryView(repository: repository)
|
||||
|
||||
NavigationLink(destination:
|
||||
WebView(url: repository.htmlUrl)
|
||||
.navigationBarTitle(Text(repository.fullName))
|
||||
) {
|
||||
|
||||
RepositoryView(repository: repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(Text("Search🔍"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,39 +11,60 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class RepositoryListViewModel: BindableObject {
|
||||
typealias SearchRepositories = (String) -> AnyPublisher<Result<[Repository], ErrorResponse>, Never>
|
||||
|
||||
let didChange: AnyPublisher<RepositoryListViewModel, Never>
|
||||
private let _didChange = PassthroughSubject<RepositoryListViewModel, Never>()
|
||||
|
||||
private let _searchWithQuery = PassthroughSubject<String, Never>()
|
||||
private lazy var repositoriesAssign = Subscribers.Assign(object: self, keyPath: \.repositories)
|
||||
private var cancellables: [AnyCancellable] = []
|
||||
|
||||
private(set) var repositories: [Repository] = [] {
|
||||
didSet {
|
||||
// TODO: Do not want to use DispatchQueue.main here
|
||||
DispatchQueue.main.async {
|
||||
self._didChange.send(self)
|
||||
}
|
||||
_didChange.send(self)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
repositoriesAssign.cancel()
|
||||
private(set) var errorMessage: String? {
|
||||
didSet {
|
||||
_didChange.send(self)
|
||||
}
|
||||
}
|
||||
var text: String = ""
|
||||
|
||||
init<S: Scheduler>(searchRepositories: @escaping SearchRepositories = RepositoryAPI.search,
|
||||
mainScheduler: S) {
|
||||
|
||||
init() {
|
||||
self.didChange = _didChange.eraseToAnyPublisher()
|
||||
|
||||
_searchWithQuery
|
||||
.flatMap { query -> AnyPublisher<[Repository], Never> in
|
||||
RepositoryAPI.search(query: query)
|
||||
.replaceError(with: [])
|
||||
let response = _searchWithQuery
|
||||
.filter { !$0.isEmpty }
|
||||
.debounce(for: .milliseconds(300), scheduler: mainScheduler)
|
||||
.flatMapLatest { query -> AnyPublisher<([Repository], String?), Never> in
|
||||
searchRepositories(query)
|
||||
.map { result -> ([Repository], String?) in
|
||||
switch result {
|
||||
case let .success(repositories):
|
||||
return (repositories, nil)
|
||||
case let .failure(response):
|
||||
return ([], response.message)
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.receive(subscriber: repositoriesAssign)
|
||||
.receive(on: mainScheduler)
|
||||
.share()
|
||||
|
||||
cancellables += [
|
||||
response
|
||||
.map { $0.0 }
|
||||
.assign(to: \.repositories, on: self),
|
||||
response
|
||||
.map { $0.1 }
|
||||
.assign(to: \.errorMessage, on: self)
|
||||
]
|
||||
}
|
||||
|
||||
func search(query: String) {
|
||||
_searchWithQuery.send(query)
|
||||
func search() {
|
||||
_searchWithQuery.send(text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// WebView.swift
|
||||
// GitHubSearchWithSwiftUI
|
||||
//
|
||||
// Created by marty-suzuki on 2019/06/11.
|
||||
// Copyright © 2019 jp.marty-suzuki. All rights reserved.
|
||||
//
|
||||
|
||||
import SafariServices
|
||||
import SwiftUI
|
||||
|
||||
struct WebView: UIViewControllerRepresentable {
|
||||
|
||||
let url: URL
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<WebView>) -> SFSafariViewController {
|
||||
return SFSafariViewController(url: url)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<WebView>) {}
|
||||
}
|
||||
Reference in New Issue
Block a user