Picker not selecting the desired option - swiftui

I am trying to setup a picker, simple. I am successfully fetching an array of projects from firebase and populating the picker with the names of the projects. The problem that I am having is that I need to get the project id when I click the list but it's not doing anything after I click the option that I want. I tried to run it in a simulator and also on my iPhone and nothing happens after I make the selection. I am pretty sure I am not updating the picker and thus I am not updating the variable with the selected project id. I tried using the .onChange on the picker but nothing happens.
import SwiftUI
struct NewProjectView: View {
#ObservedObject var viewModel = ProjectViewModel()
#ObservedObject var clientViewModel = ClientFeedViewModel()
#Environment (\.dismiss) var dismiss
#State var projectName: String = "s"
var clientNameIsEmpty: Bool {
if projectName.count < 3 {
return true
} else {
return false
}
}
var clients: [Client] {
return clientViewModel.clients
}
#State var selectedClient: String = ""
var body: some View {
NavigationView {
VStack {
Picker("", selection: $selectedClient) {
ForEach(clients, id:\.self) {
Text($0.clientName)
//I need to exctract the project id so I can pass it on
}
}
.pickerStyle(.menu)
CustomTextField(text: $projectName, placeholder: Text("Client Name"), imageName: "person.text.rectangle")
.padding()
.background(Color("JUMP_COLOR")
.opacity(0.75)
)
.cornerRadius(10)
.padding(.horizontal, 40)
Text("Name must contain more than 3 characters")
.font(.system(.subheadline))
.foregroundColor(.gray.opacity(0.3))
.padding(.top, 30)
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: {
Button(action: {
dismiss()
}, label: {
Text("Cancel")
})
})
ToolbarItem(placement: .navigationBarTrailing , content: {
Button(action: {
viewModel.newProject(name: projectName)
dismiss()
}, label: {
Text("Save")
})
.disabled(clientNameIsEmpty)
})
}
}
}
.presentationDetents([.height(400)])
//.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
Here is the picker populated with the foo data: picker

Your selection variable $selectedClient needs to have a type that matches the tagged value of each item in the picker.
As you're not specifying an explicit .tag for your text, the ForEach creates an implicit one using what it's using for tracking its loop, which in this case looks like it's a Client.
You can either change selectedClient to be a type of Client, or tag your displayed subview with the string value to populate selectedClient with, e.g.:
ForEach(clients, id: \.self) { client in
Text(client.clientName)
.tag(client.clientID)
}
Also, if each client has a unique ID, you're better off using that as ForEach's identifier than \.self. You can either specify id: \.clientID, etc., to use a single attribute – or you can add Identifiable conformance to Client and make sure that it has an id value that is guaranteed to be unique.

import SwiftUI
import Firebase
struct NewProjectView: View {
#ObservedObject var viewModel = ProjectViewModel()
#ObservedObject var clientViewModel = ClientFeedViewModel()
#Environment (\.dismiss) var dismiss
#State var projectName: String = "s"
var clientNameIsEmpty: Bool {
if projectName.count < 3 {
return true
} else {
return false
}
}
var clients: [Client] {
return clientViewModel.clients
}
#State var selectedClient: Client = Client(id: "", clientName: "Blank", timestamp: Timestamp(), ownerId: "", ownerUsername: "")
var body: some View {
NavigationView {
VStack {
Picker("d", selection: $selectedClient) {
ForEach(clients, id:\.id) { client in
Text(client.clientName)
.tag(client)
//I need to exctract the project id so I can pass it on
}
}
.pickerStyle(.menu)
Text(selectedClient.id ?? "")
CustomTextField(text: $projectName, placeholder: Text("Client Name"), imageName: "person.text.rectangle")
.padding()
.background(Color("JUMP_COLOR")
.opacity(0.75)
)
.cornerRadius(10)
.padding(.horizontal, 40)
Text("Name must contain more than 3 characters")
.font(.system(.subheadline))
.foregroundColor(.gray.opacity(0.3))
.padding(.top, 30)
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: {
Button(action: {
dismiss()
}, label: {
Text("Cancel")
})
})
ToolbarItem(placement: .navigationBarTrailing , content: {
Button(action: {
viewModel.newProject(name: projectName)
dismiss()
}, label: {
Text("Save")
})
.disabled(clientNameIsEmpty)
})
}
}
}
.presentationDetents([.height(400)])
//.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}

Related

