SwiftUI .searchable implementation in the wrong way? - swiftui

I am trying to use a tab bar in order to use different views. On some of those views I have a list of items and I wish that list to be .searchable. If I go to each of the views and search it works like a charm, but when I embed that in the tabbed view the list becomes non-responsive to click but it responds to scroll gesture.
I will expand the idea with code that I have and screenshots, but I am pretty sure that the problem resides in how I'm implementing the combination of the tab bar view and the views that have the searchable modifier:
This code works well
import SwiftUI
struct ClientListView: View {
#ObservedObject var viewModel = ClientFeedViewModel()
#State var searchText: String
#State private var showingSheet = false
#State private var showList = false
var clients: [Client] {
if searchText.count > 2 {
return searchText.isEmpty ? viewModel.clients : viewModel.search(withText: searchText)
}
return viewModel.clients
}
init(){
searchText = ""
}
var body: some View {
NavigationView {
List(clients) { client in
NavigationLink(destination: {
}, label: {
VStack {
Text(client.clientName)
}
})
.listRowSeparator(.hidden)
}
.searchable(text: $searchText)
.listStyle(.plain)
}
}
}
struct ClientListView_Previews: PreviewProvider {
static var previews: some View {
ClientListView()
}
}
The problem starts when I do this and implement the ClientListView in a tab bar view like this:
Tab bar with different views not working searchable modifier
This is the code of the Tab Bar View:
import SwiftUI
struct MainTabView: View {
#EnvironmentObject var viewModel: AuthViewModel
#Binding var selectedIndex: Int
var body: some View {
NavigationView {
VStack {
TabView(selection: $selectedIndex) {
ClientListView()
.onTapGesture {
selectedIndex = 0
}
.tabItem {
Label("Clients", systemImage: "list.bullet")
}.tag(0)
ProjectListView()
.onTapGesture {
selectedIndex = 1
}
.tabItem {
Image(systemName: "person")
Label("Projects", systemImage: "list.dash")
}.tag(1)
TaskListView()
.tabItem {
Image(systemName: "person")
Label("Tasks", systemImage: "list.dash")
}.tag(2)
.onTapGesture {
selectedIndex = 2
}
ClientListView()
.tabItem {
Label("Settings", systemImage: "gear")
}.tag(3)
.onTapGesture {
selectedIndex = 3
}
}
.navigationTitle(tabTitle)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Image("logo_silueta")
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
viewModel.signOut()
}, label: {
Text("logout")
})
}
}
.navigationBarTitleDisplayMode(.inline)
}
}
var tabTitle: String {
switch selectedIndex {
case 0: return "Clients"
case 1: return "Projects"
case 2: return "Tasks"
case 3: return "Settings"
default: return ""
}
}
}
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView(selectedIndex: .constant(0))
}
}
Navigation on the tabbed view works and displays the different names on the tab bar title, but when I click cancel or x button of the search bar, it doesn't work and also the list becomes unclickable
So far I haven't been able to find where the problem is but I am assuming its because the tab bar view is messing up with the searchable property

The culprit would seem to be your .onTapGesture modifiers, which will take precedence over any tap handling in your child views.
I'm not sure what value those modifiers bring, since using appropriate .tag values is enough for the tab view to keep track of its selected index. I'd start by removing them.

#ObservedObject var viewModel = ClientFeedViewModel() is a memory leak, try changing it to something like:
struct ClientListViewData {
var searchText: String = ""
var showingSheet = false
var showList = false
mutating func showSheet() {
showingSheet = true
}
}
struct ClientListView: View {
#Binding var data: ClientListViewData

Related

NavigationLink in a Section doesn't behave like in a normalView with a simultaneous action

I have created a simple View with a NavigationLink in a Section an when the user presses on it, the value of the variable should change and should navigate the next View simultaneously. But it doesn't work like it should. If I press the "Text", the Value changes, but no navigation. If I press the "empty Space" it navigates to the next View, but the value doesn't change.
If I out the NavigationLink in a "normal" View, it does work like it should.
Is there a way to get this working without SubViews?
#State private var newValue = -1
var body: some View {
NavigationView {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("\(newValue)")
List {
Section ("Navigationlink") {
NavigationLink(destination: EmptyView()) {
Text("to Emptyview")
}.simultaneousGesture(TapGesture().onEnded{
newValue = 100
})
}
}
}
}
}
}
struct EmptyView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
}
}
You need a #State for Navigation to take place, this is needed as a source of truth needs to change(including navigation) in SwiftUI for any View change to happen , you change the #State for newValue so it changes, but you need to do same for NavigationView, also try NavigationStack in place of NavigationView in future , try below code , good luck
struct ContentView: View {
#State private var newValue = -1
#State private var changeView = false
var body: some View {
NavigationView {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("\(newValue)")
List {
Section ("Navigationlink") {
NavigationLink(destination: EmptyView(), isActive: $changeView) {
Text("to Emptyview")
}.simultaneousGesture(TapGesture().onEnded{
newValue = 100
changeView = true
})
}
}
}
}
}
}

