Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Debounce method call from BindableObject in SwiftUI

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

like image 814
Hurobaki Avatar asked Jul 23 '19 16:07

Hurobaki


People also ask

What is debounce in Swiftui?

Debouncing makes sure that an execution runs only after the firing of that execution stops for a given time.

What is debounce in IOS?

debounce(for:scheduler:options:) Publishes elements only after a specified time interval elapses between events.


1 Answers

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
    }
}
like image 50
Hurobaki Avatar answered Sep 27 '22 21:09

Hurobaki