Text not updating correctly based on picker - ios

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
}

Related

SwiftUI Picker in reusable component with protocol cannot conform to Hashable

I'm trying to build a reusable component that includes a SwiftUI Picker that can work with different types in several places in my app. I created a Pickable protocol that conforms to Hashable, but when I try to use it, the Picker and the ForEach complain that Type 'any Pickable' cannot conform to 'Hashable'
import SwiftUI
struct PickerRow: View {
let title: String
let options: [any Pickable]
#State var selection: any Pickable
var body: some View {
HStack {
Spacer()
Text(title)
.font(.subHeading)
Picker(title, selection: $selection, content: {
ForEach(options, id: \.self) {
Text($0.name)
}
}).pickerStyle(.menu)
}
}
}
protocol Pickable: Hashable {
var name: String { get }
}
Is there a way to get something like this to work without specifying a concrete type?
If you think about it, it makes sense.
What would you expect to happen if that code was valid and you used it like this?
struct ContentView: View {
let options = [PickableA(), PickableB()]
#State var selection = PickableC()
var body: some View {
PickerRow(title: "Choose one", options: options, selection: $selection)
}
}
That couldn't possibly work right?
What you need is a way to make sure that there is a constraint that forces options and selection to be of the same concrete type (consider Equatable for example, both String and Int are conforming, but you cannot compare them).
One possible solution would be a generic constraint in the declaration of your struct (also note the #Binding instead of #State since we modify external values):
struct PickerRow<Option: Pickable>: View {
let title: String
let options: [Option]
#Binding var selection: Option
var body: some View {
HStack {
Spacer()
Text(title)
.font(.subheadline)
Picker(title, selection: $selection) {
ForEach(options, id: \.self) {
Text($0.name)
}
}.pickerStyle(.menu)
}
}
}
which you could use like this:
struct Person: Pickable {
let name: String
}
struct ContentView: View {
let options = [Person(name: "Bob"), Person(name: "Alice")]
#State var selection = Person(name: "Bob")
var body: some View {
PickerRow(title: "Choose one", options: options, selection: $selection)
}
}

Sharing Data between Views in Swift/better approach for this?

I am brand new to Swift and SwiftUi, decided to pick it up for fun over the summer to put on my resume. As a college student, my first idea to get me started was a Check calculator to find out what each person on the check owes the person who paid. Right now I have an intro screen and then a new view to a text box to add the names of the people that ordered off the check. I stored the names in an array and wanted to next do a new view that asks for-each person that was added, what was their personal total? I am struggling with sharing data between different structs and such. Any help would be greatly appreciated, maybe there is a better approach without multiple views? Anyways, here is my code (spacing a little off cause of copy and paste):
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Image("RestaurantPhoto1").ignoresSafeArea()
VStack {
Text("TabCalculator")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.bottom, 150.0)
NavigationLink(
destination: Page2(),
label: {
Text("Get Started!").font(.largeTitle).foregroundColor(Color.white).padding().background(/*#START_MENU_TOKEN#*//*#PLACEHOLDER=View#*/Color.blue/*#END_MENU_TOKEN#*/)
})
}
}
}
}
}
struct Page2: View {
#State var nameArray = [String]()
#State var name: String = ""
#State var numberOfPeople = 0
#State var personTotal = 0
var body: some View {
NavigationView {
VStack {
TextField("Enter name", text: $name, onCommit: addName).textFieldStyle(RoundedBorderTextFieldStyle()).padding()
List(nameArray, id: \.self) {
Text($0)
}
}
.navigationBarTitle("Group")
}
}
func addName() {
let newName = name.capitalized.trimmingCharacters(in: .whitespacesAndNewlines)
guard newName.count > 0 else {
return
}
nameArray.append(newName)
name = ""
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
ContentView()
}
}
}
You have multiple level for passing data between views in SwiftUI. Each one has its best use cases.
Static init properties
Binding properties
Environment Objects
Static init properties.
You're probably used to that, it's just passing constants through your view init function like this :
struct MyView: View {
var body: some View {
MyView2(title: "Hello, world!")
}
}
struct MyView2: View {
let title: String
var body: some View {
Text(title)
}
}
Binding properties.
These enables you to pass data between a parent view and child. Parent can pass the value to the child on initialization and updates of this value and child view can update the value itself (which receives too).
struct MyView: View {
// State properties stored locally to MyView
#State private var title: String
var body: some View {
// Points the MyView2's "title" binding property to the local title state property using "$" sign in front of the property name.
MyView2(title: $title)
}
}
struct MyView2: View {
#Binding var title: String
var body: some View {
// Textfield presents the same value as it is stored in MyView.
// It also can update the title according to what the user entered with keyboard (which updates the value stored in MyView.
TextField("My title field", text: $title)
}
}
Environment Objects.
Those works in the same idea as Binding properties but the difference is : it passes the value globally through all children views. However, the property is to be an "ObservableObject" which comes from the Apple Combine API. It works like this :
// Your observable object
class MyViewManager: ObservableObject {
#Published var title: String
init(title: String) {
self.title = title
}
}
struct MyView: View {
// Store your Observable object in the parent View
#StateObject var manager = MyViewManager(title: "")
var body: some View {
MyView2()
// Pass the manager to MyView2 and its children
.environmentObject(manager)
}
}
struct MyView2: View {
// Read and Write access to parent environment object
#EnvironmentObject var manager: MyViewManager
var body: some View {
VStack {
// Read and write to the manager title property
TextField("My title field", text: $manager.title)
MyView3()
// .environmentObject(manager)
// No need to pass the environment object again, it is passed by inheritance.
}
}
}
struct MyView3: View {
#EnvironmentObject var manager: MyViewManager
var body: some View {
TextField("My View 3 title field", text: $manager.title)
}
}
Hope it was helpful. If it is, don't forget to mark this answer as the right one 😉
For others that are reading this to get a better understanding, don't forget to upvote by clicking on the arrow up icon 😄

