SwiftUI picker with TextField for optional entry - swiftui

Picker with a choice of names. If ‘other’ is selected a TextField appears. User enters something into the TextField and the user entered value needs to also be reassigned to the Picker variable. I've searched and tried a 100 different options, nothing. (Also, I'm sure this isn't the best way, but don't know any better at this stage...) Thanks.
(Code simplified from actual)
import SwiftUI
struct ContentView: View {
#State private var playerList = ["sally", "bob", "mary"]
#State private var player1 = "tester"
#State private var player1other = ""
var body: some View {
NavigationView{
VStack{
List{
Picker("Player 1:", selection: $player1) {
ForEach(playerList, id: \.self) {
Text($0)
}
Divider()
Text("Non-roster player").tag("other")
}
if player1 == "other" {
TextField("Player name", text: $player1other)
}
//Now need something here that updates player1 to be whatever player1other is
// something like player1 = player1other
//that doesn't create an error - Type '()' cannot conform to 'View'
}
}
}
}
}

Edit: I should also address that NoeOnJupiter is correct in that your attempt to add player1 = player1other doesn't work because that's not a SwiftUI View but a statement. SwiftUI is declarative so you can't throw functions (for lack of a better term) in the middle of where you're building your view unless a view uses it (i.e. a button or the .onChange modifier)
One problem here is you have a static group of names that is supposed to be used to display the selected name (i.e. if the Picker selection is bob, bob will be shown by the Picker) but if there's a new name how will it be displayed by the Picker without being in the Picker's dataset?
I recommend doing one of two things:
Add new names to the dataset
Have a different Text element to display the player1 name
Approach 1:
Usage: Tap "sally", choose non-roster, type a name, submit. You'll notice that name gets added to the list.
struct ContentView: View {
#State private var playerList = ["sally", "bob", "mary"]
#State private var player1 = /**"tester"*/ "sally" // recommended by Swift Playgrounds to only use valid selections as a value for this
#State private var player1other = ""
var body: some View {
NavigationView{
VStack{
List{
Picker("Player 1:", selection: $player1) {
ForEach(playerList, id: \.self) {
Text($0)
}
Divider()
Text("Non-roster player").tag("other")
}
if player1 == "other" {
HStack {
TextField("Player name", text: $player1other)
Button("Submit") {
playerList.append(player1other)
player1 = player1other
player1other = "" // reset for next selection
}
}
}
//Now need something here that updates player1 to be whatever player1other is
// something like player1 = player1other
//that doesn't create an error - Type '()' cannot conform to 'View'
}
}
}
}
}
Approach 2:
Usage: Tap "sally", choose non-roster, start typing a name in the text-field. You'll notice the player name at the top of the screen updates in real time but will not be saved if the user changes names again.
struct ContentView: View {
#State private var playerList = ["sally", "bob", "mary"]
#State private var selectedFromList = "sally" // keeps track of picker selection, but not player
#State private var player1 = "tester"
#State private var player1other = ""
var body: some View {
NavigationView{
VStack{
// Displays current player name
Text(player1)
.font(.largeTitle)
List{
Picker("Player 1:", selection: $selectedFromList) {
ForEach(playerList, id: \.self) {
Text($0)
}
Divider()
Text("Non-roster player").tag("other")
}
// NEW
.onChange(of: selectedFromList) { newValue in
if selectedFromList != "other" {
player1 = selectedFromList
}
}
// ENDNEW
if selectedFromList == "other" { // Edited for new var
HStack {
TextField("Player name", text: $player1other)
// NEW
.onChange(of: player1other) { newValue in
player1 = player1other
}
//ENDNEW
}
}
//Now need something here that updates player1 to be whatever player1other is
// something like player1 = player1other
//that doesn't create an error - Type '()' cannot conform to 'View'
}
}
}
}
}

The reason you're getting
Type '()' cannot conform to 'View'
is because body is a ViewBuilder. Meaning it expects any type conforming to View, whereas player1 = player1other is of type Void != View.
Here's what you should do, depending on what you need:
Changing player1 to player1other when the user presses return (Recommended):
TextField("Player name", text: $player1other)
.onSubmit {
player1 = player1other
}
Directly assigning the text of TextField to be player1:
TextField("Player name", text: $player1)
Realtime updating player1other from player1:
TextField("Player name", text: $player1other)
.onChange(of: player1other) { newValue in
player1 = newValue
}

