I use Xcode 14.2, macOS Monterey 12.6.2 and Minimum Deployments 15.5.
I want to display 4 TextFields in a 2x2 grid for integer input.
The data is stored in an extern struct Model as an ObservableObject.
The environment variable is injected in the #main struct.
The TextField must be equipped with an .id modifier. Otherwise the following error message appears:
LazyVGridLayout: the ID 0 is used by multiple child views, this will give undefined results!
LazyVGridLayout: the ID 1 is used by multiple child views, this will give undefined results!
Adding .id(row + col) results in a single error message:
LazyVGridLayout: the ID 1 is used by multiple child views, this will give undefined results!
Using UUID() for generating an ID .id(UUID()) results in a strange effect. Trying to enter a multi digit value in a Textfeld fails. The software keyboard vanishes after the input of the first digit.
Changing the id to .id(row + 7 * col) results in the expected behavior of the demo App. However, this "solution" shouldn't be the right way to solve the problem.
Has somebody an idea was is going wrong?
struct ContentView: View {
let colums = [GridItem(),GridItem()]
#EnvironmentObject var model: Model
var body: some View {
List {
LazyVGrid(columns: colums) {
ForEach(0...1, id:\.self) { row in
ForEach(0...1, id: \.self) { col in
TextField("", value: $model.rows[row].values[col], format: .number)
.id(UUID())
// .id(row + 7 * col)
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class Model: ObservableObject {
#Published
var rows = [Values(), Values()]
struct Values {
var values = [0, 0]
}
}
#main
struct KeyboardFocusApp: App {
#StateObject var model = Model()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(model)
}
}
}
Following my comment please let me suggest a different approach. The whole fun of VGrid is that you don't need the grid structure in your data. You define the column and just throw the data in.
Also as commented it would be preferable to have the model data itself identifiable.
struct ContentView: View {
// #EnvironmentObject var model: Model
#StateObject var model = Model()
let colums = [GridItem(),GridItem()]
var body: some View {
List {
LazyVGrid(columns: colums) {
// no need for .id, as items are identifiable
// $ init makes $item modifiable
ForEach($model.items) { $item in
TextField("", value: $item.value, format: .number)
}
}
}
}
}
class Model: ObservableObject {
init() { // initialize with 4 items
items = []
for _ in 0..<4 {
items.append(Item())
}
}
#Published var items: [Item]
struct Item: Identifiable {
let id = UUID()
var value = 0
}
}
Related
I have 4 dogs - represented by 4 Pickers - and each one of them has a favourite treat. How can I store the selection of each Picker? I tried storing it in a dictionary, but when clicking on one of the treats nothing gets selected and the Picker view also does not get dismissed.
import SwiftUI
import OrderedCollections
enum Dog : String, CaseIterable, Identifiable {
var id: Self { self }
case Anton, Ben, Charlie, Didi
}
struct MainConstants {
let treats : OrderedDictionary <String, Int> = [
"Bone" : 123,
"Sausage" : 456,
"Cookies" : 789
]
}
struct FavouriteTreatsView: View {
let constants = MainConstants()
#State var favouriteTreats : [Dog : String] = [:]
var body: some View {
NavigationView {
Form {
ForEach (Dog.allCases) {dog in
Picker(dog.rawValue ,selection: $favouriteTreats[dog]) {
ForEach (constants.treats.keys, id: \.self) {treat in
Text(treat)
}
}
}
}
}
}
}
struct FavoriteTreatsView_Previews: PreviewProvider {
static var previews: some View {
FavouriteTreatsView()
}
}
I don't know about treating dogs, however the example below works well with lions...
First, you can have a dedicated view for each single picker - that view will store the treat of each lion on a #State var. You use the .id() modifier to tell the picker what value needs to be stored.
The view will also have a #Binding var that will receive the dictionary, to store the treat selected on the right lion.
In that picker view, you listen to changes in the treat: when a treat is selected, you store that value in the dictionary passed with the binding var.
So, assuming they are lions, here's the picker view:
struct TreatMe: View {
let constants = MainConstants()
// treat for a single Lion
#State private var treat = ""
// The dictionary that will be updated after the treat is selected
#Binding var favouriteTreats: [Lion: String]
// the Lion that will receive the treat
let lion: Lion
var body: some View {
Picker(lion.rawValue ,selection: $treat) {
ForEach (constants.treats.keys, id: \.self) {treat in
Text(treat)
// This will ensure the right value is stored
.id(treat)
}
}
// Listen to changes in the treat, then store it in the dictionary
.onChange(of: treat) { value in
favouriteTreats[lion] = value
}
}
}
You call it from the FavouriteTreatsView:
struct FavouriteTreatsView: View {
#State var favouriteTreats : [Lion : String] = [:]
var body: some View {
NavigationView {
Form {
ForEach (Lion.allCases) {lion in
// Pass the variables
TreatMe(favouriteTreats: $favouriteTreats, lion: lion)
}
}
}
}
}
hi am having issues with the picker view in swiftui
i have created one file with just a class like this
import Foundation
import SwiftUI
class something: ObservableObject {
#Published var sel = 0
}
and then I created 2 views
import SwiftUI
struct ContentView: View {
#StateObject var hihi: something
var characters = ["Makima", "Ryuk", "Itachi", "Gojou", "Goku", "Eren", "Levi", "Jiraya", "Ichigo", "Sukuna"]
var body: some View {
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(characters[hihi.sel])
}
now(hihi: something())
}
}
}
struct now: View {
#StateObject var hihi: something
var body: some View {
Text("\(hihi.sel)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(hihi: something())
}
}
now the problem am facing is that the code compiles but the picker ain't working it won't change to any other value in the array I have provided it recoils back to its original value provided that is 0th index "Makima" and it won't select any other option, why so?
please help
There are three problems, the main one being the mismatching selection.
In the Picker, your selection is based on the string value for each character. This is because the ForEach identifies each Text by the name string, since you used id: \.self.
However, your something model (which ideally should start with a capital letter by convention) has a numeric selection. Because the items in the Picker have String IDs, and this is an Int, the selection can't be set.
You can change your model to this:
class something: ObservableObject {
#Published var sel = "Makima"
}
Which also requires a slight change in the body:
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(hihi.sel) // Now getting string directly
}
now(hihi: something())
}
Notice we now have two views showing the selected character - but only the top one updates. The bottom one may now be redundant (the now view), but I'll show you how you can get it working anyway. This is where we encounter the 2nd problem:
You are creating a new instance of something() when passing it to now (again, should start with a capital). This means that the current instance of hihi stored in ContentView is not passed along. You are just creating a new instance of something, which uses the default value. This is completely independent from the hihi instance.
Replace:
now(hihi: something())
With:
now(hihi: hihi)
The final problem, which may not be as visible, is that you shouldn't be using #StateObject in now, since it doesn't own the object/data. Instead, the object is passed in, so you should use #ObservedObject instead. Although the example now works even without this change, you will have issues later on when trying to change the object within the now view.
Replace #StateObject in now with #ObservedObject.
Full answer (something is initialized in ContentView only for convenience of testing):
struct ContentView: View {
#StateObject var hihi: something = something()
var characters = ["Makima", "Ryuk", "Itachi", "Gojou", "Goku", "Eren", "Levi", "Jiraya", "Ichigo", "Sukuna"]
var body: some View {
VStack {
Section{
Picker("Please choose a character", selection: $hihi.sel) {
ForEach(characters, id: \.self) { name in
Text(name)
}
}
Text(hihi.sel)
}
now(hihi: hihi)
}
}
}
struct now: View {
#ObservedObject var hihi: something
var body: some View {
Text(hihi.sel)
}
}
class something: ObservableObject {
#Published var sel = "Makima"
}
I have a list of sliders, but I have a problem updating the text that shows the slider value.
The app workflow is like this:
User taps to add a new slider to the list.
An object that defines the slider is created and stored in an array.
The class that has the array as a property (Db) is an ObservableObject and triggers a View update for each new item.
The list is updated with a new row.
So far, so good. Each row has a slider whose value is stored in a property in an object in an array. However, the value text doesn't update as soon as the slider is moved, but when a new item is added. Please see the GIF below:
The Slider doesn't update the text value when moved
How can I bind the slider movements to the text value? I thought that by defining
#ObservedObject var slider_value: SliderVal = SliderVal()
and binding that variable to the slider, the value would be updated simultaneously but that is not the case. Thanks a lot for any help.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var db: Db
var body: some View {
NavigationView{
List(db.criteria_db){criteria in
VStack {
HStack{
Text(criteria.name).bold()
Spacer()
Text(String(criteria.slider_value.value)) //<-- Problem here
}
Slider(value: criteria.$slider_value.value, in:0...100, step: 1)
}
}
.navigationBarTitle("Criteria")
.navigationBarItems(trailing:
Button(action: {
Criteria.count += 1
db.criteria_db.append(Criteria(name: "Criteria\(Criteria.count)"))
dump(db.criteria_db)
}, label: {
Text("Add Criteria")
})
)
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(db: Db())
}
}
struct Criteria: Identifiable {
var id = UUID()
var name: String
#ObservedObject var slider_value: SliderVal = SliderVal()
static var count: Int = 0
init(name: String) {
self.name = name
}
}
class Db: ObservableObject {
#Published var criteria_db: [Criteria] = []
}
class SliderVal: ObservableObject {
#Published var value:Double = 50
}
The #ObservableObject won't work within a struct like that -- it's only useful inside a SwiftUI View or a DynamicProperty. With your use case, because the class is a reference type, the #Published property has no way of knowing that the SliderVal was changed, so the owner View never gets updated.
You can fix this by turning your model into a struct:
struct Criteria: Identifiable {
var id = UUID()
var name: String
var slider_value: SliderVal = SliderVal()
static var count: Int = 0
init(name: String) {
self.name = name
}
}
struct SliderVal {
var value:Double = 50
}
The problem, once you do this, is you don't have a Binding to use in your List. If you're lucky enough to be on SwiftUI 3.0 (iOS 15 or macOS 12), you can use $criteria within your list to get a binding to the element being currently iterated over.
If you're on an earlier version, you'll need to either use indexes to iterate over the items, or, my favorite, create a custom binding that is tied to the id of the item. It looks like this:
struct ContentView: View {
#ObservedObject var db: Db = Db()
private func bindingForId(id: UUID) -> Binding<Criteria> {
.init {
db.criteria_db.first { $0.id == id } ?? Criteria(name: "")
} set: { newValue in
db.criteria_db = db.criteria_db.map {
$0.id == id ? newValue : $0
}
}
}
var body: some View {
NavigationView{
List(db.criteria_db){criteria in
VStack {
HStack{
Text(criteria.name).bold()
Spacer()
Text(String(criteria.slider_value.value))
}
Slider(value: bindingForId(id: criteria.id).slider_value.value, in:0...100, step: 1)
}
}
.navigationBarTitle("Criteria")
.navigationBarItems(trailing:
Button(action: {
Criteria.count += 1
db.criteria_db.append(Criteria(name: "Criteria\(Criteria.count)"))
dump(db.criteria_db)
}, label: {
Text("Add Criteria")
})
)
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(db: Db())
}
}
class Db: ObservableObject {
#Published var criteria_db: [Criteria] = []
}
Now, because the models are all value types (structs), the View and #Published know when to update and your sliders work as expected.
try something like this:
Slider(value: criteria.$slider_value.value, in:0...100, step: 1)
.onChange(of: criteria.slider_value.value) { newVal in
DispatchQueue.main.async {
criteria.slider_value.value = newVal
}
}
I want to allow the user to filter data in a long list to more easily find matching titles.
I have placed a TextView inside my navigation bar:
.navigationBarTitle(Text("Library"))
.navigationBarItems(trailing: TextField("search", text: $modelData.searchString)
I have an observable object which responds to changes in the search string:
class DataModel: ObservableObject {
#Published var modelData: [PDFSummary]
#Published var searchString = "" {
didSet {
if searchString == "" {
modelData = Realm.studyHallRealm.objects(PDFSummary.self).sorted(by: { $0.name < $1.name })
} else {
modelData = Realm.studyHallRealm.objects(PDFSummary.self).sorted(by: { $0.name < $1.name }).filter({ $0.name.lowercased().contains(searchString.lowercased()) })
}
}
}
Everything works fine, except I have to tap on the field after entering each letter. For some reason the focus is taken away from the field after each letter is entered (unless I tap on a suggested autocorrect - the whole string is correctly added to the string at once)
The problem is in rebuilt NavigationView completely that result in dropped text field focus.
Here is working approach. Tested with Xcode 11.4 / iOS 13.4
The idea is to avoid rebuild NavigationView based on knowledge that SwiftUI engine updates only modified views, so using decomposition we make modifications local and transfer desired values only between subviews directly not affecting top NavigationView, as a result the last kept stand.
class QueryModel: ObservableObject {
#Published var query: String = ""
}
struct ContentView: View {
// No QueryModel environment object here -
// implicitly passed down. !!! MUST !!!
var body: some View {
NavigationView {
ResultsView()
.navigationBarTitle(Text("Library"))
.navigationBarItems(trailing: SearchItem())
}
}
}
struct ResultsView: View {
#EnvironmentObject var qm: QueryModel // << injected here from top
var body: some View {
VStack {
Text("Search: \(qm.query)") // receive query string
}
}
}
struct SearchItem: View {
#EnvironmentObject var qm: QueryModel // << injected here from top
#State private var query = "" // updates only local view
var body: some View {
let text = Binding(get: { self.query }, set: {
self.query = $0; self.qm.query = $0; // transfer query string
})
return TextField("search", text: text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(QueryModel())
}
}
What is the best way to distinguish even and odd rows in ForEach loop? Contents of the loop is not numbers (i.e. User struct), and it can be filtered using search phrase (using just index of item in array is not applicable in that way, I think). I need to change a color of that rows.
If I understand question correctly, you can use indices of your data array and check condition index % 2 == 1 (because indices begins from 0) for odd rows. For filtered data I suggest computed value:
import SwiftUI
import Combine
struct HighlightingRowData: Identifiable {
let id = UUID()
let title: String
}
final class SomeData: ObservableObject {
#Published var data: [HighlightingRowData] = [HighlightingRowData(title: "R. Martin"), HighlightingRowData(title: "McConell"), HighlightingRowData(title: "London"), HighlightingRowData(title: "London")]
}
struct HighlitedRowsInList: View {
#EnvironmentObject var someData: SomeData
#State private var searchedText = ""
private var filteredData: [HighlightingRowData] {
return searchedText == "" ? someData.data : someData.data.filter { $0.title.contains(searchedText) }
}
var body: some View {
List {
TextField("filter", text: $searchedText)
ForEach(filteredData.indices, id: \.self) { rowIndex in
HStack {
Text(self.filteredData[rowIndex].title)
Spacer()
}
.background(rowIndex % 2 == 1 ? Color.yellow : Color.clear)
}
}
}
}
struct HighlitedRowsInList_Previews: PreviewProvider {
static var previews: some View {
HighlitedRowsInList()
.environmentObject(SomeData())
}
}
you can achieve something like this with this code: