Add new Element in Picker in SwiftUI - ios

I can't find how to add some element in a picker view in SwiftUI, in my sample, I want add "Z" value in picker when I click the button.
struct ContentView: View {
#State var values: [String] = ["A", "B", "C"]
#State private var selectedValue = 0
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedValue, label: Text("Value")) {
ForEach(0 ..< values.count) {
Text(self.values[$0])
}
}
}
Button(action: {
self.values.append("Z")
}, label: {
Text("Add")
})
}.navigationBarTitle("Select a value")
}
}
When I click on the button, Z is added to "values" array but Picker is not refreshed.
Thank you :)

You must identify values by id for SwiftUI to make it's changes detectable:
ForEach(0 ..< self.values.count, id: \.self) {
Text(self.values[$0])
}
This way SwiftIU knowns it should rebuild the picker on change.
Tip: You can use elements directly like this:
ForEach(values, id: \.self) {
Text($0)
}
Don't forget to change the selectedValue type and value to match with the dataSource IF you followed the tip above:
#State private var selectedValue = "A"

Change selectedValue from int to String
#State private var selectedValue = "A"
add the parameter id and the tag modifier
Picker(selection: $selectedValue, label: Text("Value")) {
ForEach(values, id: \.self) {
Text($0).tag(String($0))
}
}

Related

Get selected item from picker | SwiftUI

I want to get the selected item from a Picker to update some data on the Firebase Database, but when i use the onTapGesture is not working
Note: The items inside the picker are Strings
My Code:
Picker(selection: $numUnitIndex, label: Text("Numerical Unit: \(numUnit)")) {
ForEach(0 ..< units.count) {
Text(self.units[$0]).tag($0).foregroundColor(.blue)
.onTapGesture {
//updateUnit(newUnit: self.units[numUnitIndex])
print("selected \(numUnitIndex)")
}
}
}.pickerStyle(MenuPickerStyle())
Here is a simple example of right way of doing this, no need onTapGesture here:
struct ContentView: View {
let units: [String] = ["😀", "😎", "🥶", "😡", "😍"]
#State private var selectedUnit: Int = 0
var body: some View {
Picker(selection: $selectedUnit, label: Text("You selected: \(units[selectedUnit])")) {
ForEach(units.indices, id: \.self) { unitIndex in Text(units[unitIndex]) }
}
.pickerStyle(MenuPickerStyle())
.onChange(of: selectedUnit, perform: { newValue in print("Selected Unit: \(units[newValue])", "Selected Index: \(newValue)")})
}
}
You shouldn't use indices but the objects in the array in your ForEach and there is no need for onTapGesture, the variable passed to selection will hold the selected value.
Something like this
let units: [String] = ["a", "b", "c"]
#State private var selectedUnit: String
init() {
selectedUnit = units.first ?? ""
}
var body: some View {
VStack {
Picker("Units", selection: $selectedUnit) {
ForEach(units, id: \.self) { unit in
Text(unit)
.foregroundColor(.blue)
.font(.largeTitle)
}
}.pickerStyle(MenuPickerStyle())
Text("Selected unit is \(selectedUnit)")
}
}

Text not updating correctly based on picker

