iOS SwiftUI Searchbar and REST-API

前端 未结 2 1923
悲哀的现实
悲哀的现实 2021-02-04 22:14

I\'m experimenting with SwiftUI and would like to fetch an update from my REST API with a search string.

However, I\'m not sure how to bring the two components together

相关标签:
2条回答
  • 2021-02-04 22:49

    There is no need to get UIKit involved, you can declare a simple search bar like this:

    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() {
    
        }
    }
    

    and then place the search logic inside performSearch().

    0 讨论(0)
  • 2021-02-04 22:51

    The search text should be inside the view model.

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

    Each time onSearchTapped is called, it fires a request for new games.

    There's plenty of things going on here - let's start from requestGames.

    I'm using JSONPlaceholder free API to fetch some data and show it in the List.

    requestGames performs the network request, decodes [Game] from the received Data. In addition to that, the returned array is filtered using the search string (because of the free API limitation - in a real world scenario you'd use a query parameter in the request URL).

    Now let's have a look at the view model constructor.

    The order of the events is:

    • Get the "search tapped" subject.
    • Perform a network request (flatMap)
    • Inside the flatMap, loading logic is handled (dispatched on the main queue as isLoading uses a Publisher underneath, and there will be a warning if a value is published on a background thread).
    • replaceError changes the error type of the publisher to Never, which is a requirement for the assign operator.
    • receiveOn is necessary as we're probably still in a background queue, thanks to the network request - we want to publish the results on the main queue.
    • assign updates the array games on the view model.
    • store saves the Cancellable in the disposeBag

    Here's the view code (without the loading, for the sake of the demo):

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

    Search bar implementation:

    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 讨论(0)
提交回复
热议问题