I am new to Swift and even more so to SwiftUI. I started to create a little basic project. I use Github API to fetch repositories list.
So I created a "Search Bar" like since SwiftUI doesn't have a SearchBar component. I would like to perform the fetch operation everytime my Textfield content is changed.
I don't want the fetch method to be called too often. I decided to debounce it. I'm facing a problem, I don't find/understand example.
I tried to implement a debounce solution but it doesn't work, my application just crash.
Here's my BindableObject
import SwiftUI
import Combine
class ReposStore: BindableObject {
private var service: GithubService
let didChange = PassthroughSubject<Void, Never>()
@Published var searchText: String = ""
var repos: [Repository] = [] {
didSet {
didChange.send()
}
}
var error: String = "" {
didSet {
didChange.send()
}
}
var test: String = "" {
didSet {
didChange.send()
}
}
private var cancellable: AnyCancellable? = nil
init(service: GithubService) {
self.service = service
cancellable = AnyCancellable($searchText
.removeDuplicates()
.debounce(for: 2, scheduler: DispatchQueue.main)
.flatMap { self.fetch(matching: $0) }
.assign(to: \.test, on: self)
)
}
func fetch(matching query: String = "") {
print("### QUERY \(query)")
self.service.getUserRepositories(matching: query) { [weak self] result in
DispatchQueue.main.async {
print("### RESULT HERE \(result)")
switch result {
case .success(let repos): self?.repos = repos
case .failure(let error): self?.error = error.localizedDescription
}
}
}
}
}
And this is my View
import SwiftUI
struct RepositoryList : View {
@EnvironmentObject var repoStore: ReposStore
@State private var userName: String = ""
var body: some View {
VStack {
NavigationView {
VStack(spacing: 0) {
HStack {
Image(systemName: "magnifyingglass").background(Color.blue).padding(.leading, 10.0)
TextField($repoStore.repoUser, placeholder: Text("Search")).background(Color.red)
.padding(.vertical, 4.0)
.padding(.trailing, 10.0)
}
.border(Color.secondary, width: 1, cornerRadius: 5)
.padding()
List {
ForEach(self.repoStore.repos) { repository in
NavigationLink(
destination: RepositoryDetail(repository: repository).environmentObject(self.repoStore)
) {
RepositoryRow(repository: repository)
}
}
}.navigationBarTitle(Text("Repositories"))
}
}
}
}
I tried to use a Timer and schedule and action every 8 seconds but this method cause my application to crash.
More, I don't really know if it's a good practice to declare a function with "@objc" annotation ...
Could someone help me to implement a correct way to debounce a method inside a BindableObject ?
Thank you in advance :)
Debouncing makes sure that an execution runs only after the firing of that execution stops for a given time.
debounce(for:scheduler:options:) Publishes elements only after a specified time interval elapses between events.
I finally managed to set up the debounce.
If it could helps someone, here's my implementation :
import SwiftUI
import Combine
class Store : ObservableObject {
private var cancellable: AnyCancellable? = nil
@Published var searchText: String= ""
@Published var user: User? = nil
init() {
cancellable = AnyCancellable(
$searchText.removeDuplicates()
.debounce(for: 0.8, scheduler: DispatchQueue.main)
.sink { searchText in
self.searchUser()
})
}
func searchUser() {
var urlComponents = URLComponents(string: "https://api.github.com/users/\(searchText)")!
urlComponents.queryItems = [
URLQueryItem(name: "access_token", value: EnvironmentConfiguration.shared.github_token)
]
var request = URLRequest(url: urlComponents.url!)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
searchCancellable = URLSession.shared.send(request: request)
.decode(type: User.self, decoder: JSONDecoder())
.map { $0 }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.assign(to: \.user, on: self)
}
}
extension URLSession {
func send(request: URLRequest) -> AnyPublisher<Data, URLSessionError> {
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()
}
enum URLSessionError: Error {
case invalidResponse
case serverErrorMessage(statusCode: Int, data: Data)
case urlError(URLError)
}
}
extension Publisher {
static func empty() -> AnyPublisher<Output, Failure> {
return 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 Fail(error: error)
.eraseToAnyPublisher()
}
}
struct User: Hashable, Identifiable, Decodable {
var id: Int
var login: String
var avatar_url: URL
var name: String?
enum CodingKeys: String, CodingKey {
case id, login, avatar_url, name
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With