Текст для поиска должен быть внутри модели представления.
final class GameListViewModel: ObservableObject {
@Published var isLoading: Bool = false
@Published var games: [Game] = []
var searchTerm: String = ""
private let searchTappedSubject = PassthroughSubject<Void, Error>()
private var disposeBag = Set<AnyCancellable>()
init() {
searchTappedSubject
.flatMap {
self.requestGames(searchTerm: self.searchTerm)
.handleEvents(receiveSubscription: { _ in
DispatchQueue.main.async {
self.isLoading = true
}
},
receiveCompletion: { comp in
DispatchQueue.main.async {
self.isLoading = false
}
})
.eraseToAnyPublisher()
}
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.assign(to: \.games, on: self)
.store(in: &disposeBag)
}
func onSearchTapped() {
searchTappedSubject.send(())
}
private func requestGames(searchTerm: String) -> AnyPublisher<[Game], Error> {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
return Fail(error: URLError(.badURL))
.mapError { $0 as Error }
.eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.mapError { $0 as Error }
.decode(type: [Game].self, decoder: JSONDecoder())
.map { searchTerm.isEmpty ? $0 : $0.filter { $0.title.contains(searchTerm) } }
.eraseToAnyPublisher()
}
}
Каждый раз, когда вызывается onSearchTapped
, запускается запрос на новые игры.
Там происходит много вещейздесь - давайте начнем с requestGames
.
Я использую JSONPlaceholder бесплатный API для получения некоторых данных и отображения их в списке.
requestGames
выполняет сетевой запрос, декодирует [Game]
из полученного Data
.Кроме того, возвращаемый массив фильтруется с использованием строки поиска (из-за ограничения бесплатного API - в реальном сценарии вы будете использовать параметр запроса в URL-адресе запроса).
Теперь давайте получимпосмотрите на конструктор модели представления.
Порядок событий:
- Получите тему "поиск коснулся".
- Выполните сетевой запрос (
flatMap
) - Внутри
flatMap
обрабатывается логика загрузки (отправляется в основную очередь, так как isLoading
использует Publisher
снизу, и будет предупреждение, если значение будет опубликовано в фоновом потоке). replaceError
изменяет тип ошибки издателя на Never
, что является требованием для оператора assign
. receiveOn
необходимо, поскольку мы, вероятно,все еще в фоновой очереди, благодаря сетевому запросу - мы хотим опубликовать результаты в главной очереди. assign
обновляет массив games
в модели представления. store
сохраняет Cancellable
в disposeBag
Вот код вида (без загрузки, для демонстрации):
struct ContentView: View {
@ObservedObject var viewModel = GameListViewModel()
var body: some View {
NavigationView {
Group {
VStack {
SearchBar(text: $viewModel.searchTerm,
onSearchButtonClicked: viewModel.onSearchTapped)
List(viewModel.games, id: \.title) { game in
Text(verbatim: game.title)
}
}
}
.navigationBarTitle(Text("Games"))
}
}
}
Реализация панели поиска :
struct SearchBar: UIViewRepresentable {
@Binding var text: String
var onSearchButtonClicked: (() -> Void)? = nil
class Coordinator: NSObject, UISearchBarDelegate {
let control: SearchBar
init(_ control: SearchBar) {
self.control = control
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
control.text = searchText
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
control.onSearchButtonClicked?()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}