SwiftUI .searchable implementation in the wrong way?

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

Programmatically Presenting and Dismissing Views SwiftUI

I am working on a project that is attempting to present and dismiss views in a NavigationView using state and binding. The reason I am doing this is there is a bug in the #Environment(.presentationMode) var presentaionMode: Binding
model. It's causing odd behavior. It's discussed in this post here.
The example below has three views that are progressively loaded on to the view. The first two ContentView to NavView1 present and dismiss perfectly. However, once NavView2 is loaded, the button that is used to toggle the state of presentNavView2 ends up adding another NavView2 view on the stack and does not dismiss it as expected. Any thoughts as to why this would be?
ContentView
struct ContentView: View {
#State private var presentNavView1 = false
var body: some View {
NavigationView {
List {
NavigationLink(destination: NavView1(presentNavView1: self.$presentNavView1), isActive: self.$presentNavView1, label: {
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("To NavView1")
}) // Button
}) // NavigationLink
} // List
.navigationTitle("Home")
} // NavigationView
} // View
}
NavView1
struct NavView1: View {
#State private var presentNavView2 = false
#Binding var presentNavView1: Bool
var body: some View {
List {
NavigationLink(destination: NavView2(presentNavView2: self.$presentNavView2), isActive: self.$presentNavView2, label: {
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("To NavView2")
}) // Button
}) // NavigationLink
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("Back")
})
} // List
.navigationTitle("NavView1")
} // View
}
NavView2
struct NavView2: View {
#Binding var presentNavView2: Bool
var body: some View {
VStack {
Text("NavView2")
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("Back")
}) // Button
} // VStack
.navigationTitle("NavView2")
}
}
You can use DismissAction, because PresentationMode will be deprecated. I tried the code and it works perfectly! Here you go!
import SwiftUI
struct MContentView: View {
#State private var presentNavView1 = false
var body: some View {
NavigationView {
List {
NavigationLink(destination: NavView1(), isActive: self.$presentNavView1, label: {
Button(action: {
self.presentNavView1.toggle()
}, label: {
Text("To NavView1")
})
})
}
.navigationTitle("Home")
}
}
}
struct NavView1: View {
#Environment(\.dismiss) private var dismissAction: DismissAction
#State private var presentNavView2 = false
var body: some View {
List {
NavigationLink(destination: NavView2(), isActive: self.$presentNavView2, label: {
Button(action: {
self.presentNavView2.toggle()
}, label: {
Text("To NavView2")
})
})
Button(action: {
self.dismissAction.callAsFunction()
}, label: {
Text("Back")
})
}
.navigationTitle("NavView1")
}
}
struct NavView2: View {
#Environment(\.dismiss) private var dismissAction: DismissAction
var body: some View {
VStack {
Text("NavView2")
Button(action: {
self.dismissAction.callAsFunction()
}, label: {
Text("Back")
})
}
.navigationTitle("NavView2")
}
}
struct MContentView_Previews: PreviewProvider {
static var previews: some View {
MContentView()
}
}

SwiftUI - Picker value not changing when accessing data from UserDefaults

