在 SwiftUI 中从 BindableObject 调用 Debounce 方法

Debounce method call from BindableObject in SwiftUI

我是 Swift 的新手,对 SwiftUI 更是如此。我开始创建一个小的基本项目。我使用 Github API 来获取存储库列表。

所以我创建了一个 "Search Bar",因为 SwiftUI 没有 SearchBar 组件。每次更改我的文本字段内容时,我都想执行获取操作。

我不希望 fetch 方法被调用得太频繁。我决定去抖动它。我遇到了一个问题,我没有 find/understand 示例。

我尝试实施去抖动解决方案,但它不起作用,我的应用程序崩溃了。

这是我的 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: [=10=]) }
            .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
                }
            }
        }
    }
}

这是我的观点

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"))
                }
            }
        }
    }

我尝试使用计时器并每 8 秒安排一次操作,但这种方法导致我的应用程序崩溃。

此外,我真的不知道用“@objc”注释声明一个函数是否是一个好习惯...

有人可以帮我实现一种正确的方法来对 BindableObject 中的方法进行去抖动吗?

提前谢谢你:)

我终于成功设置了去抖动。

如果它可以帮助某人,这是我的实现:

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 { [=10=] }
        .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([=10=]) }
            .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
    }
}