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:
Related
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
}
}
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 got a problem with Picker. When I try to change state of data in selected segment it's drops it (gif attached). How I can have stay selected segment in Picker when displaying data is changed (when I press "love" button, selected segment should stay without changes)
Single item has option:
struct Landmark: Identifiable, Codable, Hashable {
var id: Int
var name: String
var imageName: String
var mainImage: Image {
Image(imageName)
var liked: Bool
var popular: Bool
var recommended: Bool
}
I got class with array of objects:
final class ModelData: ObservableObject {
#Published var landmarks: [Landmark]
}
And trying use picker like that:
struct tempSegmentControl: View {
#EnvironmentObject var modelData: ModelData
#State private var selected = ModelData().landmarks
var popularLandmarks: [Landmark] {
modelData.landmarks.filter { landmark in
(landmark.popular)
}
}
var rocommendedLandmarks: [Landmark] {
modelData.landmarks.filter { landmark in
(landmark.recommended)
}
}
var body: some View {
VStack {
Picker("Selection", selection: $selected) {
Text("All").tag(modelData.landmarks)
Text("Popular").tag(popularLandmarks)
Text("Recommended").tag(rocommendedLandmarks)
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: UIScreen.main.bounds.width * 0.95)
ForEach(selected, id: \.id) { land in
NavigationLink(
destination: DetailView2(landmark: land)) {
MainItem3(landmark: land)
}
}
}
}
}
Drop selected segment
selection: isn't going to work properly with an array type like this ([Landmark]).
Instead, you'll probably want to do something simple like:
#State private var selection = 0
//...
Picker("Selection", selection: $selection) {
Text("All").tag(0)
Text("Popular").tag(1)
Text("Recommended").tag(2)
And then use a computed property for your array that you iterate through later:
var selected : [Landmark] {
switch selection {
case 0:
return modelData.landmarks
case 1:
//etc
}
}
You could do this with an enum : Int as well, which would probably be a little safer later on if you cases expand (and it would give you a limited set in your computed property).
I want a dynamic array of mutable strings to be presented by a mother view with a list of child views, each presenting one of the strings, editable. Also, the mother view will show a concatenation of the strings which will update whenever one of the strings are updated in the child views.
Can't use (1) ForEach(self.model.strings.indices) since set of indices may change and can't use (2) ForEach(self.model.strings) { string in since the sub views wants to edit the strings but string will be immutable.
The only way I have found to make this work is to make use of an #EnvironmentObject that is passed around along with the parameter. This is really clunky and borders on offensive.
However, I am new to swiftui and I am sure there a much better way to go about this, please let know!
Here's what I have right now:
import SwiftUI
struct SimpleModel : Identifiable { var id = UUID(); var name: String }
let simpleData: [SimpleModel] = [SimpleModel(name: "text0"), SimpleModel(name: "text1")]
final class UserData: ObservableObject { #Published var simple = simpleData }
struct SimpleRowView: View {
#EnvironmentObject private var userData: UserData
var simple: SimpleModel
var simpleIndex: Int { userData.simple.firstIndex(where: { $0.id == simple.id })! }
var body: some View {
TextField("title", text: self.$userData.simple[simpleIndex].name)
}
}
struct SimpleView: View {
#EnvironmentObject private var userData: UserData
var body: some View {
let summary_binding = Binding<String>(
get: {
var arr: String = ""
self.userData.simple.forEach { sim in arr += sim.name }
return arr;
},
set: { _ = $0 }
)
return VStack() {
TextField("summary", text: summary_binding)
ForEach(userData.simple) { tmp in
SimpleRowView(simple: tmp).environmentObject(self.userData)
}
Button(action: { self.userData.simple.append(SimpleModel(name: "new text"))}) {
Text("Add text")
}
}
}
}
Where the EnironmentObject is created and passed as SimpleView().environmentObject(UserData()) from AppDelegate.
EDIT:
For reference, should someone find this, below is the full solution as suggested by #pawello2222, using ObservedObject instead of EnvironmentObject:
import SwiftUI
class SimpleModel : ObservableObject, Identifiable {
let id = UUID(); #Published var name: String
init(name: String) { self.name = name }
}
class SimpleArrayModel : ObservableObject, Identifiable {
let id = UUID(); #Published var simpleArray: [SimpleModel]
init(simpleArray: [SimpleModel]) { self.simpleArray = simpleArray }
}
let simpleArrayData: SimpleArrayModel = SimpleArrayModel(simpleArray: [SimpleModel(name: "text0"), SimpleModel(name: "text1")])
struct SimpleRowView: View {
#ObservedObject var simple: SimpleModel
var body: some View {
TextField("title", text: $simple.name)
}
}
struct SimpleView: View {
#ObservedObject var simpleArrayModel: SimpleArrayModel
var body: some View {
let summary_binding = Binding<String>(
get: { return self.simpleArrayModel.simpleArray.reduce("") { $0 + $1.name } },
set: { _ = $0 }
)
return VStack() {
TextField("summary", text: summary_binding)
ForEach(simpleArrayModel.simpleArray) { simple in
SimpleRowView(simple: simple).onReceive(simple.objectWillChange) {_ in self.simpleArrayModel.objectWillChange.send()}
}
Button(action: { self.simpleArrayModel.simpleArray.append(SimpleModel(name: "new text"))}) {
Text("Add text")
}
}
}
}
You don't actually need an #EnvironmentObject (it will be available globally for all views in your environment).
You may want to use #ObservedObject instead (or #StateObject if using SwiftUI 2.0):
...
return VStack {
TextField("summary", text: summary_binding)
ForEach(userData.simple, id:\.id) { tmp in
SimpleRowView(userData: self.userData, simple: tmp) // <- pass userData to child views
}
Button(action: { self.userData.simple.append(SimpleModel(name: "new text")) }) {
Text("Add text")
}
}
struct SimpleRowView: View {
#ObservedObject var userData: UserData
var simple: SimpleModel
...
}
Note that if your data is not constant you should use a dynamic ForEach loop (with an explicit id parameter):
ForEach(userData.simple, id:\.id) { ...
However, the best results you can achieve when you make your SimpleModel a class and ObservableObject. Here is a better solution how do do it properly:
SwiftUI update data for parent NavigationView
Also, you can simplify your summary_binding using reduce:
let summary_binding = Binding<String>(
get: { self.userData.simple.reduce("") { $0 + $1.name } },
set: { _ = $0 }
)
I'm currently a beginner in swiftUI and I just wanted to know how to calculate the number of rows in a list. For example: Lets say my list has x rows:
List {
Text("row1")
Text("row2")
Text("row3")
//and so on....
}
How would I find how many rows are there? I've tried researching this, but I just come across more harder and complex code.
Instead of the x Text elements in your example you could use an array with a state wrapper:
struct ExampleView: View {
#State var rowElements: [String] = ["row1", "row2", "row3"]
var body: some View {
List(rowElements, id: \.self) {rowElement in
Text(rowElement)
}
}
When you now add/remove Elements from the array your List automatically updates. That means the number of List rows equals the number of Strings contained in rowElements and can be read with rowElements.count()
This will help both of you to do it way easier without having to hard code or even create that array.
import SwiftUI
struct Example: View {
var body: some View {
List(0 ..< 5) { item in
DetailExampleView(count: item)
}
}
}
struct Example_Previews: PreviewProvider {
static var previews: some View {
Example()
}
}
struct DetailExampleView: View {
#State var count: Int = 0
var body: some View {
Text("Item no.\(count)")
}
}
Anyway, if you would like your count to start on 1 instead of 0, just add
DetailExampleView(count: item + 1)