iOS SwiftUI Searchbar и REST-API - PullRequest
       16

iOS SwiftUI Searchbar и REST-API

1 голос
/ 19 сентября 2019

Я экспериментирую с SwiftUI и хочу получить обновление из моего REST API со строкой поиска.

Тем не менее, я не уверен, как теперь объединить эти два компонента.

Я надеюсь, у вас есть идея.

Вот мой код:

struct ContentView: View {
    @State private var searchTerm: String = ""
    @ObservedObject var gameData: GameListViewModel = GameListViewModel(searchString: ### SEARCH STRING ???? ###)

    var body: some View {
        NavigationView{
            Group{
                // Games werden geladen...
                if(self.gameData.isLoading) {
                    LoadingView()
                }
                // Games sind geladen:
                else{
                    VStack{
                        // Suche:
                        searchBarView(text: self.$searchTerm)

                        // Ergebnisse:
                        List(self.gameData.games){ game in
                            NavigationLink(destination: GameDetailView(gameName: game.name ?? "0", gameId: 0)){
                                HStack {
                                    VStack(alignment: .leading, spacing: 2) {
                                        Text(game.name ?? "Kein Name gefunden")
                                            .font(.headline)
                                        Text("Cover: \(game.cover?.toString() ?? "0")")
                                            .font(.subheadline)
                                            .foregroundColor(.gray)
                                    }
                                }
                            }
                        }
                    }
                }
           }
       .navigationBarTitle(Text("Games"))
       }
   }
}

И реализация панели поиска:

import Foundation
import SwiftUI

struct searchBarView: UIViewRepresentable {

    @Binding var text:String

    class Coordinator: NSObject, UISearchBarDelegate {

        @Binding var text: String

        init(text: Binding<String>){
            _text = text
        }

        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            print(searchText)
            text = searchText
        }
    }
    func makeCoordinator() -> searchBarView.Coordinator {
        return Coordinator(text: $text)
    }
    func makeUIView(context: UIViewRepresentableContext<searchBarView>) -> UISearchBar {
        let searchBar = UISearchBar(frame: .zero)
        searchBar.delegate = context.coordinator
        return searchBar
    }
    func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<searchBarView>) {
        uiView.text = text
    }
}

Ответы [ 2 ]

0 голосов
/ 20 сентября 2019

Текст для поиска должен быть внутри модели представления.

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
    }

}
0 голосов
/ 19 сентября 2019

Нет необходимости подключать UIKit, вы можете объявить простую строку поиска следующим образом:

struct SearchBar: View {

    @State var searchString: String = ""

    var body: some View {

        HStack {
            TextField("Start typing",
                      text: $searchString,
                      onCommit: { self.performSearch() })
                .textFieldStyle(RoundedBorderTextFieldStyle())
            Button(action: { self.performSearch() }) {
                Image(systemName: "magnifyingglass")
            }
        }   .padding()
    }

    func performSearch() {

    }
}

, а затем поместить логику поиска в executeSearch ().

...