I am making an app where I am showing different views based of user's selection by a picker. The binding value of the picker is initially set by UserDefaults in a viewModel. The problem is when I choose a picker value in my app, The picker automatically go back to initial state, as if someone forcing the picker not the change the values.
Settings ViewModel :
import Foundation
class SettingsViewModel:ObservableObject{
#Published var showSettings = false
//Here is the problem
#Published var choosenUserType = UserDefaults.standard.string(forKey: "userType"){
didSet{
UserDefaults.standard.set(self.choosenUserType, forKey: "userType")
}
}
static var userTypes = ["Client", "Worker"]
}
Home View:
import SwiftUI
struct HomeView: View {
#StateObject var settingsVM = SettingsViewModel()
var body: some View {
VStack{
switch settingsVM.choosenUserType{
case "Client":
Text("This is client")
case "Worker":
Text("This is worker")
default:
Text("This is default")
}
}.navigationTitle("Tanvirgeek Co")
.navigationBarItems(trailing: Button(action: {
settingsVM.showSettings.toggle()
}, label: {
Text("Settings")
}))
.sheet(isPresented: $settingsVM.showSettings, content: {
SettingsView(dissmiss: $settingsVM.showSettings)
.environmentObject(settingsVM)
})
}
}
Settings View:
import SwiftUI
struct SettingsView: View {
#EnvironmentObject var settingVM:SettingsViewModel
#Binding var dissmiss:Bool
var body: some View {
VStack{
Picker(selection: $settingVM.choosenUserType, label: Text("Choose User Type"), content: {
ForEach(SettingsViewModel.userTypes, id: \.self) { userType in
Text("\(userType)")
}
})
Button(action: {
dissmiss.toggle()
}, label: {
Text("Dismiss")
})
}
}
}
What I am doing wrong? How to change the picker's binding variable value through the picked value here?
Your choosenUserType ends up with an inferred type of String? because that's what UserDefaults.string(forKey:) returns.
The Picker's selection type needs to match exactly with the tag type. The tags (which are inferred in this case as well) are of type String.
I've solved this by giving a default value to choosenUserType so that it can be a String (not String?):
class SettingsViewModel:ObservableObject{
#Published var showSettings = false
#Published var choosenUserType : String = UserDefaults.standard.string(forKey: "userType") ?? SettingsViewModel.userTypes[0] {
didSet{
UserDefaults.standard.set(self.choosenUserType, forKey: "userType")
}
}
static var userTypes = ["Client", "Worker"]
}
Also, in your SettingsView, you don't have to interpolate the userType in the Text -- you can just provide it directly:
struct SettingsView: View {
#EnvironmentObject var settingVM:SettingsViewModel
#Binding var dissmiss:Bool
var body: some View {
VStack{
Picker(selection: $settingVM.choosenUserType, label: Text("Choose User Type")) {
ForEach(SettingsViewModel.userTypes, id: \.self) { userType in
Text(userType)
}
}
Button(action: {
dissmiss.toggle()
}, label: {
Text("Dismiss")
})
}
}
}

SwiftUI List rows with INFO button