Related

Weird behavior with NavigationSplitView and #State

I have a NavigationSplitView in my app, I have an #State variable in my detail view that gets created in init.
When I select something from the sidebar and the detail view renders, at first everything looks ok. But when I select a different item on the sidebar, the contents of the #state variable don't get recreated.
Using the debugger I can see the init of the detail view get called every time I select a new item in the sidebar, and I can see the #State variable get created. But when it actually renders, the #State variable still contains the previous selection's values.
I've reduced this problem to a test case I'll paste below. The top text in the detail view is a variable passed in from the sidebar, and the second line of text is generated by the #State variable. Expected behavior would be, if I select "one" the detail view would display "one" and "The name is one". If I select "two" the detail view would display "two" and "The name is two".
Instead, if I select "one" first, it displays correctly. But when I select "two", it displays "two" and "The name is one".
Note that if I select "two" as the first thing I do after launching the app, it correctly displays "two" and "The name is two", but when I click on "one" next, it will display "one" and "the name is two". So the state variable is being set once, then never changing again,
Here's the sample code and screenshots:
import SwiftUI
struct Item: Hashable, Identifiable {
let id = UUID()
let name: String
}
struct ContentView: View {
#State private var selectedItem: Item.ID? = nil
private let items = [Item(name: "one"), Item(name: "two"), Item(name: "three")]
func itemForID(_ id: UUID?) -> Item? {
guard let itemID = id else { return nil }
return items.first(where: { item in
item.id == itemID
})
}
var body: some View {
NavigationSplitView{
List(selection: $selectedItem) {
ForEach(items) { item in
Text(item.name)
.tag(item.id)
}
}
} detail: {
if let name = itemForID(selectedItem)?.name {
DetailView(name: name)
} else {
Text("Select an item")
}
}
}
}
struct DetailView: View {
#State var detailItem: DetailItem
var name: String
init(name: String) {
self.name = name
_detailItem = State(wrappedValue: DetailItem(name: name))
}
var body: some View {
VStack {
Text(name)
Text(detailItem.computedText)
}
}
}
struct DetailItem {
let name: String
var computedText: String {
return "The name is \(name)"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Question. What is the purpose of having detailItem as a #State? if you remove the #State, this test case works.
Will the way computedText change over time?
struct DetailView: View {
// #State var detailItem: DetailItem
var detailItem: DetailItem
var name: String
init(name: String) {
self.name = name
// _detailItem = State(wrappedValue: DetailItem(name: name))
detailItem = DetailItem(name: name)
}
var body: some View {
VStack {
Text(name)
Text(detailItem.computedText)
}
}
}
This has nothing to do with NavigationSplitView, but how you initialise #State property.
According to the Apple document on #State (https://developer.apple.com/documentation/swiftui/state):
Don’t initialise a state property of a view at the point in the view hierarchy where you instantiate the view, because this can conflict with the storage management that SwiftUI provides.
As well as the documentation of init(wrappedValue:) (https://developer.apple.com/documentation/swiftui/state/wrappedvalue):
Don’t call this initializer directly. Instead, declare a property with the State attribute, and provide an initial value:
#State private var isPlaying: Bool = false
From my understanding, if you force to initialise the state in the view init, it will persist through the lifetime of the view, and subsequence change of it won't take any effect on the view.
The recommended way in Apple documentation is to create the struct in the parent view and pass it to the child view, and if you need to change the struct in the child view, use #Binding to allow read and write access.
If you want to ignore the documentation and force it to work, you can give an id to your DetailView, forcing it to refresh the view when the item id has changed:
var body: some View {
NavigationSplitView{
List(selection: $selectedItem) {
ForEach(items) { item in
Text(item.name)
.tag(item.id)
}
}
} detail: {
if let name = itemForID(selectedItem)?.name {
DetailView(name: name).id(selectedItem)
} else {
Text("Select an item")
}
}
}
Your Item struct is bad, if the name is unique it should be:
struct Item: Identifiable {
var id: String { name }
let name: String
}
Otherwise:
struct Item: Identifiable {
let id = UUID()
let name: String
}

SwiftUI Picker unable to transfer data from Picker to Binding variable

Hopefully you can see what I'm trying to achieve from the code below but simply put, I'm trying to update .selectedTown which is binded to my Picker. The row tapped on will bind to .selectedTown which will then update the Text 'Your selected town is: [.selectedTown]'
However, the selected row is not binding and the text remains 'Your selected town is: '
struct ContentView: View {
struct Town: Identifiable {
let name: String
let id = UUID()
}
private var towns = [
Town(name: "Bristol"),
Town(name: "Oxford"),
Town(name: "Portsmouth"),
Town(name: "Newport"),
Town(name: "Glasgow"),
]
#State private var selectedTown: String = ""
var body: some View {
NavigationView {
VStack {
Form {
Section {
Picker("", selection: $selectedTown) {
ForEach(towns, id: \.id) {
Text("\($0.name)")
}
}
.pickerStyle(.inline)
.labelsHidden()
} header: {
Text("Random Towns")
}
}
Text("Your selected town is: \(selectedTown)")
.padding()
}
.navigationTitle("Random")
}
}
}
Hopefully this is just a small fix but I've tried for what seems a day to find a solutino and am now stuck. Any help would be gratefully received,
Simon
The types don't match. your array is a towns: [Town] and your selectedTown: String
Option 1 is to change the variable
#State private var selectedTown: Town = Town(name: "Sample")
Option 2 is to add a tag
Text("\($0.name)").tag($0.name)
Option 3 is change the variable and the tag
#State private var selectedTown: Town? = nil
Text("\($0.name)").tag($0 as? Town)
The "best" option depends on what you use selectedTown for.
The type of selection should be same as picked item or use tag, like below
Picker("", selection: $selectedTown) {
ForEach(towns, id: \.id) {
Text("\($0.name)").tag($0.name) // << here !!
}
}
Tested with Xcode 13.2 / iOS 15.2

Observing changes #FocusState variable not working as expected

I want to validate the user's changes in a specific textfield and thus need to hook in when this textfield loses focus. I am using this year's introduced .focused(_:equals:) modifier with an enum and a #FocusState.
Observing my optional #FocusState var, which is nil when the screen first loads, with .onChange(of:perform:), this is only called once, when #FocusState changes from nil to a value–but not when it changes to another enum value.
The latter is expected though, and once this happens I could check what the previous field was–at least that's my approach for now.
Why is this approach not working–and is there a better way to go about this?
struct FocusView: View {
#State private var nameLast = "Ispum"
#State private var phoneNumber = "+41 79 888 88 88"
enum Field {
case nameLast
case phoneNumber
}
#FocusState private var focusedField: Field?
#State private var prevFocusedField: Field?
var body: some View {
VStack {
TextField("Last Name", text: $nameLast)
.focused($focusedField, equals: .nameLast)
TextField("Phone Number", text: $phoneNumber, prompt: Text(""))
.focused($focusedField, equals: .nameLast)
}
.onChange(of: focusedField) { newFocusField in
//only called once–when `focusedField` changes its state from `nil` to a value
print("Old value: \(prevFocusedField)")
print("New value: \(focusedField)")
if prevFocusedField == .phoneNumber {
//Validation
}
prevFocusedField = newFocusField
}
.textFieldStyle(.roundedBorder)
.padding()
}
}
There is a typo in your code (probably copy-paste)
TextField("Phone Number", text: $phoneNumber, prompt: Text(""))
.focused($focusedField, equals: .nameLast) // << here !!
you should use different value for second field:
VStack {
TextField("Last Name", text: $nameLast)
.focused($focusedField, equals: .nameLast)
TextField("Phone Number", text: $phoneNumber, prompt: Text(""))
.focused($focusedField, equals: .phoneNumber) // << fix !!
}
Tested with Xcode 13 / iOS 15

How to select an item by a button or tapGesture in my SwiftUI List and transfer it to another view

In my app I´m needing two players (only 2 names/strings), selected from an array built in a List/ForEach SwiftUI-code, which are used in another view.
What is the way to bring the name into a string for my Text(item)?
Can I select two items out of the list?
Thx for any help.
Franz
My code (modified, found by Ale Patron,Tutorial using UserDefaults with encoding and decoding the array/list ):
#State private var allTeams: [PlayerItem] = []
#State private var newPlayer = ""
#State private var selectedPlayer = ""
#State private var selection: String?
struct PlayerItem: Identifiable {
var id = UUID()
let player: String
}
var body: some View {
VStack{
HStack {
TextField("Add Players/Teams...", text: $newPlayer)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.allTeams.append(PlayerItem(player: self.newPlayer))
self.newPlayer = ""
}) {
Image(systemName: "plus")
}
.padding(.leading, 5)
}.padding()
List{
ForEach(allTeams) { playerItem in
Text(playerItem.player)
}
.onTapGesture {
print("How can I select my first und my second player")
}
}
Text("Selected Player: \(selectedPlayer)")
}
}
}
You should use indices for what you are trying to do.
Try this:
ForEach(allTeams.indices) { i in
Text(allTeams[i].player)
.onTapGesture {
print("How can I select my first und my second player")
print("The selected player is \(allTeams[i].player). The second player is \(allTeams[i + 1].player)"
}
}
Make sure to also check if the selected player is the last one in the array and to catch this. Otherwise, you may encounter an out-of-bounds error.

Set initial value for a TextField in SwiftUI - compare new and old value

I have seen a lot of examples and tutorial of how to use an empty TextField for collecting new values, but none that shows how to use a TextField to edit a value.
In my use-case, I want the TextField to be prepopulated/prefilled with data from my viewmodel, then as user edits the data, a Save button should be enabled. In my form, I also have a navigationlink that leads to a sub-page where the user can select something from a list, and then be routed back to the form.
It behaves as described as long I use an empty field; the user can type something temporary in the field, navigate to the sub page, and the temp value is still like it was when he left.
struct TextFieldDemo: View {
var model:String // Actual a more complex view model
#State var editedValue:String = ""
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Group{
Text("Some label")
TextField("Placeholder text", text: $editedValue)
}
Divider()
Text("Some navigation link to push in a page where " +
"the user can select something from a list and click back...")
// If the user starts to edit the textfield - follows a navigation link and comes back
// he should be able to continue edit the field where he left of - the text field should
// not have been reset to the original value.
Button(action: {
// Call some save function in the ViewModel
},label: {
Text("SAVE")
}
).disabled(model == editedValue)
}.onAppear(){
// I could have done something like:
// self.editedValue = model
// but it seems like this will fire if the user navigates into the described page and reset
// the TextField to the model value.
}
}
}
struct TextFieldDemo_Previews: PreviewProvider {
static var previews: some View {
TextFieldDemo(model: "The old value")
}
}
To initialize the text field with the value from your model, you need to define your own initializer and use the State(wrappedValue:) initializer for #State vars:
struct TextFieldDemo: View {
var model:String // Actual a more complex view model
#State var editedValue: String
init(model: String) {
self.model = model
self._editedValue = State(wrappedValue: model) // _editedValue is State<String>
}
var body: some View {
VStack(alignment: .leading, spacing: 20) {
Group{
Text("Some label")
TextField("Placeholder text", text: $editedValue)
}
Divider()
Text("Some navigation link to push in a page where " +
"the user can select something from a list and click back...")
// If the user starts to edit the textfield - follows a navigation link and comes back
// he should be able to continue edit the field where he left of - the text field should
// not have been reset to the original value.
Button(action: {
// Call some save function in the ViewModel
},label: {
Text("SAVE")
}
).disabled(model == editedValue)
}.onAppear(){
// I could have done something like:
// self.editedValue = model
// but it seems like this will fire if the user navigates into the described page and reset
// the TextField to the model value.
}
}
}
struct TextFieldDemo_Previews: PreviewProvider {
static var previews: some View {
TextFieldDemo(model: "The old value")
}
}
how about something like this test code. The key is to use the "ObservableObject":
import SwiftUI
class MyModel: ObservableObject {
#Published var model = "model1"
}
struct ContentView: View {
#ObservedObject var myModel = MyModel()
#State var editedValue = ""
var body: some View {
NavigationView {
VStack(alignment: .leading, spacing: 20) {
Group{
Text("Some label")
TextField("Placeholder text", text: Binding<String>(
get: { self.editedValue },
set: {
self.editedValue = $0
self.myModel.model = self.editedValue
})).onAppear(perform: loadData)
}
Divider()
NavigationLink(destination: Text("the nex page")) {
Text("Click Me To Display The next View")
}
// If the user starts to edit the textfield - follows a navigation link and comes back
// he should be able to continue edit the field where he left of - the text field should
// not have been reset to the original value.
Button(action: {
// Call some save function in the ViewModel
self.myModel.model = self.editedValue
},label: {
Text("SAVE")
})
}
}.navigationViewStyle(StackNavigationViewStyle())
}
func loadData() {
self.editedValue = myModel.model
}
}