I'm trying to change the row colour in the list.
struct ContentView: View {
#State var userProfiles : [Profile] = [Profile.default]
var body: some View {
VStack{
List(userProfiles.indices, id: \.self, rowContent: row(for:)).background(Color.red)
}.background(Color.red)
}
// helper function to have possibility to generate & inject proxy binding
private func row(for idx: Int) -> some View {
let isOn = Binding(
get: {
// safe getter with bounds validation
idx < self.userProfiles.count ? self.userProfiles[idx] : DtrProfile.default
},
set: { self.userProfiles[idx] = $0 }
)
return Toggle(isOn: isOn.isFollower, label: { Text("\(idx)").background(Color.red) } )
}
}
Output:-
I want to achieve:-
Can someone please explain to me how to change the full row colour. I've tried to implement by above but no results yet.
Any help would be greatly appreciated.
Thanks in advance.
Use listRowBackground
var body: some View {
VStack{
List {
ForEach(0...15, id: \.self, content: row(for:)).background(Color.red).listRowBackground(Color.red)
}
}
}
Related
It seems that there is a problem in SwiftUI with List and deleting items. The items in the list and data get out of sync.
This is the code sample that reproduces the problem:
import SwiftUI
struct ContentView: View {
#State var popupShown = false
var body: some View {
VStack {
Button("Show list") { popupShown.toggle() }
if popupShown {
MainListView()
}
}
.animation(.easeInOut, value: popupShown)
}
}
struct MainListView: View {
#State var texts = (0...10).map(String.init)
func delete(at positions: IndexSet) {
positions.forEach { texts.remove(at: $0) }
}
var body: some View {
List {
ForEach(texts, id: \.self) { Text($0) }
.onDelete { delete(at: $0) }
}
.frame(width: 300, height: 300)
}
}
If you perform a delete action on the first row and scroll to the last row, the data and list contents are not in sync anymore.
This is only happening when animation is attached to it. Removing .animation(.easeInOut, value: popupShown) workarounds the issue.
This code sample works as expected on iOS 14 and doesn't work on iOS 15.
Is there a workaround for this problem other then removing animation?
It isn't the animation(). The clue was seeing It appears that having the .animation outside of the conditional causes the problem. Moving it to the view itself corrected it to some extent. However, there is a problem with this ForEach construct: ForEach(texts, id: \.self). As soon as you start deleting elements of your array, the UI gets confused as to what to show where. You should ALWAYS use an Identifiable element in a ForEach. See the example code below:
struct ListDeleteView: View {
#State var popupShown = false
var body: some View {
VStack {
Button("Show list") { popupShown.toggle() }
if popupShown {
MainListView()
.animation(.easeInOut, value: popupShown)
}
}
}
}
struct MainListView: View {
#State var texts = (0...10).map({ TextMessage(message: $0.description) })
func delete(at positions: IndexSet) {
texts.remove(atOffsets: positions)
}
var body: some View {
List {
ForEach(texts) { Text($0.message) }
.onDelete { delete(at: $0) }
}
.frame(width: 300, height: 300)
}
}
struct TextMessage: Identifiable {
let id = UUID()
let message: String
}
For some reason I don't understand, when I add/remove items from a #State var in MainView, the OutterViews are not being updated properly.
What I am trying to achieve is that the user can only "flag" (select) one item at a time. For instance, when I click on "item #1" it will be flagged. If I click on another item then "item #1" will not be flagged anymore but only the new item I just clicked.
Currently, my code shows all items as if they were flagged even when they are not anymore. The following code has the minimum structure and functionality I'm implementing for MainView, OutterView, and InnerView.
I've tried using State vars instead of the computed property in OutterView, but it doesn't work. Also, I tried using a var instead of the computed property in OutterViewand initialized it in init() but also doesn't work.
Hope you can help me to find what I am doing wrong.
Thanks!
struct MainView: View {
#State var flagged: [String] = []
var data: [String] = ["item #1", "item #2", "item #3", "item #4", "item #5"]
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(data, id:\.self) { text in
OutterView(text: text, flag: flagged.contains(text)) { (flag: Bool) in
if flag {
flagged = [text]
} else {
if let index = flagged.firstIndex(of: text) {
flagged.remove(at: index)
}
}
}
}
}
Text("Flagged: \(flagged.description)")
Button(action: {
flagged = []
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
#State private var flag: Bool
private let text: String
private var color: Color { flag ? Color.green : Color.gray }
private var update: (Bool)->Void
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
update(flag)
}
}
init(text: String, flag: Bool = false, update: #escaping (Bool)->Void) {
self.text = text
self.update = update
_flag = State(initialValue: flag)
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
Here's a simple version that does what you're looking for (explained below):
struct Item : Identifiable {
var id = UUID()
var flagged = false
var title : String
}
class StateManager : ObservableObject {
#Published var items = [Item(title: "Item #1"),Item(title: "Item #2"),Item(title: "Item #3"),Item(title: "Item #4"),Item(title: "Item #5")]
func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.items[index].flagged
} set: { (newValue) in
self.items = self.items.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.flagged = newValue
} else {
//not the same index
if newValue {
itemCopy.flagged = false
}
}
return itemCopy
}
}
}
func reset() {
items = items.map { item in
var itemCopy = item
itemCopy.flagged = false
return itemCopy
}
}
}
struct MainView: View {
#ObservedObject var stateManager = StateManager()
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(Array(stateManager.items.enumerated()), id:\.1.id) { (index,item) in
OutterView(text: item.title, flag: stateManager.singularBinding(forIndex: index))
}
}
Text("Flagged: \(stateManager.items.filter({ $0.flagged }).map({$0.title}).description)")
Button(action: {
stateManager.reset()
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
var text: String
#Binding var flag: Bool
private var color: Color { flag ? Color.green : Color.gray }
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
}
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
What's happening:
There's a Item that has an ID for each item, the flagged state of that item, and the title
StateManager keeps an array of those items. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the item array. Any time a checkbox is set, it unchecks all of the other boxes.
The ForEach now gets an enumeration of the items. This could be done without enumeration, but it was easy to write the custom binding by index like this. You could also filter by ID instead of index. Note that because of the enumeration, it's using .1.id for the id parameter -- .1 is the item while .0 is the index.
Inside the ForEach, the custom binding from before is created and passed to the subview
In the subview, instead of using #State, #Binding is used (this is what the custom Binding is passed to)
Using this strategy of an ObservableObject that contains all of your state and passes it on via #Published properties and #Bindings makes organizing your data a lot easier. It also avoids having to pass closures back and forth like you were doing initially with your update function. This ends up being a pretty idiomatic way of doing things in SwiftUI.
Consider the following project with two views. The first view presents the second one:
import SwiftUI
struct ContentView: View {
private let data = 0...1000
#State private var selection: Set<Int> = []
#State private var shouldShowSheet = false
var body: some View {
self.showSheet()
//self.showPush()
}
private func showSheet() -> some View {
Button(action: {
self.shouldShowSheet = true
}, label: {
Text("Selected: \(selection.count) items")
}).sheet(isPresented: self.$shouldShowSheet) {
EditFormView(selection: self.$selection)
}
}
private func showPush() -> some View {
NavigationView {
Button(action: {
self.shouldShowSheet = true
}, label: {
NavigationLink(destination: EditFormView(selection: self.$selection),
isActive: self.$shouldShowSheet,
label: {
Text("Selected: \(selection.count) items")
})
})
}
}
}
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
#State private var editMode: EditMode = .active
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
Text("\(value)")
}
}.environment(\.editMode, self.$editMode)
}
}
Steps to reproduce:
Create an app with the above two views
Run the app and present the sheet with the editable list
Select some items at random indexes, for example a handful at index 0-10 and another handful at index 90-100
Close the sheet by swiping down/tapping back button
Open the sheet again
Scroll to indexes 90-100 to view the selection in the reused cells
Expected:
The selected indexes as you had will be in “selected state”
Actual:
The selection you had before is not marked as selected in the UI, even though the binding passed to List contains those indexes.
This occurs both on the “sheet” presentation and the “navigation link” presentation.
If you select an item in the list, the “redraw” causes the original items that were originally not shown as selected to now be shown as selected.
Is there a way around this?
It looks like EditMode bug, worth submitting feedback to Apple. The possible solution is to use custom selection feature.
Here is a demo of approach (modified only part). Tested & worked with Xcode 11.4 / iOS 13.4
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
self.cell(for: value)
}
}
}
// also below can be separated into standalone view
private func cell(for value: Int) -> some View {
let selected = self.selection.contains(value)
return HStack {
Image(systemName: selected ? "checkmark.circle" : "circle")
.foregroundColor(selected ? Color.blue : nil)
.font(.system(size: 24))
.onTapGesture {
if selected {
self.selection.remove(value)
} else {
self.selection.insert(value)
}
}.padding(.trailing, 8)
Text("\(value)")
}
}
}
I want make placeholder custom style so i try to use the method of Mojtaba Hosseini in SwiftUI. How to change the placeholder color of the TextField?
if text.isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
but in my case, I use a foreach with a Array for make a list of Textfield and Display or not the Text for simulate the custom placeholder.
ForEach(self.ListeEquip.indices, id: \.self) { item in
ForEach(self.ListeJoueurs[item].indices, id: \.self){idx in
// if self.ListeJoueurs[O][O] work
if self.ListeJoueurs[item][index].isEmpty {
Text("Placeholder")
.foregroundColor(.red)
}
}
}
How I can use dynamic conditional with a foreach ?
Now I have a another problem :
i have this code :
struct EquipView: View {
#State var ListeJoueurs = [
["saoul", "Remi"],
["Paul", "Kevin"]
]
#State var ListeEquip:[String] = [
"Rocket", "sayans"
]
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices) { item in
BulleEquip(EquipName: item, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
}
}
}
struct BulleEquip: View {
var EquipName = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
VStack{
VStack{
Text("Équipe \(EquipName+1)")
}
VStack { // Added this
ForEach(self.ListeJoueurs[EquipName].indices) { index in
ListeJoueurView(EquipNamed: self.EquipName, JoueurIndex: index, ListeJoueurs: self.$ListeJoueurs, ListeEquip: self.$ListeEquip)
}
HStack{
Button(action: {
self.ListeJoueurs[self.EquipName].append("") //problem here
}){
Text("button")
}
}
}
}
}
}
struct ListeJoueurView: View {
var EquipNamed = 0
var JoueurIndex = 0
#Binding var ListeJoueurs :[[String]]
#Binding var ListeEquip :[String]
var body: some View {
HStack{
Text("Joueur \(JoueurIndex+1)")
}
}
}
I can run the App but I have this error in console when I click the button :
ForEach, Int, ListeJoueurView> count (3) != its initial count (2). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
Can someone enlighten me?
TL;DR
You need a VStack, HStack, List, etc outside each ForEach.
Updated
For the second part of your question, you need to change your ForEach to include the id parameter:
ForEach(self.ListeJoueurs[EquipName].indices, id: \.self)
If the data is not constant and the number of elements may change, you need to include the id: \.self so SwiftUI knows where to insert the new views.
Example
Here's some example code that demonstrates a working nested ForEach. I made up a data model that matches how you were trying to call it.
struct ContentView: View {
// You can ignore these, since you have your own data model
var ListeEquip: [Int] = Array(1...3)
var ListeJoueurs: [[String]] = []
// Just some random data strings, some of which are empty
init() {
ListeJoueurs = (1...4).map { _ in (1...4).map { _ in Bool.random() ? "Text" : "" } }
}
var body: some View {
VStack { // Added this
ForEach(self.ListeEquip.indices, id: \.self) { item in
VStack { // Added this
ForEach(self.ListeJoueurs[item].indices, id: \.self) { index in
if self.ListeJoueurs[item][index].isEmpty { // If string is blank
Text("Placeholder")
.foregroundColor(.red)
} else { // If string is not blank
Text(self.ListeJoueurs[item][index])
}
}
}.border(Color.black)
}
}
}
}
Explanation
Here's what Apple's documentation says about ForEach:
A structure that computes views on demand from an underlying collection of of [sic] identified data.
So something like
ForEach(0..2, id: \.self) { number in
Text(number.description)
}
is really just shorthand for
Text("0")
Text("1")
Text("2")
So your ForEach is making a bunch of views, but this syntax for declaring views is only valid inside a View like VStack, HStack, List, Group, etc. The technical reason is because these views have an init that looks like
init(..., #ViewBuilder content: () -> Content)
and that #ViewBuilder does some magic that allows this unique syntax.
I want to change another unrelated #State variable when a Picker gets changed, but there is no onChanged and it's not possible to put a didSet on the pickers #State. Is there another way to solve this?
Deployment target of iOS 14 or newer
Apple has provided a built in onChange extension to View, which can be used like this:
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor, label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.onChange(of: favoriteColor) { tag in print("Color tag: \(tag)") }
}
}
Deployment target of iOS 13 or older
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor.onChange(colorChange), label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
}
func colorChange(_ tag: Int) {
print("Color tag: \(tag)")
}
}
Using this helper
extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> {
return Binding(
get: { self.wrappedValue },
set: { selection in
self.wrappedValue = selection
handler(selection)
})
}
}
First of all, full credit to ccwasden for the best answer. I had to modify it slightly to make it work for me, so I'm answering this question hoping someone else will find it useful as well.
Here's what I ended up with (tested on iOS 14 GM with Xcode 12 GM)
struct SwiftUIView: View {
#State private var selection = 0
var body: some View {
Picker(selection: $selection, label: Text("Some Label")) {
ForEach(0 ..< 5) {
Text("Number \($0)") }
}.onChange(of: selection) { _ in
print(selection)
}
}
}
The inclusion of the "_ in" was what I needed. Without it, I got the error "Cannot convert value of type 'Int' to expected argument type '()'"
I think this is simpler solution:
#State private var pickerIndex = 0
var yourData = ["Item 1", "Item 2", "Item 3"]
// USE this if needed to notify parent
#Binding var notifyParentOnChangeIndex: Int
var body: some View {
let pi = Binding<Int>(get: {
return self.pickerIndex
}, set: {
self.pickerIndex = $0
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// USE this if needed to notify parent
self.notifyParentOnChangeIndex = $0
})
return VStack{
Picker(selection: pi, label: Text("Yolo")) {
ForEach(self.yourData.indices) {
Text(self.yourData[$0])
}
}
.pickerStyle(WheelPickerStyle())
.padding()
}
}
I know this is a year old post, but I thought this solution might help others that stop by for a visit in need of a solution. Hope it helps someone else.
import Foundation
import SwiftUI
struct MeasurementUnitView: View {
#State var selectedIndex = unitTypes.firstIndex(of: UserDefaults.standard.string(forKey: "Unit")!)!
var userSettings: UserSettings
var body: some View {
VStack {
Spacer(minLength: 15)
Form {
Section {
Picker(selection: self.$selectedIndex, label: Text("Current UnitType")) {
ForEach(0..<unitTypes.count, id: \.self) {
Text(unitTypes[$0])
}
}.onReceive([self.selectedIndex].publisher.first()) { (value) in
self.savePick()
}
.navigationBarTitle("Change Unit Type", displayMode: .inline)
}
}
}
}
func savePick() {
if (userSettings.unit != unitTypes[selectedIndex]) {
userSettings.unit = unitTypes[selectedIndex]
}
}
}
I use a segmented picker and had a similar requirement. After trying a few things I just used an object that had both an ObservableObjectPublisher and a PassthroughSubject publisher as the selection. That let me satisfy SwiftUI and with an onReceive() I could do other stuff as well.
// Selector for the base and radix
Picker("Radix", selection: $base.value) {
Text("Dec").tag(10)
Text("Hex").tag(16)
Text("Oct").tag(8)
}
.pickerStyle(SegmentedPickerStyle())
// receiver for changes in base
.onReceive(base.publisher, perform: { self.setRadices(base: $0) })
base has both an objectWillChange and a PassthroughSubject<Int, Never> publisher imaginatively called publisher.
class Observable<T>: ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<T, Never>()
var value: T {
willSet { objectWillChange.send() }
didSet { publisher.send(value) }
}
init(_ initValue: T) { self.value = initValue }
}
typealias ObservableInt = Observable<Int>
Defining objectWillChange isn't strictly necessary but when I wrote that I liked to remind myself that it was there.
For people that have to support both iOS 13 and 14, I added an extension which works for both. Don't forget to import Combine.
Extension View {
#ViewBuilder func onChangeBackwardsCompatible<T: Equatable>(of value: T, perform completion: #escaping (T) -> Void) -> some View {
if #available(iOS 14.0, *) {
self.onChange(of: value, perform: completion)
} else {
self.onReceive([value].publisher.first()) { (value) in
completion(value)
}
}
}
}
Usage:
Picker(selection: $selectedIndex, label: Text("Color")) {
Text("Red").tag(0)
Text("Blue").tag(1)
}.onChangeBackwardsCompatible(of: selectedIndex) { (newIndex) in
print("Do something with \(newIndex)")
}
Important note: If you are changing a published property inside an observed object within your completion block, this solution will cause an infinite loop in iOS 13. However, it is easily fixed by adding a check, something like this:
.onChangeBackwardsCompatible(of: showSheet, perform: { (shouldShowSheet) in
if shouldShowSheet {
self.router.currentSheet = .chosenSheet
showSheet = false
}
})
SwiftUI 1 & 2
Use onReceive and Just:
import Combine
import SwiftUI
struct ContentView: View {
#State private var selection = 0
var body: some View {
Picker("Some Label", selection: $selection) {
ForEach(0 ..< 5, id: \.self) {
Text("Number \($0)")
}
}
.onReceive(Just(selection)) {
print("Selected: \($0)")
}
}
}
iOS 14 and CoreData entities with relationships
I ran into this issue while trying to bind to a CoreData entity and found that the following works:
Picker("Level", selection: $contact.level) {
ForEach(levels) { (level: Level?) in
HStack {
Circle().fill(Color.green)
.frame(width: 8, height: 8)
Text("\(level?.name ?? "Unassigned")")
}
.tag(level)
}
}
.onChange(of: contact.level) { _ in savecontact() }
Where "contact" is an entity with a relationship to "level".
The Contact class is an #ObservedObject var contact: Contact
saveContact is a do-catch function to try viewContext.save()...
The very important issue : we must pass something to "tag" modifier of Picker item view (inside ForEach) to let it "identify" items and trigger selection change event. And the value we passed will return to Binding variable with "selection" of Picker.
For example :
Picker(selection: $selected, label: Text("")){
ForEach(data){item in //data's item type must conform Identifiable
HStack{
//item view
}
.tag(item.property)
}
}
.onChange(of: selected, perform: { value in
//handle value of selected here (selected = item.property when user change selection)
})