This is my model
struct ListItemModel: Codable, Identifiable {
let id: String
let name: String
}
This is the view that will be displayed with a Picker. The list will be populated by an outside source but I simplified it for this example.
struct TypeSelectionView: View {
#State private var selected = 0
let testList = [ListItemModel(id: "11", name: "name1"),
ListItemModel(id: "12", name: "name2")]
var body: some View {
VStack {
Picker(selection: $selected, label: Text("Pick a Type")) {
ForEach(0 ..< testList.count) {
Text(testList[$0].name)
}
}.labelsHidden()
Picker(selection: $selected, label: Text("Pick a Type")) {
ForEach(testList) {type in
Text(type.name)
}
}.labelsHidden()
Text("Selected Type: \(testList[selected].name)")
Spacer()
}
}
}
struct TypeSelectionView_Previews: PreviewProvider {
static var previews: some View {
TypeSelectionView()
}
}
The first Picker is correctly changing the display of the Text view on the page when the Picker changes but the second Picker does not. Is their a way to make the second Picker do the do the same thing where as you change the Picker the Text view will update accordingly
or is the first Picker the way you should always go when making Pickers in SwiftUI?
The reason your second Picker doesn't work is that the values returned by the Picker correspond to the id of the items. In the case of your second Picker, those are String.
You can apply a .tag() to each item, and then the Picker will return that. For example, if you added an explicit tag it would work:
Text(type.name).tag(testList.firstIndex(where: { $0.id == type.id })!)
Alternatively, if you changed your id values to be Int and the id values corresponded to the position in the array, it would work.
Because of the difficulties of implementing a tag, it is easy to see why many developers choose to just iterate on 0 ..< testList.count.
Ok so this my first ever answer for a stack overflow question, I'm quite a newbie myself but hopefully I can be of some help.
The code when placed in to Xcode shows two pickers whose initial values are name1 but when you change the first picker the second picker and the text displaying the selected type change accordingly, but because both pickers share the same source of truth #State private var selected = 0, changing this will have unintended side effects.
import SwiftUI
struct TypeSelectionView: View {
#State private var selected = 0
#State var testList = [ListItemModel(id: "11", name: "name1"),
ListItemModel(id: "12", name: "name2")]
#State var priorityTypes = ["low", "medium", "high", "critical"]
var body: some View {
VStack {
Picker("Pick a Type", selection: $selected) {
ForEach(0..<testList.count) {
Text(self.testList[$0].name)
}
}.labelsHidden()
Picker("Pick a Type", selection: $selected) {
ForEach(0..<testList.count) {
Text(self.testList[$0].name)
}
}.labelsHidden()
Text("Selected Type: \(testList[selected].name)")
Spacer()
}
}
}
struct TypeSelectionView_Previews: PreviewProvider {
static var previews: some View {
TypeSelectionView()
}
}
struct ListItemModel: Codable, Identifiable {
let id: String
let name: String
}

Complete list is recreates all views even if just one item has changed

I have a very simple schoolbook example of a SwiftUI List view that renders items from data in an array. Data in the array is Identifiable. But, when I change the the data in the array, add or remove a item then all rows in the list view are recreated. Is that correct? My understanding was that Identifiable should make sure that only the view in the list that are changed are recreated.
My list is inside a navigation view and each row links to a detail view. The problem is that since all items in the list are removed and recreated every time the data is changed then if that that happens when Im in a detail view (it's triggered by a notification) then Im thrown out back to the list.
What am I missing?
Edit: Added code example
This is my data struct:
struct Item: Identifiable {
let id: UUID
let name: String
init(name: String) {
self.id = UUID()
self.name = name
}
}
This is my ItemView
struct ItemView: View {
var item: Item
init(item: Item) {
self.item = item
print("ItemView created \(self.item.id)")
}
var body: some View {
Text(self.item.name)
}
}
An finally my list view:
struct KeyList: View {
#State var items = [Item(name: "123"), Item(name: "456"), Item(name: "789")]
var body: some View {
VStack {
List(self.items) { item in
ItemView(item: item)
}
Button(action: {
self.items.append(Item(name: "New"))
}) {
Text("Add")
}
}
}
}
When I press add it will print "ItemView created" 4 times. My understanding is that it should only do it 1 time?
Here is an example of how this could work. Tested and working on iOS 13.5
The List doesn't get recreated again when only one item is being removed. So this was accomplished.
About the poping of the View this has already been answered here:
SwiftUI ForEach refresh makes view pop
I have here a small workaround for this problem. Add the items you want to remove to an array. Then when going back, remove these items (Which will make the view pop) or go back programmatically and nothing gets removed
struct ContentView: View {
#State var text:Array<String> = ["a", "b", "c"]
var body: some View {
NavigationView() {
VStack() {
List() {
ForEach(self.text, id: \.self){ item in
NavigationLink(destination: SecondView(textItem: item, text: self.$text)) {
Text(item)
}
}
}
Button(action: {
self.text.remove(at: 0)
}){
Text("Remove \(self.text[0])")
}
}
}
}
}
struct SecondView: View {
#State var textItem: String
#Binding var text: Array<String>
#State var tmpArray: Array<String> = []
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack() {
Text(self.textItem)
Button(action: {
//Append to a tmp array which will later be used to determine what to remove
self.tmpArray.append(self.text[0])
}){
Text("Remove \(self.text[0])")
}
Button(action: {
if self.tmpArray.count > 0 {
//remove your stuff which will automatically pop the view
self.text.remove(at: 0)
} else {
// programmatically go back as nothing has been deleted
self.presentationMode.wrappedValue.dismiss()
}
}){
Text("Go Back")
}
}
}
}

Im having trouble with dynamic lists in SwiftUI. I cant get my list to update dynamically using a picker