UIKit used to support TableView Cell that enabled a Blue info/disclosure button. The following was generated in SwiftUI, however getting the underlying functionality to work is proving a challenge for a beginner to SwiftUI.
Generated by the following code:
struct Session: Identifiable {
let date: Date
let dir: String
let instrument: String
let description: String
var id: Date { date }
}
final class SessionsData: ObservableObject {
#Published var sessions: [Session]
init() {
sessions = [Session(date: SessionsData.dateFromString(stringDate: "2016-04-14T10:44:00+0000"),dir:"Rhubarb", instrument:"LCproT", description: "brief Description"),
Session(date: SessionsData.dateFromString(stringDate: "2017-04-14T10:44:00+0001"),dir:"Custard", instrument:"LCproU", description: "briefer Description"),
Session(date: SessionsData.dateFromString(stringDate: "2018-04-14T10:44:00+0002"),dir:"Jelly", instrument:"LCproV", description: " Description")
]
}
static func dateFromString(stringDate: String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
return dateFormatter.date(from:stringDate)!
}
}
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(session: session )
}
}
.navigationTitle("Session data")
}
// without this style modification we get all sorts of UIKit warnings
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SessionRow: View {
var session: Session
#State private var presentDescription = false
var body: some View {
HStack(alignment: .center){
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
Spacer()
// SessionGraph is a place holder for the Graph data.
NavigationLink(destination: SessionGraph()) {
// if this isn't an EmptyView then we get a disclosure indicator
EmptyView()
}
// Note: without setting the NavigationLink hidden
// width to 0 the List width is split 50/50 between the
// SessionRow and the NavigationLink. Making the NavigationLink
// width 0 means that SessionRow gets all the space. Howeveer
// NavigationLink still works
.hidden().frame(width: 0)
Button(action: { presentDescription = true
print("\(session.dir):\(presentDescription)")
}) {
Image(systemName: "info.circle")
}
.buttonStyle(BorderlessButtonStyle())
NavigationLink(destination: SessionDescription(),
isActive: $presentDescription) {
EmptyView()
}
.hidden().frame(width: 0)
}
.padding(.vertical, 4)
}
}
struct SessionGraph: View {
var body: some View {
Text("SessionGraph")
}
}
struct SessionDescription: View {
var body: some View {
Text("SessionDescription")
}
}
The issue comes in the behaviour of the NavigationLinks for the SessionGraph. Selecting the SessionGraph, which is the main body of the row, propagates to the SessionDescription! hence Views start flying about in an un-controlled manor.
I've seen several stated solutions to this issue, however none have worked using XCode 12.3 & iOS 14.3
Any ideas?
When you put a NavigationLink in the background of List row, the NavigationLink can still be activated on tap. Even with .buttonStyle(BorderlessButtonStyle()) (which looks like a bug to me).
A possible solution is to move all NavigationLinks outside the List and then activate them from inside the List row. For this we need #State variables holding the activation state. Then, we need to pass them to the subviews as #Binding and activate them on button tap.
Here is a possible example:
struct SessionList: View {
#EnvironmentObject private var sessionData: SessionsData
// create state variables for activating NavigationLinks
#State private var presentGraph: Session?
#State private var presentDescription: Session?
var body: some View {
NavigationView {
List {
ForEach(sessionData.sessions) { session in
SessionRow(
session: session,
presentGraph: $presentGraph,
presentDescription: $presentDescription
)
}
}
.navigationTitle("Session data")
// put NavigationLinks outside the List
.background(
VStack {
presentGraphLink
presentDescriptionLink
}
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
#ViewBuilder
var presentGraphLink: some View {
// custom binding to activate a NavigationLink - basically when `presentGraph` is set
let binding = Binding<Bool>(
get: { presentGraph != nil },
set: { if !$0 { presentGraph = nil } }
)
// activate the `NavigationLink` when the `binding` is `true`
NavigationLink("", destination: SessionGraph(), isActive: binding)
}
#ViewBuilder
var presentDescriptionLink: some View {
let binding = Binding<Bool>(
get: { presentDescription != nil },
set: { if !$0 { presentDescription = nil } }
)
NavigationLink("", destination: SessionDescription(), isActive: binding)
}
}
struct SessionRow: View {
var session: Session
// pass variables as `#Binding`...
#Binding var presentGraph: Session?
#Binding var presentDescription: Session?
var body: some View {
HStack {
Button {
presentGraph = session // ...and activate them manually
} label: {
VStack(alignment: .leading) {
Text(session.dir)
.font(.headline)
.truncationMode(.tail)
.frame(minWidth: 20)
Text(session.instrument)
.font(.caption)
.opacity(0.625)
.truncationMode(.middle)
}
}
.buttonStyle(PlainButtonStyle())
Spacer()
Button {
presentDescription = session
print("\(session.dir):\(presentDescription)")
} label: {
Image(systemName: "info.circle")
}
.buttonStyle(PlainButtonStyle())
}
.padding(.vertical, 4)
}
}

SwiftUI List/Form bad animation when adding/removing rows

I have a really simple list with text that when the user taps on it, it expands with a datepicker inside.
The problem is that the animation looks really broken, not sure what I can do about this besides doing the entire thing from scratch, that at this point I'd rather just use UIKit.
If you have an idea of how this can be fixed I'd really appreciate.
Here's the code:
struct ContentView: View {
let items = ["123", "345", "678"]
#State private var selectedItems = Set<String>()
#State private var test = Date()
var body: some View {
Form {
ForEach(items.indices) { index in
Button(action: {
withAnimation {
if selectedItems.contains(items[index]) {
selectedItems.remove(items[index])
} else {
selectedItems.insert(items[index])
}
}
}, label: {
Text(items[index])
.foregroundColor(.primary)
})
if selectedItems.contains(items[index]) {
DatePicker(selection: $test, in: ...Date(), displayedComponents: .date) {
}
.datePickerStyle(WheelDatePickerStyle())
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ForEach(content:) should only be used for static collections.
If you have a dynamic collection (such as in your example - you're adding/removing entries), you need to use ForEach(id:content:):
ForEach(items.indices, id: \.self) { index in
Note that if your collection can have duplicate items, then id: \.self will not work properly and you may need to create a struct conforming to Identifiable instead.
Use Section inside ForEach.
struct ContentView: View {
let items = ["123", "345", "678"]
#State private var selectedItems = Set<String>()
#State private var test = Date()
var body: some View {
Form {
ForEach(items.indices) { index in
Section(header: header(index), content: {
if selectedItems.contains(items[index]) {
DatePicker(selection: $test, in: ...Date(), displayedComponents: .date) {
}
.datePickerStyle(WheelDatePickerStyle())
}
})
}
}
}
private func header(_ index: Int) -> some View {
Button(action: {
withAnimation {
if selectedItems.contains(items[index]) {
selectedItems.remove(items[index])
} else {
selectedItems.insert(items[index])
}
}
}, label: {
Text(items[index])
.foregroundColor(.primary)
})
}
}