Is there a way to create objects in swiftUI view based on a value gathered from a previous view?

I have recently started my journey into iOS development learning swift and swift UI. I keep running into issues when it comes to app architecture. The problem i am trying to solve is this: Let's say I have an app where the user first selects a number and then presses next. The user selected number is supposed to represent the number of text fields that appear on the next view. For example, if the user selects 3 then 3 text fields will appear on the next view but if the user selects 5 then 5 texts fields will appear. Is the solution to just have a view for each case? Or is there some way to dynamically add objects to a view based on the user input. Can anyone explain how they would handle a case like this?
Views can get passed parameters (including in NavigationLink) that can determine what they look like. Here's a simple example with what you described:
struct ContentView : View {
#State var numberOfFields = 3
var body: some View {
NavigationView {
VStack {
Stepper(value: $numberOfFields, in: 1...5) {
Text("Number of fields: \(numberOfFields)")
}
NavigationLink(destination: DetailView(numberOfFields: numberOfFields)) {
Text("Navigate")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailView : View {
var numberOfFields : Int
var body: some View {
VStack {
ForEach(0..<numberOfFields) { index in
TextField("", text: .constant("Field \(index + 1)"))
}
}
}
}
Notice how numberOfFields is stored as #State in the parent view and then passed to the child view dynamically.
In general, it would probably be a good idea to visit some SwiftUI tutorials as this type of thing will be covered by most of them. Apple's official tutorials are here: https://developer.apple.com/tutorials/swiftui
Another very popular resource is Hacking With Swift: https://www.hackingwithswift.com/100/swiftui
Update, based on comments:
struct ContentView : View {
#State var numberOfFields = 3
var body: some View {
NavigationView {
VStack {
Stepper(value: $numberOfFields, in: 1...5) {
Text("Number of fields: \(numberOfFields)")
}
NavigationLink(destination: DetailView(textInputs: Array(repeating: "test", count: numberOfFields))) {
Text("Navigate")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct Model : Identifiable {
var id = UUID()
var text : String
}
class ViewModel : ObservableObject {
#Published var strings : [Model] = []
}
struct DetailView : View {
var textInputs : [String]
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(Array(viewModel.strings.enumerated()), id: \.1.id) { (index,text) in
TextField("", text: $viewModel.strings[index].text)
}
}.onAppear {
viewModel.strings = textInputs.map { Model(text: $0) }
}
}
}

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())
}
}

Add new Element in Picker in SwiftUI

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))
}
}

Resources