1

So I have an SwiftUI view, that displays data in a List. You can search and filter through the objects. So far so easy, but this searchbar (initializised with .searchable()) is always visible. Only when you scroll down the list it disappears. How can I hide the search bar on appear and only when you pull down slightly it appears. Like in the default apple apps eg. notes. Is this possible with .searchable modifier or do i need to create a custom search bar, if so how? Here an example code:

        ZStack{
            NavigationView {
                List {
                    if(filteredTodayItems.isEmpty == false){
                        Section(header: ListHeader(text: "Today")) {
                            ForEach(filteredTodayItems) { item in
                                objectCell(item: item, rememberSheet: $showRememberSheet, currentItem: $currentItem, showRememberSheet: $showRememberSheet, showDetailSheet: $showDetailSheet, showAddingChangeSheet: $showAddingChangeSheet)
                            }
                            }
                    }else if(tomorrowItems.isEmpty && laterItems.isEmpty){
                        Text("Click on the Plus to enter a secret!")
                    }
                }
                .searchable(text: $searchText, prompt: Text("Search"))
                .navigationTitle("Secrets")
            }
        }

The funny thing i discovered now, is, that when i add .searchable(text: $searchText, prompt: Text("Search")) to the Section() it kind of works.. The search field is only displayed when pulling down, but the content of the sections is totally "destroyed", means swipe actions multiplied etc. When i add the modifier to an empty section at the top everything works, besides an empty section is visible

For better understanding, here is the full body:

        ZStack{
            NavigationView {
                    List {
                        if(filteredTodayItems.isEmpty == false){
                            Section(header: ListHeader(text: "Today")) {
                                ForEach(filteredTodayItems) { item in
                                    objectCell(item: item, rememberSheet: $showRememberSheet, currentItem: $currentItem, showRememberSheet: $showRememberSheet, showDetailSheet: $showDetailSheet, showAddingChangeSheet: $showAddingChangeSheet, isToday: true)
                                }
                            }
                        }else if(tomorrowItems.isEmpty && laterItems.isEmpty){
                            Text("Click on the Plus to enter a secret!")
                        }
                        if(filteredTomorrowItems.isEmpty == false){
                            Section(header: ListHeader(text: "Tomorrow")) {
                                ForEach(filteredTomorrowItems) { item in
                                    objectCell(item: item, rememberSheet: $showRememberSheet, currentItem: $currentItem, showRememberSheet: $showRememberSheet, showDetailSheet: $showDetailSheet, showAddingChangeSheet: $showAddingChangeSheet, isToday: false)
                                }
                            }
                        }
                        if(filteredLaterItems.isEmpty == false){
                            Section(header: ListHeader(text: "Later")) {
                                ForEach(filteredLaterItems) { item in
                                    objectCell(item: item, rememberSheet: $showRememberSheet, currentItem: $currentItem, showRememberSheet: $showRememberSheet, showDetailSheet: $showDetailSheet, showAddingChangeSheet: $showAddingChangeSheet, isToday: false)
                                }
                            }
                        }
                    }
                    .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
                    .partialSheet(isPresented: $showRememberSheet) {
                        checkSecretView(currentItem: $currentItem, showRememberSheet: $showRememberSheet, showToast: $showToast, showToast2: $showToast2, value: $value)
                    }
                    .toolbar {
                        ToolbarItem (placement: .navigationBarLeading) {
                            Button(action: {
                                showSettings.toggle()
                            }) {
                                Image(systemName: "gear")
                            }
                        }
                        ToolbarItem (placement: .navigationBarTrailing) {
                            Button(action: {
                                showAddingSheet.toggle()
                            }) {
                                Image(systemName: "plus")
                            }
                        }
                    }
                    .listStyle(InsetGroupedListStyle())
                    .navigationTitle("Secrets")
            }
            .sheet(isPresented: $showAddingSheet) {
                AddingSheet(editingItem: nil)
            }
            .sheet(isPresented: $showDetailSheet) {
                InfoSheet(currentItem: $currentItem)
            }
            .sheet(isPresented: $showSettings) {
                Settings()
            }
            .sheet(isPresented: $showAddingChangeSheet) {
                let idString = UserDefaults.standard.string(forKey: "ToBeEditedLiveGoalID")
                let id = UUID(uuidString: idString ?? "")
                let objectToEdit = items.first(where: { $0.id == id})
                AddingSheet(editingItem: objectToEdit)
            }
            .attachPartialSheetToRoot()
            .simpleToast(isPresented: $showToast, options: toastOptions){
                HStack{
                    Image(systemName: "checkmark.circle.fill")
                    Text("Correctly Entered").bold()
                }
                .padding(.vertical,8)
                .padding(.horizontal,16)
                .background(.green.opacity(0.9))
                .foregroundColor(.white)
                .cornerRadius(10)
            }
            .simpleToast(isPresented: $showToast2, options: toastOptions){
                HStack{
                    Image(systemName: "xmark.circle.fill")
                    Text("Wrongly Entered").bold()
                }
                .padding(.vertical,8)
                .padding(.horizontal,16)
                .background(.red.opacity(0.9))
                .foregroundColor(.white)
                .cornerRadius(10)
            }
        }
        .blur(radius: isUnlocked ? 0 : 25)
        .disabled(!isUnlocked)
        .overlay(
            lockedScreen(color: Binding.constant(.accentColor) , isUnlocked: $isUnlocked)
                .opacity(isUnlocked ? 0 : 1)
        )
        .onAppear{
            isUnlocked = !bioSaveAuthActive
            if(!isUnlocked){
                authenticate()
            }
        }
    }