Basically as the title states. I have the picker called Ingredients and when I go into the list and click an element it should work as a button (or maybe not) and use the add function to append that element into the ingredients list which is a state variable which should then in turn update the list at the bottom and display its elements, but it doesnt. I have done other projects with a similar idea of an updating list but never with a picker. Any help appreciated. Also worth mentioning is that the TEST button works for what i want to achieve and the #ObservedObject can be ignored.
import SwiftUI
struct AddRecipe: View {
#ObservedObject var recipe: RecipeFinal
#State private var name = ""
#State private var time = 0
#State private var diff = ""
#State private var ingredients = [String]()
static var diffT = ["Easy", "Medium", "Difficult"]
static var ingred = ["Onion","Salt","Oil","Tomato", "Garlic",
"Peppers","Bread","Vinegar"]
var body: some View {
NavigationView {
Form {
TextField("Name", text: $name)
Stepper(value: $time, in: 0...120, step: 15) {
Text("Time: \(time) minutes")
}
Picker ("Difficulty", selection: $diff) {
ForEach (AddRecipe.self.diffT, id: \.self) {
Text($0)
}
}
Button("TEST") {
self.ingredients.append("TEST")
}
Picker("Ingredients", selection: $ingredients) {
ForEach (AddRecipe.self.ingred, id: \.self) { ing in
Button(action: {
self.add(element: ing)
}) {
Text("\(ing)")
}
}
}
Section(header: Text("Ingredients")) {
List (self.ingredients, id: \.self) {
Text($0)
}
}
}
}
}
func add (element: String) {
self.ingredients.append(element)
}
}
struct AddRecipe_Previews: PreviewProvider {
static var previews: some View {
AddRecipe(recipe: RecipeFinal())
}
}

SwiftUI Textfields inside Lists

