skip to Main Content

I would like to make a search bar in this view but I am wondering how I go about calling my completion block which fills the data. At the moment I have the completion block in .onAppear but when I go to add a search bar, the data will only be loaded once in .onAppear and so when I search, the results won’t update. Here is my view

struct CocktailList: View {
    @State private var cocktails: [Cocktail]
    @State private var searchText = ""
    var body: some View {
        VStack {
            NavigationView {
                List(cocktails, id: .idDrink) { cocktail in
                    NavigationLink(destination: CocktailDetail(fromCocktail: cocktail)) {
                        CocktailRow(fromCocktail: cocktail)
                    }
                }
                .navigationTitle("Search Results")
            }.onAppear {
                requestCocktail(searchTerm: "daiquiri") { cocktails in
                    self.cocktails = cocktails
                }
            }
        }
    }
    
    init() {
        self.cocktails = []
    }
}

How do I call requestCocktail() upon the update of the searchText and what is the best way to integrate a simple search bar?

2

Answers


  1. I gave you an example with a TextField and different related actions: onCommit, onEditing, when the text changes, when the text hasn’t changed for 1 second. It’s up to you to choose when you want to execute requestCocktail().

    import Combine
    import SwiftUI
    struct SwiftUIView11: View {
        @State private var text: String = ""
        @State private var result: String = ""
        let textHasChanged = PassthroughSubject<Void, Never>()
        let debouncer: AnyPublisher<Void, Never>
    
        init() {
            debouncer = textHasChanged
                .debounce(for: .seconds(1), scheduler: DispatchQueue.main)
                .eraseToAnyPublisher()
        }
    
        var body: some View {
            VStack {
                Text(result)
    
                TextField("", text: $text, onEditingChanged: {
                    isEditing in
                    result = isEditing ? "isEditing" : "isNotEditing"
                }, onCommit: {
                    result = "onCommit"
                })
                    .background(Color.yellow)
                .onChange(of: text) { newText in
                    result = "text changed : (newText)"
                    textHasChanged.send()
                }
                .onReceive(debouncer) { _ in
                    result = "paused editing"
                }
            }
        }
    }
    
    Login or Signup to reply.
  2. Start with a clear separation of concerns in mind:

    (Prerequisite: iOS 15)

    1. Your Cocktail View
    struct CocktailsView: View {
        let items: [String]
        @Binding var query: String
    
        var body: some View {
            List {
                ForEach(items, id: .self) { string in
                    Text(verbatim: string)
                }
            }
            .searchable(text: $query, prompt: "enter query")
        }
    }
    
    

    It displays your items and provides a "search bar".

    Note, that it does not change its view state and that is uses a binding for the query string to communicate it with its parent view.

    I also omitted some details from the original, since it is not relevant to the question.

    1. Provide a View Model:
    final class ViewModel: ObservableObject {
    
        // Defines the _what_ to render in the View
        struct ViewState {
            var query: String = ""
            var items: [String] = []
        }
    
        // A set of Inputs
        enum Event {
            case query(String)
        }
    
        private let input = PassthroughSubject<Event, Never>()
        private var cancellable: AnyCancellable!
    
        @Published private(set) var viewState: ViewState = .init()
    
    
        init() {
            // Sets up a system of pipes, loosely resembling a 
            // FSM with extended state but no outputs:
            cancellable = input
                .prepend(.query(""))
                .map { event in
                    return self.update(state: self.viewState, event: event)
                }
                .sink(receiveValue: { newState in
                    self.viewState = newState
                })
        }
    
        // We don't want to expose the PassthroughSubject to the 
        // view, so define a convenience function that hides 
        // the details for Views:
        func send(_ event: Event) {
            input.send(event)
        }
    
        // Combines the _transition_ an the _output_ function 
        // of the FSM. It's pretty easy in this use case.
        private func update(state: ViewState, event: Event) -> ViewState {
            let newState: ViewState
            switch (self.viewState, event) {
            case (_, .query(let query)) where query.isEmpty:
                newState = .init(query: "", items: self.items)
            case (_, .query(let query)):
                let query = query.lowercased()
                newState = .init(query: query,
                                 items: self.items.filter { $0.lowercased().contains(query) } )
            }
            print("update((state), (event) -> (newState)" )
            return newState
        }
    
        private let items: [String] =
            "quis ipsum suspendisse ultrices gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim sit amet venenatis urna cursus eget"
            .split(separator: " ")
            .map { String($0) }
    
    }
    

    Here, the View Model is "event driven" and uses Combine under the hood. It also resembles a "extended finite state machine" where "input" are the inputs of the FSM. But, really, the is pretty irrelevant – it’s an implementation detail – and just an example how you can implement a view model.

    With an FSS you need an update function. It is a pure function solely responsible to generate a new View State from the current view state and the event.

    1. Use a Content View which puts the things together:
    struct ContentView: View {
        @ObservedObject var viewModel: ViewModel
    
        var body: some View {
            let binding: Binding<String> = .init {
                viewModel.viewState.query
            } set: { query in
                viewModel.send(.query(query))
            }
    
            NavigationView {
                CocktailsView(
                    items: viewModel.viewState.items,
                    query: binding)
                .navigationTitle("Results")
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search