SwiftUI UISearchController replacement: search field, results and some scrollable content fail to coexist in a meaningful manner - search

Starting with this
var body: some View {
ScrollView {
VStack(spacing: 0.0) {
Some views here
}
}
.edgesIgnoringSafeArea(.top)
}
How would I add
List(suggestions, rowContent: { text in
NavigationLink(destination: ResultsPullerView(searchText: text)) {
Text(text)
}
})
.searchable(text: $searchText)
on top if that scrollable content?
Cause no matter how I hoax this together when
#State private var suggestions: [String] = []
gets populated (non empty) the search results are not squeezed in (or, better yet, shown on top of
"Some views here"
So what I want to achieve in different terms: search field is on top, scrollable content driven by the search results is underneath, drop down with search suggestions either temporarily squeeses scrollable content down or is overlaid on top like a modal sheet.
Thanks!

If you are looking for UIKit like search behaviour you have to display your results in an overlay:
1. Let's declare a screen to display the results:
struct SearchResultsScreen: View {
#Environment(\.isSearching) private var isSearching
var results: [String]?
var body: some View {
if isSearching, let results {
if results.isEmpty {
Text("nothing to see here")
} else {
List(results, id: \.self) { fruit in
NavigationLink(destination: Text(fruit)) {
Text(fruit)
}
}
}
}
}
}
2. Let's have an ObservableObject to handle the logic:
class Search: ObservableObject {
static private let fruit = [
"Apples 🍏",
"Cherries 🍒",
"Pears 🍐",
"Oranges 🍊",
"Pineapples 🍍",
"Bananas 🍌"
]
#Published var text: String = ""
var results: [String]? {
if text.isEmpty {
return nil
} else {
return Self.fruit.filter({ $0.contains(text)})
}
}
}
3. And lastly lets declare the main screen where the search bar is displayed:
struct ContentView: View {
#StateObject var search = Search()
var body: some View {
NavigationView {
LinearGradient(colors: [.orange, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
.overlay(SearchResultsScreen(results: search.results))
.searchable(text: $search.text)
.navigationTitle("Find that fruit")
}
}
}

Related

How to select an item from a search file and place in textfield in another file

Using SwiftUI - Xcode 14.2 - iOS 16.0
I have tried different search tutorials to create a search file for my project but am unable to find out how to select the item in the search file and place that selected item in a textfield in another file. I have searched this site for other posts, i tried searching through Google, YouTube, etc...
In File 1, I have a textfield that that has a prompt 'start typing' and when selected, it directs you to the Search file to select the item you want, so it can be placed in place of the prompt.
File 1 (where the textfield is needed to paste the selected item):
VStack {
NavigationLink(destination: NameSearch()) {
TextField("Name", text: .constant(""), prompt: Text(" Start typing ")
.foregroundColor(.blue))
.multilineTextAlignment(.leading)
.padding()
}
}
Once I click on the 'start typing' prompt, it navigates to NameSearch.swift file, as seen below.
NameSearch.swift:
import SwiftUI
struct NameSearch: View {
let name = [
"Jane", "George", "Sam", "Henry", "Sally", "Liz", "John"
]
#State private var searchText = ""
var body: some View {
NavigationStack {
VStack {
// Search view
SearchBarView(searchText: $searchText)
List {
// Filtered list of names
ForEach(name.filter{$0.hasPrefix(searchText) || searchText == ""}, id:\.self) {
searchText in Text(searchText)
}
}
.navigationBarTitle(Text("Search Name"))
.resignKeyboardOnDragGesture()
}
}
}
}
struct NameSearch_Previews: PreviewProvider {
static var previews: some View {
Group {
NameSearch()
.environment(\.colorScheme, .light)
NameSearch()
.environment(\.colorScheme, .dark)
}
}
}
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
struct ResignKeyboardOnDragGesture: ViewModifier {
var gesture = DragGesture().onChanged{_ in
UIApplication.shared.endEditing(true)
}
func body(content: Content) -> some View {
content.gesture(gesture)
}
}
extension View {
func resignKeyboardOnDragGesture() -> some View {
modifier(ResignKeyboardOnDragGesture())
}
}
struct SearchBarView: View {
#Binding var searchText: String
#State private var showCancelButton: Bool = false
var onCommit: () ->Void = {print("onCommit")}
var body: some View {
HStack {
HStack {
Image(systemName: "magnifyingglass")
// Search text field
ZStack (alignment: .leading) {
if searchText.isEmpty { // Separate text for placeholder to give it the proper color
Text("Search")
}
TextField("", text: $searchText, onEditingChanged: { isEditing in
self.showCancelButton = true
}, onCommit: onCommit).foregroundColor(.primary)
}
// Clear button
Button(action: {
self.searchText = ""
}) {
Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.secondary) // For magnifying glass and placeholder test
.background(Color(.tertiarySystemFill))
.cornerRadius(10.0)
if showCancelButton {
// Cancel button
Button("Cancel") {
UIApplication.shared.endEditing(true) // this must be placed before the other commands here
self.searchText = ""
self.showCancelButton = false
}
.foregroundColor(Color(.systemBlue))
}
}
.padding(.horizontal)
.navigationBarHidden(showCancelButton)
}
}
Question 1: How do I hide all the names from showing in the list so that I just see the search bar and the cancel button and an empty list?
Question 2: Once I type the name I am looking for, it should pop up and I want to select name - how can I do this?
once I type the name in search bar, it appears in the empty list
I select that name
it then takes me back to File 1
replaces the 'start typing' prompt with the name i just selected in the Search file.
Question 3: I have noticed in the Search file, I am getting a warning with the following code. How can I resolve it?
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
The warning that appears is:
'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a
relevant window scene instead
Firstly, thank you for providing a working example of your code.
As you're building for iOS 15+, you should probably be using the .searchable modifier rather than rolling your own.
The 2021 WWDC video introducing this feature is here https://developer.apple.com/wwdc21/10176
Some new features from 2022 here: https://developer.apple.com/wwdc22/10052

SwiftUI - Perform action when cancel is clicked - .searchable function

When using the .searchable(text: $text) function, a cancel button appears in the search bar when searching.
Is there any way to perform an action when the cancel button is clicked? I would like to call a function when cancel is clicked, but cannot figure out how to perform an action when cancel is tapped.
The Apple Documentation does not mention anything about this. Back in UIKit there was the func searchBarCancelButtonClicked(searchBar: UISearchBar) { to do this.
Below is an image of the cancel button I am referring to:
You can use the isSearching environment value (https://developer.apple.com/documentation/swiftui/environmentvalues/issearching?changes=_6) to see if a search is being performed. To do an action upon cancelation, you could watch for a change from true to false using onChange:
struct ContentView: View {
#State private var searchText = ""
#Environment(\.dismissSearch) var dismissSearch
var body: some View {
NavigationView {
VStack {
ChildView()
Text("Searching for \(searchText)")
}
.searchable(text: $searchText)
.navigationTitle("Searchable Example")
}
}
}
struct ChildView : View {
#Environment(\.isSearching) var isSearching
var body: some View {
Text("Child")
.onChange(of: isSearching) { newValue in
if !newValue {
print("Searching cancelled")
}
}
}
}
Probably important to note that it seems like isSearching has to be inside a child view of the searchable modifier in order for it to work properly
Based on #jnpdx 's answer, something equivalent, but more generic is:
struct SearchView<Content: View>: View {
#Environment(\.isSearching) var isSearching
let content: (Bool) -> Content
var body: some View {
content(isSearching)
}
init(#ViewBuilder content: #escaping (Bool) -> Content) {
self.content = content
}
}
And then, use it like:
struct ContentView: View {
#State private var searchText = ""
#Environment(\.dismissSearch) var dismissSearch
var body: some View {
NavigationView {
VStack {
SearchView { isSearching in
Text("Child")
.onChange(of: isSearching) { newValue in
if !newValue {
print("Searching cancelled")
}
}
}
Text("Searching for \(searchText)")
}
.searchable(text: $searchText)
.navigationTitle("Searchable Example")
}
}
}
use isEmpty and onAppear.
struct SearchView: View {
#State var text: String = ""
var body: some View {
NavigationView {
VStack {
if text.isEmpty {
main.onAppear {
print("empty")
// code here
}
} else {
main
}
}.searchable(text: $text)
.onSubmit(of: .search) {
print("submit")
}
}
}
var main: some View {
Text("search").searchable(text: $text)
}
}

Pass FetchedResults through NavigationLink

I have a two CoreData objects:
RoadTrip
StatePlate.
Each RoadTrip items holds an NSSet of StatePlate.
Screen 1 (TripList) shows a list of all RoadTrip items. Screen 2 (StateList) shows a list of all StatePlate items in associated with the RoadTrip that a user selects. Selecting a StatePlate item in Screen 2 will toggle a bool value associated with that item.
Even though I can show the data and can toggle the bool value of each StatePlate, I am not seeing an immediate change to the UI of the screen. The StatePlate should jump from Section to Section in Screen 2 when it's bool value is toggled.
How can I pass this FetchedObject correctly from Screen 1 to Screen 2 so the UI is binded with the data?
Screen 1 (TripList)
struct TripList: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: RoadTrip.entity(), sortDescriptors: []) var roadTripItems: FetchedResults<RoadTrip>
var body: some View {
List {
ForEach(roadTripItems, id: \.self) { trip in
NavigationLink(destination: StateList(trip: trip)
.environment(\.managedObjectContext, self.managedObjectContext)) {
TripRow(roadTrip: trip)
}
}
}
}
}
Screen 2 (StateList)
struct StateList: View {
#Environment(\.managedObjectContext) var managedObjectContext
var trip: RoadTrip
var plates: [StatePlate] {
trip.plateArray
}
var unseenPlates: [StatePlate] {
trip.plateArray.filter { !$0.hasBeenSeen }
}
var seenPlates: [StatePlate] {
trip.plateArray.filter { $0.hasBeenSeen }
}
var body: some View {
List {
if !unseenPlates.isEmpty {
Section(header: Text("Unseen Plates")) {
ForEach(unseenPlates, id: \.self) { plate in
StateRow(plate: plate)
}
}
}
if !seenPlates.isEmpty {
Section(header: Text("Seen Plates")) {
ForEach(seenPlates, id: \.self) { plate in
StateRow(plate: plate)
}
}
}
}
}
}
StateRow
struct StateRow: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var plate: StatePlate
var body: some View {
Button(action: {
self.plate.hasBeenSeen.toggle()
try? self.managedObjectContext.save()
}) {
HStack {
Text(String(describing: plate.name!))
Spacer()
if plate.hasBeenSeen {
Image(systemName: "eye.fill")
} else {
Image(systemName: "")
}
}
}
}
}
Your trip as object is not changed when plate has changed, so even if it was observed UI was not refreshed.
Here is possible force-refresh approach.
struct StateList: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var trip: RoadTrip // << make observed
// .. other code
and add handling for updated plate/s
StateRow(plate: plate)
.onReceive(plate.objectWillChange) { _ in
self.trip.objectWillChange.send()
}

NavigationLinks are being grouped

I have a restaurant menu app that is grouping menu items inside of sections of the menu with NavigationLinks on each menu item which are intended to display a more detailed description of the item. All the menu items in a section are being grouped together as if they were just a single link and triggering the error "Fatal error: UIKitNavigationBridge: multiple active destinations: file SwiftUI". In other words, it is trying to display the detail for all the items within that section when you click on any individual item.
I'm doing this with a section view that displays the various sections and in turn, each section displays the items within that section.
It appears to be a bug in SwiftUI, but since I'm relatively new to SwiftUI, I thought I'd seek more seasoned advice.
import SwiftUI
struct MenuSectionView: View
{
#Environment(\.managedObjectContext) var managedObjectContext
#EnvironmentObject var env: GlobalEnvironment
var group: Group
var items: [MenuItem]
init(group: Group)
{
self.group = group
items = getMenuItems(businessid: group.businessid!, groupid: group.groupid)
}
var body: some View
{
VStack
{
ForEach (items, id: \.itemid)
{
itemx in
if group.groupid == itemx.groupid
{
MenuItemView(item: itemx)
}
}
}
}
}
import SwiftUI
import CoreData
struct MenuItemView: View
{
#Environment(\.managedObjectContext) var context
#EnvironmentObject var env: GlobalEnvironment
var item: MenuItem
init(item: MenuItem)
{
self.item = item
}
var body: some View
{
return VStack
{
NavigationLink(destination: DetailView(item: item))
{
VStack
{
HStack
{
if let image = item.image
{
Image(uiImage: UIImage(data: image)!).resizable().frame(width: 40, height: 40).cornerRadius(5)
} else
{
Image(item.name!).resizable().frame(width: 40, height: 40).cornerRadius(5)
}
Text(item.name!)
}
Text(item.desc!)
}
}
}
}
}
}
Apparently the VStack in the above example was the cause of the error/bug. I eliminated it and now the links work correctly. It is still a bug since in a more complex iteration, the VStack is needed. I found the same thing happens with Buttons within stacks.

SwiftUI: make layout relative to a central view

Assume I build a view like this:
struct MyView: View {
#State private var a: String
#State private var b: String
#State private var c: String
var body: some View {
VStack {
HStack {
Text(a)
// this is the central view
Text(b).font(.headline)
}
Text(c)
}
}
}
I would like the central text view (the one displaying b) to be the anchor of the layout. That is, no matter how other text values change, I would like the central text to always stay in the centre of MyView (the centre of the text element and the centre of MyView should stay identical) and the other text elements should be laid out around the central one.
How to I achieve this? I tried to look at alignment guides, but I just don't seem to understand how to use them properly.
After spending some time to learn how alignment works in detail, I managed to arrive at a solution that only uses stacks and custom alignments, with minimal alignment guides and without needing to save any intermediate state. It's purely declarative, so I am supposed this is how SwiftUI designers intended it. I still think that there might have been a better design for it, but one can work with it.
struct ContentView: View {
#State var a: String = "AAAAA"
#State var b: String = "BBBB"
#State var c: String = "CCCCCC"
var body: some View {
VStack {
ZStack(alignment: .mid) {
// create vertical and horizontal
// space to align to
HStack { Spacer() }
VStack { Spacer() }
VStack(alignment: .midX) {
Text(self.a)
HStack(alignment: .center) {
Text(self.c)
Text(self.b)
.font(.title)
.border(Color.blue)
.alignmentGuide(.midX) { d in
(d[.leading] + d[.trailing])/2
}
.alignmentGuide(.midY) { d in
(d[.top] + d[.bottom])/2
}
}
}
}
.layoutPriority(1.0)
.overlay(CrossHair().stroke(Color.pink, lineWidth: 2))
TextField("", text: self.$b).textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
fileprivate extension HorizontalAlignment {
enum MidX : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return (d[.leading] + d[.trailing])/2
}
}
static let midX = HorizontalAlignment(MidX.self)
}
fileprivate extension VerticalAlignment {
enum MidY : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return (d[.top] + d[.bottom])/2
}
}
static let midY = VerticalAlignment(MidY.self)
}
fileprivate extension Alignment {
static let mid = Alignment(horizontal: .midX, vertical: .midY)

Resources