Black Fox
  • 23
  • 8

5 Answers5

1

Try removing the ZStack, and moving the .searchable modifier and the if statements around, so that your code looks something like this:

       NavigationView {
            Group {
            if !filterTodayItems.isEmpty {
                List {
                    Section(header: ListHeader(text: "Today")) {
                        ForEach(filteredTodayItems) { item in
                            objectCell(item: item, rememberSheet: $showRememberSheet, currentItem: $currentItem, showRememberSheet: $showRememberSheet, showDetailSheet: $showDetailSheet, showAddingChangeSheet: $showAddingChangeSheet)
                        }
                    }
                    .searchable(text: $searchText, prompt: Text("Search"))
                }
            } else {
                Text("Click on the Plus to enter a secret!")
            }
            }
            .navigationTitle("Secrets")
        }
  • Unfortunatly it still does not work... The first error I got was, that I can not add a .navigationTitle() to this kind of view. I added the full body to the question, so you can understand better, how it looks in total. – Black Fox Jul 16 '23 at 11:56
  • Sorry I edited the reply, you can wrap the if statements around a ```Group {}``, which will allow the navigationTitle to work – iTech Everything Jul 16 '23 at 12:19
  • when I do this, it brings me back to another issue. A) The Section title, seams now being in a cell and also centered, not anymore leading. B) the bigger issue, when I try to swipe on of the cells, it swipes all. Hard to describe, but they sem to be now all in one cell and when I eg swipe right instead of one delete action, i get 8! (number of cells) times this action – Black Fox Jul 16 '23 at 13:05
  • Ah I see! Not sure exactly why this is happening, it's also very hard to debug the code since it refers to other Views which aren't mentioned, so there's always a chance that the issue is actually in a sub view deep in the view hierarchy :P – iTech Everything Jul 16 '23 at 14:26
  • Yeah totally true, if you want I can create a git repo with the whole connected code, as we did it some time ago with the paywall. Appreciate your help! – Black Fox Jul 16 '23 at 14:32
  • That sounds good, I'd love to help! I have implemented this in my own app, I hopefully can help you get this resolved too. We can continue over on Twitter :) – iTech Everything Jul 16 '23 at 18:33
1

I think it will be difficult to use the existing method, and I think we need to make a custom search bar as you said. The other parts can be implemented as shown below to cause the searchBar to appear only when the scrollView is at the top.

import SwiftUI

struct ContentView: View {
    
    @State private var verticalOffset: CGFloat = 0.0
    
    var body: some View {
        
        VStack {
            if verticalOffset > 0 {
                HStack {
                    Text("Your searchable View")
                        .padding()
                    Spacer()
                }
                .frame(width: UIScreen.main.bounds.width - 50, height: 50)
                .background(.gray)
                .cornerRadius(10)
            } else {
                Spacer()
                    .frame(height: 50)
            }
            
            ObservableScrollView.init(scrollOffset: $verticalOffset) { (proxy: ScrollViewProxy) in
                ForEach(0..<100) { i in
                    Text("Item \(i)").padding().id(i)
                        .frame(width: UIScreen.main.bounds.width)
                }
            }
        }
    }
    
    struct ObservableScrollView<Content>: View where Content : View {
        @Namespace var scrollSpace
        
        @Binding var scrollOffset: CGFloat
        let content: (ScrollViewProxy) -> Content
        
        init(scrollOffset: Binding<CGFloat>,
             @ViewBuilder content: @escaping (ScrollViewProxy) -> Content) {
            _scrollOffset = scrollOffset
            self.content = content
        }
        
        var body: some View {
            ScrollView(showsIndicators: false) {
                ScrollViewReader { proxy in
                    content(proxy)
                        .background(GeometryReader { geo in
                            let offset = -geo.frame(in: .named(scrollSpace)).minY
                            Color.clear
                                .preference(key: ScrollViewOffsetPreferenceKey.self,
                                            value: offset)
                        })
                }
            }
            .coordinateSpace(name: scrollSpace)
            .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in
                scrollOffset = value
            }
        }
    }
    
    struct ScrollViewOffsetPreferenceKey: PreferenceKey {
        static var defaultValue = CGFloat.zero
        
        static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
            value += nextValue()
        }
    }
}

Here is a video testing the above code.

monglong
  • 455
  • 3
  • 11
  • If I tested your code correctly, you are showing the search view, when the view is scrolled down right? But I need it the other way around. Not visible, when launching, not visible when scrolling down, only visible when pulling slightly down (like eg apple notes). – Black Fox Jul 16 '23 at 14:30
1

I kind of had the same trouble way back and I found out the solution was annoyingly simple. Hope that one works out for you too.

I couldn't make an accurate guess on what's wrong with your destroyed elements (or sections) with this little information. But the problem might be the search bar interferes with the layout or functionality of the sections or cells.

The .searchable() modifier determines where the search bar appears in the view hierarchy, and placing it at different levels can affect the behavior of the search functionality. You can try adding the .searchable() function to your NavigationView instead of implementing it in your List:

NavigationView {
    // your code here, and your list, obviously
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))

By placing the .searchable() modifier at the same level as the NavigationView, the search bar will appear independently of the list content, which might help avoid the issues you mentioned.

SwiftUI can have complex interactions and behaviors, hope this little change can effectively solves your issue. Feel free to share more information so that we could find a solution if this one doesn't work for you. Also here's a really cool link for you to check out about the .searchable() modifier.

leopanic13
  • 62
  • 5
  • Did exactly, what you suggested, but it does not help. Still always visible... – Black Fox Jul 16 '23 at 14:25
  • Sorry to hear that! To be more clear, you want to **re-hide** your search bar when you scroll down, right? I just wanna be on the same page – leopanic13 Jul 16 '23 at 21:30
  • No, I want to have it always hidden, till you pull down a bit. Then it should pop up. When you scroll down it should re-hide as it already does defaultly – Black Fox Jul 30 '23 at 06:41
1

I'm sorry, I misunderstood the problem in the previous answer. If you want the same view as the Apple Note, you can do it as below.

import SwiftUI

struct SomeView: View {
  var name: String
  
  var body: some View {
    Text(name)
  }
}

struct SomeData: Identifiable {
  var name: String
  var id: String { self.name }
}

struct ContentView: View {
    
    @State var searchQueryString = ""
    var datas = (0...100).map(String.init).map(SomeData.init)
    
    var filteredDatas: [SomeData] {
      if searchQueryString.isEmpty {
        return datas
      } else {
        return datas.filter { $0.name.localizedStandardContains(searchQueryString) }
      }
    }
    
    var body: some View {
        
        NavigationView {
          List(filteredDatas) { data in
            NavigationLink {
              SomeView(name: data.name)
            } label: {
              Text(data.name)
            }
          }
          .navigationTitle("Search Test")
        }
        .searchable(
          text: $searchQueryString,
          placement: .navigationBarDrawer, // <- here is.
          prompt: "placholder..."
        )
    }
}

Here is a video testing the above code.

monglong
  • 455
  • 3
  • 11
0

So, this is maybe not the ideal answer, because it does not solve my problem, but it sets my search bar equal to apples search bar in for example notes. Why is it default hidden in their app but not in mine? Simple! The moment the whole screen is filled with content / enough list entries are available, the search bar disappears, if not specifically pulled down. If their are not enough entries, it will always stay visible!

Black Fox
  • 23
  • 8