Add border to only 1 button

I have a simple setup where I have 2 buttons and when a user clicks on one of them I want a border to show around it so that they know which one they clicked
I only ever want 1 button to have a border at once
I came up with this
import SwiftUI
struct TestView: View {
#State var isBorder:Bool = false
var body: some View {
VStack{
Button {
isBorder.toggle()
} label: {
Label("Sports", systemImage: "sportscourt")
}
.foregroundColor(.black)
.padding()
.background(Color(hex: "00bbf9"))
.cornerRadius(8)
.overlay(isBorder ? RoundedRectangle(cornerRadius: 8).stroke(Color.black, lineWidth:2) : nil)
Button {
isBorder.toggle()
} label: {
Label("Firends", systemImage: "person")
}
.foregroundColor(.black)
.padding()
.background(Color(hex: "fee440"))
.cornerRadius(8)
.overlay(isBorder ? RoundedRectangle(cornerRadius: 8).stroke(Color.black, lineWidth:2) : nil)
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
But a border shows around both buttons because I am using 1 variable "isBorder"
How could I adapt my solution so that I can accommodate for more buttons
As the comments allready stated this should be encapsulated in its own view. But here you also have the additional problem with the shared state of what button is clicked.
First create an enum that holds all properties a button has that distinguishes it from others:
enum ButtonEnum: Int, CaseIterable{
case sports, friends
var systemIcon: String{
switch self{
case .sports:
return "sportscourt"
case .friends:
return "person"
}
}
var label: String{
switch self{
case .sports:
return "Sports"
case .friends:
return "Friends"
}
}
var backgroundColor: Color{
switch self{
case .sports:
return Color("00bbf9")
case .friends:
return Color("fee440")
}
}
}
Then create a View that represents that button:
struct BorderedButton: View{
#Binding var selected: ButtonEnum?
var buttonType: ButtonEnum
var action: () -> Void
var body: some View{
Button {
selected = buttonType
action()
} label: {
Label(buttonType.label, systemImage: buttonType.systemIcon)
}
.foregroundColor(.black)
.padding()
.background(buttonType.backgroundColor)
.cornerRadius(8)
.overlay(selected == buttonType ? RoundedRectangle(cornerRadius: 8).stroke(Color.black, lineWidth:2) : nil)
}
}
and usage example:
struct TestView: View {
#State private var selected: ButtonEnum? = nil
var body: some View {
VStack{
BorderedButton(selected: $selected, buttonType: .friends) {
print("friends pressed")
}
BorderedButton(selected: $selected, buttonType: .sports) {
print("sports selected")
}
}
}
}
This solution can be easily expanded by adding new cases to the enum.

Popover displaying inaccurate information inside ForEach

I'm having a problem where I have a ForEach loop inside a NavigationView. When I click the Edit button, and then click the pencil image at the right hand side on each row, I want it to display the text variable we are using from the ForEach loop. But when I click the pencil image for the text other than test123, it still displays the text test123 and I have absolutely no idea why.
Here's a video. Why is this happening?
import SwiftUI
struct TestPopOver: View {
private var stringObjects = ["test123", "helloworld", "reddit"]
#State private var editMode: EditMode = .inactive
#State private var showThemeEditor = false
#ViewBuilder
var body: some View {
NavigationView {
List {
ForEach(self.stringObjects, id: \.self) { text in
NavigationLink( destination: HStack{Text("Test!")}) {
HStack {
Text(text)
Spacer()
if self.editMode.isEditing {
Image(systemName: "pencil.circle").imageScale(.large)
.onTapGesture {
if self.editMode.isEditing {
self.showThemeEditor = true
}
}
}
}
}
.popover(isPresented: $showThemeEditor) {
CustomPopOver(isShowing: $showThemeEditor, text: text)
}
}
}
.navigationBarTitle("Reproduce Editing Bug!")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}
}
}
struct CustomPopOver: View {
#Binding var isShowing: Bool
var text: String
var body: some View {
VStack(spacing: 0) {
HStack() {
Spacer()
Button("Cancel") {
self.isShowing = false
}.padding()
}
Divider()
List {
Section {
Text(text)
}
}.listStyle(GroupedListStyle())
}
}
}
This is a very common issue (especially since iOS 14) that gets run into a lot with sheet but affects popover as well.
You can avoid it by using popover(item:) rather than isPresented. In this scenario, it'll actually use the latest values, not just the one that was present when then view first renders or when it is first set.
struct EditItem : Identifiable { //this will tell it what sheet to present
var id = UUID()
var str : String
}
struct ContentView: View {
private var stringObjects = ["test123", "helloworld", "reddit"]
#State private var editMode: EditMode = .inactive
#State private var editItem : EditItem? //the currently presented sheet -- nil if no sheet is presented
#ViewBuilder
var body: some View {
NavigationView {
List {
ForEach(self.stringObjects, id: \.self) { text in
NavigationLink( destination: HStack{Text("Test!")}) {
HStack {
Text(text)
Spacer()
if self.editMode.isEditing {
Image(systemName: "pencil.circle").imageScale(.large)
.onTapGesture {
if self.editMode.isEditing {
self.editItem = EditItem(str: text) //set the current item
}
}
}
}
}
.popover(item: $editItem) { item in //item is now a reference to the current item being presented
CustomPopOver(text: item.str)
}
}
}
.navigationBarTitle("Reproduce Editing Bug!")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct CustomPopOver: View {
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var text: String
var body: some View {
VStack(spacing: 0) {
HStack() {
Spacer()
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}.padding()
}
Divider()
List {
Section {
Text(text)
}
}.listStyle(GroupedListStyle())
}
}
}
I also opted to use the presentationMode environment property to dismiss the popover, but you could pass the editItem binding and set it to nil as well (#Binding var editItem : EditItem? and editItem = nil). The former is just a little more idiomatic.

SwiftUI, setting title to child views of TabView inside of NavigationView does not work

Why I am putting TabView into a NavigationView is because I need to hide the bottom tab bar when user goes into 2nd level 'detail' views which have their own bottom action bar.
But doing this leads to another issue: all the 1st level 'list' views hosted by TabView no longer display their titles. Below is a sample code:
import SwiftUI
enum Gender: String {
case female, male
}
let members: [Gender: [String]] = [
Gender.female: ["Emma", "Olivia", "Ava"], Gender.male: ["Liam", "Noah", "William"]
]
struct TabItem: View {
let image: String
let label: String
var body: some View {
VStack {
Image(systemName: image).imageScale(.large)
Text(label)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
ListView(gender: .female).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
ListView(gender: .male).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
}
}
}
struct ListView: View {
let gender: Gender
var body: some View {
let names = members[gender]!
return List {
ForEach(0..<names.count, id: \.self) { index in
NavigationLink(destination: DetailView(name: names[index])) {
Text(names[index])
}
}
}.navigationBarTitle(Text(gender.rawValue), displayMode: .inline)
}
}
struct DetailView: View {
let name: String
var body: some View {
ZStack {
VStack {
Text("profile views")
}
VStack {
Spacer()
HStack {
Spacer()
TabItem(image: "pencil.circle", label: "Edit")
Spacer()
TabItem(image: "minus.circle", label: "Delete")
Spacer()
}
}
}
.navigationBarTitle(Text(name), displayMode: .inline)
}
}
What I could do is to have a #State var title in the root view and pass the binding to all the list views, then have those list views to set their title back to root view on appear. But I just don't feel so right about it, is there any better way of doing this? Thanks for any help.
The idea is to join TabView selection with NavigationView content dynamically.
Demo:
Here is simplified code depicting approach (with using your views). The NavigationView and TabView just position independently in ZStack, but content of NavigationView depends on the selection of TabView (which content is just stub), thus they don't bother each other. Also in such case it becomes possible to hide/unhide TabView depending on some condition - in this case, for simplicity, presence of root list view.
struct TestTabsOverNavigation: View {
#State private var tabVisible = true
#State private var selectedTab: Int = 0
var body: some View {
ZStack(alignment: .bottom) {
contentView
tabBar
}
}
var contentView: some View {
NavigationView {
ListView(gender: selectedTab == 0 ? .female : .male)
.onAppear {
withAnimation {
self.tabVisible = true
}
}
.onDisappear {
withAnimation {
self.tabVisible = false
}
}
}
}
var tabBar: some View {
TabView(selection: $selectedTab) {
Rectangle().fill(Color.clear).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
Rectangle().fill(Color.clear).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
.frame(height: 50) // << !! might be platform dependent
.opacity(tabVisible ? 1.0 : 0.0)
}
}
This maybe a late answer, but the TabView items need to be assigned tag number else binding selection parameter won't happen. Here is how I do the same thing on my project:
#State private var selectedTab:Int = 0
private var pageTitles = ["Home", "Customers","Sales", "More"]
var body: some View {
NavigationView{
TabView(selection: $selectedTab, content:{
HomeView()
.tabItem {
Image(systemName: "house.fill")
Text(pageTitles[0])
}.tag(0)
CustomerListView()
.tabItem {
Image(systemName: "rectangle.stack.person.crop.fill")
Text(pageTitles[1])
}.tag(1)
SaleView()
.tabItem {
Image(systemName: "tag.fill")
Text(pageTitles[2])
}.tag(2)
MoreView()
.tabItem {
Image(systemName: "ellipsis.circle.fill")
Text(pageTitles[3])
}.tag(3)
})
.navigationBarTitle(Text(pageTitles[selectedTab]),displayMode:.inline)
.font(.headline)
}
}

Show a new View from Button press Swift UI

I would like to be able to show a new view when a button is pressed on one of my views.
From the tutorials I have looked at and other answered questions here it seems like everyone is using navigation button within a navigation view, unless im mistaken navigation view is the one that gives me a menu bar right arrows the top of my app so I don't want that. when I put the navigation button in my view that wasn't a child of NavigationView it was just disabled on the UI and I couldn't click it, so I guess I cant use that.
The other examples I have seen seem to use presentation links / buttons which seem to show a sort of pop over view.
Im just looking for how to click a regular button and show another a view full screen just like performing a segue used to in the old way of doing things.
Possible solutions
1.if you want to present on top of current view(ex: presentation style in UIKit)
struct ContentView: View {
#State var showingDetail = false
var body: some View {
Button(action: {
self.showingDetail.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $showingDetail) {
DetailView()
}
}
}
2.if you want to reset current window scene stack(ex:after login show home screen)
Button(action: goHome) {
HStack(alignment: .center) {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
func goHome() {
if let window = UIApplication.shared.windows.first {
window.rootViewController = UIHostingController(rootView: HomeScreen())
window.makeKeyAndVisible()
}
}
3.push new view (ex: list->detail, navigation controller of UIKit)
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Show Detail View")
}.navigationBarTitle("Navigation")
}
}
}
}
4.update the current view based on #state property, (ex:show error message on login failure)
struct ContentView: View {
#State var error = true
var body: some View {
...
... //login email
.. //login password
if error {
Text("Failed to login")
}
}
}
For simple example you can use something like below
import SwiftUI
struct ExampleFlag : View {
#State var flag = true
var body: some View {
ZStack {
if flag {
ExampleView().tapAction {
self.flag.toggle()
}
} else {
OtherExampleView().tapAction {
self.flag.toggle()
}
}
}
}
}
struct ExampleView: View {
var body: some View {
Text("some text")
}
}
struct OtherExampleView: View {
var body: some View {
Text("other text")
}
}
but if you want to present more view this way looks nasty
You can use stack to control view state without NavigationView
For Example:
class NavigationStack: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var list: [AuthState] = []
public func push(state: AuthState) {
list.append(state)
didChange.send()
}
public func pop() {
list.removeLast()
didChange.send()
}
}
enum AuthState {
case mainScreenState
case userNameScreen
case logginScreen
case emailScreen
case passwordScreen
}
struct NavigationRoot : View {
#EnvironmentObject var state: NavigationStack
#State private var aligment = Alignment.leading
fileprivate func CurrentView() -> some View {
switch state.list.last {
case .mainScreenState:
return AnyView(GalleryState())
case .none:
return AnyView(LoginScreen().environmentObject(state))
default:
return AnyView(AuthenticationView().environmentObject(state))
}
}
var body: some View {
GeometryReader { geometry in
self.CurrentView()
.background(Image("background")
.animation(.fluidSpring())
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width, height: geometry.size.height,
alignment: self.aligment))
.edgesIgnoringSafeArea(.all)
.onAppear {
withAnimation() {
switch self.state.list.last {
case .none:
self.aligment = Alignment.leading
case .passwordScreen:
self.aligment = Alignment.trailing
default:
self.aligment = Alignment.center
}
}
}
}
.background(Color.black)
}
}
struct ExampleOfAddingNewView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.push(state: .emailScreen) }){
Text("Tap me")
}
}
}
}
struct ExampleOfRemovingView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.pop() }){
Text("Tap me")
}
}
}
}
In my opinion this bad way, but navigation in SwiftUI much worse