3 Feb 20205 min read

Search View in SwiftUI

For a few days I keep digging into the SwiftUI framework and as long as it's really fun to play with I think... it's usable just in a playground for now. It's quite limited and for a more advanced application might be just easier to go with a UIKit. There is plenty of things which UIKit contains but have to be implemented in SwiftUI, and one of those things is a SearchView. In this week's article let's take a closer look at how to do it with a new fancy framework.
We can implement a search view in two ways. The first one is about wrapping the UIKit's SearchBar so it can be usable in SwiftUI, and the other one is to implement the view from scratch.

Final Result

Wrapping UISearchBar

Thankfully developers responsible for the SwiftUI framework predicted that people will rather want to use UIKit components as well as many UIViews that are already implemented in their projects. So wrapping UISearchBar is as simple as conforming to the UIViewRepresentable protocol where we have to implement three methods as you can see below.
Coordinator class will be responsible for communicating changes that occur in your UIKit view to other parts of your SwiftUI interface. For example, you can use a coordinator to delegate messages from your wrapped view to other SwiftUI views.

struct SearchBar: UIViewRepresentable {
    // 1
    @Binding var text: String

    // 2
    func makeUIView(context: Context) -> UISearchBar {
        let searchBar = UISearchBar()
        searchBar.delegate = context.coordinator

        return searchBar
    }

    // 3
    func updateUIView(_ uiView: UISearchBar, context: Context) {
        uiView.text = text
    }

    // 4
    func makeCoordinator() -> SearchBar.Coordinator {
        Coordinator(text: $text)
    }

    // 5
    class Coordinator: NSObject, UISearchBarDelegate {
        @Binding var text: String

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

        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            text = searchText
        }
    }
}
  1. We will need a Binding of a String type so we can communicate text changes in our SearchBar to the other view
  2. Creates a UISearchBar instance and sets the delegate to the Coordinator's object
  3. In this function is used to update UISearchBar from the other SwiftUI's view
  4. Creates a coordinator
  5. Coordinator class that conforms to the previously set delegate so we can react to text changes in UISearchBar

And that's all you need to wrap the UISearchBar (or any other UIKit's view).

INDIE DEV
MusicHarbor
MusicHarbor

MusicHarbor is an App Store featured app that helps you to track new music releases, music videos, concert dates and news from your favorite artists. Think of it as your personal, chronological feed of every single new release from the artists you follow. It has support for the latest iOS features, and is highly customizable, so you can configure it to work the way you want!

SwiftUI all the way

What if we would like to do it purely in SwiftUI though? It's not that hard in this case either and can do it in only a few lines of code, without having to add any external Assets as we can just use the system's images for it! Isn't that great? Just check the code below.

struct SearchBar: View {
    @Binding var text: String

    var body: some View {
        HStack {
            HStack {
                Image(systemName: "magnifyingglass")

                TextField("Search", text: $text)
                    .foregroundColor(.primary)

                if !text.isEmpty {
                    Button(action: {
                        self.text = ""
                    }) {
                        Image(systemName: "xmark.circle.fill")
                    }
                } else {
                    EmptyView()
                }
            }
            .padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)
            .foregroundColor(.secondary)
            .background(Color(.secondarySystemBackground))
            .cornerRadius(10.0)
        }
        .padding(.horizontal)
    }
}

Improvements

There is still one thing missing that we rather need - closing the keyboard on list drag. It's not that simple in SwiftUI either, there is no easy way to resign the first responder. We need a little bit more of extra code - first of all an UIApplication extension to force the end of editing of any currently focused text field.

extension UIApplication {
    func endEditing(_ force: Bool) {
        self.windows
            .filter{$0.isKeyWindow}
            .first?
            .endEditing(force)
    }
}

And now we can attach a drag gesture to our list's view.

.gesture(DragGesture().onChanged { _ in
                UIApplication.shared.endEditing(true)
            })

It should resign the first responder correctly while dragging a list now.

Usage

Now we just have to create a view containing our search bar and a list.

struct SearchView: View {
    let array = "SwiftUI is great but some views might need an extra work".components(separatedBy: " ")

    @State private var searchText = ""

    var body: some View {
        VStack {
            SearchBar(text: $searchText)

            List {
                ForEach(array.filter{$0.hasPrefix(searchText) || searchText == ""}, id:\.self) {
                    searchText in Text(searchText)
                }
            }
            .gesture(DragGesture().onChanged { _ in
                UIApplication.shared.endEditing(true)
            })
        }
    }
}

Here we use our previously created SearchBar (either the pure SwiftUI or the one using UISearchBar) and add a List of some items to search for as well as we attach a drag gesture to the List so we can hide keyboard on list drfagging.

Conclusion

We implemented SearchView using two techniques - a pure SwiftUI and wrapping UIKit's SearchBar component. I have no idea whether such views will be ever delivered in SwiftUI framework or will we always have to implement it ourselves but I really, really hope that they will single out this framework and release new versions independently as having to wait a year for major improvements is a really long time. They could even open-source it - why not? That would be even better I guess as we could implement any fixes or improvements on our own... sometimes there is just this little thing that is missing from the current implementation and we have re-do everything from scratch because there is no way to modify it in any way.

Thank you for reading!