I want a list with rows, with each row having 2 Textfields inside of it. Those rows should be saved in an array, so that I can use the data in an other view for further functions. If the text in the Textfield is changed, the text should be saved inside the right entry in the array.
You also can add new rows to the list via a button, which should also change the array for the rows.
The goal is to have a list of key value pairs, each one editable and those entries getting saved with the current text.
Could someone help me and/or give me hint for fixing this problem?
So far I have tried something like this:
// the array of list entries
#State var list: [KeyValue] = [KeyValue()]
// the List inside of a VStack
List(list) { entry in
KeyValueRow(list.$key, list.$value)
}
// the single item
struct KeyValue: Identifiable {
var id = UUID()
#State var key = ""
#State var value = ""
}
// one row in the list with view elements
struct KeyValueRow: View {
var keyBinding: Binding<String>
var valueBinding: Binding<String>
init(_ keyBinding: Binding<String>, _ valueBinding: Binding<String>){
self.keyBinding = keyBinding
self.valueBinding = valueBinding
}
var body: some View {
HStack() {
TextField("key", text: keyBinding)
Spacer()
TextField("value", text: valueBinding)
Spacer()
}
}
}
Also, about the button for adding new entries.
Problem is that if I do the following, my list in the view goes blank and everything is empty again
(maybe related: SwiftUI TextField inside ListView goes blank after filtering list items ?)
Button("Add", action: {
self.list.append(KeyValue())
})
I am not sure what the best practice is keep a view up to date with state in an array like this, but here is one approach to make it work.
For the models, I added a list class that conforms to Observable object, and each KeyValue item alerts it on changes:
class KeyValueList: ObservableObject {
#Published var items = [KeyValue]()
func update() {
self.objectWillChange.send()
}
func addItem() {
self.items.append(KeyValue(parent: self))
}
}
class KeyValue: Identifiable {
init(parent: KeyValueList) {
self.parent = parent
}
let id = UUID()
private let parent: KeyValueList
var key = "" {
didSet { self.parent.update() }
}
var value = "" {
didSet { self.parent.update() }
}
}
Then I was able to simply the row view to just keep a single piece of state:
struct KeyValueRow: View {
#State var item: KeyValue
var body: some View {
HStack() {
TextField("key", text: $item.key)
Spacer()
TextField("value", text: $item.value)
Spacer()
}
}
}
And for the list view:
struct TextFieldList: View {
#ObservedObject var list = KeyValueList()
var body: some View {
VStack {
List(list.items) { item in
HStack {
KeyValueRow(item: item)
Text(item.key)
}
}
Button("Add", action: {
self.list.addItem()
})
}
}
}
I just threw an extra Text in there for testing to see it update live.
I did not run into the Add button blanking the view as you described. Does this solve that issue for you as well?
Working code example for iOS 15
In SwiftUI, Apple recommends passing the binding directly into the List constructor and using a #Binding in the ViewBuilder block to iterate through with each element.
Apple recommends this approach over using the Indices to iterate over the collection since this doesn't reload the whole list every time a TextField value changes (better efficiency).
The new syntax is also back-deployable to previous releases of SwiftUI apps.
struct ContentView: View {
#State var directions: [Direction] = [
Direction(symbol: "car", color: .mint, text: "Drive to SFO"),
Direction(symbol: "airplane", color: .blue, text: "Fly to SJC"),
Direction(symbol: "tram", color: .purple, text: "Ride to Cupertino"),
Direction(symbol: "bicycle", color: .orange, text: "Bike to Apple Park"),
Direction(symbol: "figure.walk", color: .green, text: "Walk to pond"),
Direction(symbol: "lifepreserver", color: .blue, text: "Swim to the center"),
Direction(symbol: "drop", color: .indigo, text: "Dive to secret airlock"),
Direction(symbol: "tram.tunnel.fill", color: .brown, text: "Ride through underground tunnels"),
Direction(symbol: "key", color: .red, text: "Enter door code:"),
]
var body: some View {
NavigationView {
List($directions) { $direction in
Label {
TextField("Instructions", text: $direction.text)
}
}
.listStyle(.sidebar)
.navigationTitle("Secret Hideout")
}
}
}
struct Direction: Identifiable {
var id = UUID()
var symbol: String
var color: Color
var text: String
}
No need to mess up with classes, Observable, Identifiable. You can do it all with structs.
Note, that version below will do fine for insertions, but fail if you try to delete array elements:
import SwiftUI
// the single item
struct KeyValue {
var key: String
var value: String
}
struct ContentView: View {
#State var boolArr: [KeyValue] = [KeyValue(key: "key1", value: "Value1"), KeyValue(key: "key2", value: "Value2"), KeyValue(key: "key3", value: "Value3"), KeyValue(key: "key4", value: "Value4")]
var body: some View {
NavigationView {
// id: \.self is obligatory if you need to insert
List(boolArr.indices, id: \.self) { idx in
HStack() {
TextField("key", text: self.$boolArr[idx].key)
Spacer()
TextField("value", text: self.$boolArr[idx].value)
Spacer()
}
}
.navigationBarItems(leading:
Button(action: {
self.boolArr.append(KeyValue(key: "key\(UInt.random(in: 0...100))", value: "value\(UInt.random(in: 0...100))"))
print(self.boolArr)
})
{ Text("Add") }
, trailing:
Button(action: {
self.boolArr.removeLast() // causes "Index out of range" error
print(self.boolArr)
})
{ Text("Remove") })
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Update:
A little trick to make it work with deletions as well.
import SwiftUI
// the single item
struct KeyValue {
var key: String
var value: String
}
struct KeyValueView: View {
#Binding var model: KeyValue
var body: some View {
HStack() {
TextField("Key", text: $model.key)
Spacer()
TextField("Value", text: $model.value)
Spacer()
}
}
}
struct ContentView: View {
#State var kvArr: [KeyValue] = [KeyValue(key: "key1", value: "Value1"), KeyValue(key: "key2", value: "Value2"), KeyValue(key: "key3", value: "Value3"), KeyValue(key: "key4", value: "Value4")]
var body: some View {
NavigationView {
List(kvArr.indices, id: \.self) { i in
KeyValueView(model: Binding(
get: {
return self.kvArr[i]
},
set: { (newValue) in
self.kvArr[i] = newValue
}))
}
.navigationBarItems(leading:
Button(action: {
self.kvArr.append(KeyValue(key: "key\(UInt.random(in: 0...100))", value: "value\(UInt.random(in: 0...100))"))
print(self.kvArr)
})
{ Text("Add") }
, trailing:
Button(action: {
self.kvArr.removeLast() // Works like a charm
print(self.kvArr)
})
{ Text("Remove") })
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Swift 5.5
This version of swift enables one line code for this by passing the bindable item directly from the array.
struct DirectionsList: View {
#Binding var directions: [Direction]
var body: some View {
List($directions) { $direction in
Label {
TextField("Instructions", text: $direction.text)
} icon: {
DirectionsIcon(direction)
}
}
}
}

Resources