Why My SwiftUI List does not update elements on the list? - swiftui

This is my sample, completely possible to test example:
import SwiftUI
struct Category: Identifiable {
var name: String
var color: Color
var id = UUID()
init(name: String, color: Color) {
self.name = name
self.color = color
}
}
struct CategoryWrapper: Identifiable {
var id: UUID
let category: Category
let isSelected: Bool
init(category: Category, isSelected: Bool) {
self.category = category
self.isSelected = isSelected
self.id = category.id
}
}
class ViewModel: ObservableObject {
#Published var wrappers = [CategoryWrapper]()
var selectedIdentifier = UUID()
private var categories: [Category] = [
Category(name: "PURPLE", color: .purple),
Category(name: "GRAY", color: .gray),
Category(name: "YELLOW", color: .yellow),
Category(name: "BROWN", color: .brown),
Category(name: "green", color: .green),
Category(name: "red", color: .red),
]
init() {
reload()
}
func reload() {
wrappers = categories.map { CategoryWrapper(category: $0, isSelected: $0.id == selectedIdentifier) }
}
}
typealias CategoryAction = (Category?) -> Void
struct CategoryView: View {
var category: Category
#State var isSelected: Bool = false
private var action: CategoryAction?
init(category: Category, isSelected: Bool, action: #escaping CategoryAction) {
self.category = category
self.isSelected = isSelected
self.action = action
}
var body: some View {
Button {
isSelected.toggle()
action?(isSelected ? category : nil)
} label: {
Text(category.name)
.font(.caption)
.foregroundColor(.white)
.background(isSelected ? category.color : .clear)
.frame(width: 150, height: 24)
.cornerRadius(12)
}
}
}
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
ScrollView {
Text("Categories")
ForEach(viewModel.wrappers) { wrapper in
CategoryView(
category: wrapper.category,
isSelected: wrapper.isSelected
) { category in
viewModel.selectedIdentifier = category?.id ?? UUID()
viewModel.reload()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and the result is:
Every row view is tappable. Every tap should select current category and deselect the others. Why doesn't it work? When I tap on gray or yellow, previously selected rows are not deselected. Why?

There are many problems with your code:
Since you have single selection, selectedIdentifier should be optional;
In CategoryView, you don't need to mark isSelected with #State because the list will reload itself after setting selectedIdentifier & calling reload():
struct Category: Identifiable {
var name: String
var color: Color
var id = UUID()
init(name: String, color: Color) {
self.name = name
self.color = color
}
}
struct CategoryWrapper: Identifiable {
var id: UUID
let category: Category
let isSelected: Bool
init(category: Category, isSelected: Bool) {
self.category = category
self.isSelected = isSelected
self.id = category.id
}
}
class ViewModel: ObservableObject {
#Published var wrappers = [CategoryWrapper]()
var selectedIdentifier: UUID?
private var categories: [Category] = [
Category(name: "PURPLE", color: .purple),
Category(name: "GRAY", color: .gray),
Category(name: "YELLOW", color: .yellow),
Category(name: "BROWN", color: .brown),
Category(name: "green", color: .green),
Category(name: "red", color: .red),
]
init() {
reload()
}
func reload() {
wrappers = categories.map { CategoryWrapper(category: $0, isSelected: $0.id == selectedIdentifier) }
}
}
typealias CategoryAction = (Category?) -> Void
struct CategoryView: View {
var category: Category
var isSelected: Bool = false
private var action: CategoryAction?
init(category: Category, isSelected: Bool, action: #escaping CategoryAction) {
self.category = category
self.isSelected = isSelected
self.action = action
}
var body: some View {
Button {
action?(!isSelected ? category : nil)
} label: {
Text(category.name)
.font(.caption)
.foregroundColor(.white)
.frame(width: 150, height: 24)
.background(isSelected ? category.color : .clear)
.cornerRadius(12)
}
}
}
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
ScrollView {
Text("Categories")
ForEach(viewModel.wrappers) { wrapper in
CategoryView(
category: wrapper.category,
isSelected: wrapper.isSelected
) { category in
viewModel.selectedIdentifier = category?.id
viewModel.reload()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

Updating state variable does not update TextField's text

I have TextField in a form which should be updated dynamically, on a specific user action. Since I'm embedding this SwiftUI view inside a UIKit view, I had to customize it a bit:
/// Base TextField
struct CustomTextFieldSUI: View {
#State var text: String = ""
var placeholder: Text
var disableAutoCorrect = false
var isSecure: Bool = false
var onEditingChanged: (Bool) -> () = { _ in }
var onCommit: () -> () = { }
var onChange: (String) -> Void
var keyboardType: UIKeyboardType = .default
var contentType: UITextContentType?
var body: some View {
ZStack(alignment: .leading) {
if text.isEmpty {
placeholder
.foregroundColor(placeholderColor)
}
if isSecure {
SecureField("", text: $text.onChange({ newText in
onChange(newText)
}), onCommit: onCommit)
.foregroundColor(foregroundColor)
} else {
TextField("", text: $text.onChange({ newText in
onChange(newText)
}), onEditingChanged: onEditingChanged, onCommit: onCommit)
.foregroundColor(foregroundColor)
.disableAutocorrection(disableAutoCorrect)
.keyboardType(keyboardType)
.textContentType(contentType)
.autocapitalization(keyboardType == UIKeyboardType.emailAddress ? .none : .words)
}
}
}
}
Which itself is used to create another custom view:
struct TextFieldWithIconSUI: View {
#State var text: String = ""
#State var isSecure: Bool
let icon: Image
let placeholder: String
var prompt: String? = nil
let disableAutoCorrect: Bool = false
var keyboardType: UIKeyboardType = .default
var contentType: UITextContentType?
var onEditingChanged: (Bool) -> () = { _ in }
var onCommit: () -> () = { }
var onChange: (String) -> Void
var body: some View {
VStack {
ZStack {
RoundedRectangle(cornerRadius: 0)
HStack(alignment: .center, spacing: iconSpacing) {
CustomTextFieldSUI(
text: text,
placeholder: Text(placeholder),
placeholderColor: .gray,
foregroundColor: .white,
disableAutoCorrect: disableAutoCorrect,
isSecure: isSecure, onEditingChanged: onEditingChanged, onCommit: onCommit, onChange: { newText in
text = newText
onChange(newText)
},
keyboardType: keyboardType,
contentType: contentType)
icon
.scaledToFit()
.onTapGesture {
isSecure.toggle()
}
}
}
if (prompt != nil) {
VStack(alignment: .leading) {
Text(prompt!)
.padding(.leading, 10)
.padding(.top, -2)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(2)
}
}
}
}
}
Now in the client view I'm using it with a list to create a simple auto-complete list:
TextFieldWithIconSUI(text: displayCountryName, isSecure: false, icon: emptyImage, placeholder: "Country", keyboardType: .default, contentType: .countryName, onEditingChanged: { changed in
self.shouldShowCountriesList = changed
}, onChange: { text in
self.displayCountryName = text
self.shouldShowCountriesList = true
})
And somewhere in my form, I'm using the text from that TextField to show the autocorrect list. The list is shown but when I select an item it does not update the TextField's text, in debugger the state variable is updated, but the UI is not showing the new value.
if shouldShowCountriesList {
VStack(alignment: .leading) {
List {
ForEach(countriesList.filter { $0.lowercased().hasPrefix(country.object.lowercased()) }.prefix(3), id: \.self) { countryName in
Text(countryName)
.onTapGesture {
self.displayCountryName = countryName
self.shouldShowCountriesList = false
}
}
}
}
}
I had to use Binding variables for my custom text field:
struct CustomTextFieldSUI: View {
var text: Binding<String>
var placeholder: Text
var placeholderColor: Color
var foregroundColor: Color
var disableAutoCorrect = false
var isSecure: Bool = false
var onEditingChanged: (Bool) -> () = { _ in }
var onCommit: () -> () = { }
var onChange: (String) -> Void
var keyboardType: UIKeyboardType = .default
var contentType: UITextContentType?
var body: some View {
let bindingText = Binding(
get: {
self.text.wrappedValue
},
set: {
self.text.wrappedValue = $0
onChange($0)
})
ZStack(alignment: .leading) {
if text.wrappedValue.isEmpty {
placeholder
.foregroundColor(placeholderColor)
}
if isSecure {
SecureField("", text: bindingText, onCommit: onCommit)
.foregroundColor(foregroundColor)
} else {
TextField("", text: bindingText, onEditingChanged: onEditingChanged, onCommit: onCommit)
.foregroundColor(foregroundColor)
.disableAutocorrection(disableAutoCorrect)
.keyboardType(keyboardType)
.textContentType(contentType)
.autocapitalization(keyboardType == UIKeyboardType.emailAddress ? .none : .words)
}
}
}
}
And passed the binding variables from the host.

Weird behavior matchedGeometryEffect with list

Why has only the orange Color a right animation? Green and Red is laying under the list while the animation, but why?
With VStavk there is no problem but with list. Want an animation when switching from list View to Grid View.
struct Colors: Identifiable{
var id = UUID()
var col: Color
}
struct ContentView: View {
#State var on = true
#Namespace var ani
var colors = [Colors(col: .green),Colors(col: .orange),Colors(col: .red)]
var body: some View {
VStack {
if on {
List{
ForEach(colors){col in
col.col
.matchedGeometryEffect(id: "\(col.id)", in: ani)
.animation(.easeIn)
}
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.listStyle(InsetGroupedListStyle())
.frame(height: 400)
} else {
LazyVGrid(columns: [GridItem(.fixed(200)),GridItem(.fixed(200))], content: {
ForEach(colors){col in
col.col
.matchedGeometryEffect(id: "\(col.id)", in: ani)
.animation(.easeIn)
}
})
.frame(height: 400)
}
Button("toggle"){
withAnimation(.easeIn){
on.toggle()
}
}
}
}
Thanks to the comment of #Asperi and this post: Individually modifying child views passed to a container using #ViewBuilder in SwiftUI with the answer of #Tushar Sharma I tried something like this:
import SwiftUI
struct SomeContainerView<Content: View>:View {
var ani: Namespace.ID
var model:[Model] = []
init(namespace: Namespace.ID,model:[Model],#ViewBuilder content: #escaping (Model) -> Content) {
self.content = content
self.model = model
ani = namespace
}
let content: (Model) -> Content
var body: some View {
VStack{
ForEach(model,id:\.id){model in
content(model)
.background(Color.gray.matchedGeometryEffect(id: model.id, in: ani))
}
}
}
}
struct ContentView:View {
#ObservedObject var modelData = Objects()
#Namespace var ani
#State var show = true
var body: some View{
VStack{
Toggle("toggle", isOn: $show.animation())
if show{
SomeContainerView(namespace: ani,model: modelData.myObj){ data in
HStack{
Text("\(data.name)")
data.color.frame(width: 100,height : 100)
}
}
}else{
LazyVGrid(columns: [GridItem(.fixed(110)),GridItem(.fixed(110))],spacing: 10){
ForEach(modelData.myObj){model in
Text("\(model.name)")
.frame(width: 100,height: 100)
.background(Color.gray.matchedGeometryEffect(id: model.id, in: ani))
}
}
}
}
}
}
struct Model: Identifiable{
var id = UUID().uuidString
var name:String
var color:Color
init(name:String,color:Color) {
self.name = name
self.color = color
}
}
class Objects:ObservableObject{
#Published var myObj:[Model] = []
init() {
initModel()
}
func initModel(){
let model = Model(name: "Jack", color: .green)
let model1 = Model(name: "hey Jack", color: .red)
let model2 = Model(name: "hey billy", color: .red)
myObj.append(model)
myObj.append(model1)
myObj.append(model2)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to Add Color beside text into the list in swiftUI?(Data Flow)

I try to get( text & color ) from user and add them to the list in SwiftUI I already can pass text data but unfortunately for color I can't while they should be the same, below there is an image of app.To work we should provide a Binding for PreAddTextField .Thanks for your help
here is my Code :
import SwiftUI
struct AddListView: View {
#Binding var showAddListView : Bool
#ObservedObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(text: "", color: .purple)) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id:
\.id and not \.self
PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id), colorPickerColor: <#Binding<Color>#>)
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
AddListView(showAddListView: .constant(false),appState: AppState())
}
}
struct PreAddTextField: View {
#Binding var textInTextField : String
#Binding var colorPickerColor : Color
var body: some View {
HStack {
TextField("Enter text", text: $textInTextField)
ColorPicker("", selection: $colorPickerColor)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var save : () -> Void
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
// What should happen here to add Text to List???
save()
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
var addItem : () -> Void
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
addItem()
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
DataModel :
import SwiftUI
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
var color : Color
}
class AppState : ObservableObject {
#Published var textData : [Text1] = [.init(text: "Item 1", color: .purple),.init(text: "Item 2", color: .purple)]
}
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(text: "", color: .purple)] //start with one empty item
//save all of the new items -- don't save anything that is empty
func saveToAppState(appState: AppState) {
appState.textData.append(contentsOf: textItemsToAdd.filter { !$0.text.isEmpty })
}
//these Bindings get used for the TextFields -- they're attached to the item IDs
func bindingForId(id: UUID) -> Binding<String> {
.init { () -> String in
self.textItemsToAdd.first(where: { $0.id == id })?.text ?? ""
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return .init(id: id, text: newValue, color: .purple)
}
}
}
}
and finaly :
import SwiftUI
struct ListView: View {
#StateObject var appState = AppState() //store the AppState here
#State private var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(appState.textData, id : \.self){ text in
HStack {
Image(systemName: "square")
.foregroundColor(text.color)
Text(text.text)
}
}
if showAddListView {
AddListView(showAddListView: $showAddListView, appState: appState)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
Instead of using a Binding for just the String, you could create a Binding for the entire Text1 item:
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(text: "", color: .purple)]
func saveToAppState(appState: AppState) {
appState.textData.append(contentsOf: textItemsToAdd.filter { !$0.text.isEmpty })
}
func bindingForId(id: UUID) -> Binding<Text1> { //now returns a Text1
.init { () -> Text1 in
self.textItemsToAdd.first(where: { $0.id == id }) ?? Text1(text: "", color: .clear)
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return newValue
}
}
}
}
struct PreAddTextField: View {
#Binding var item : Text1
var body: some View {
HStack {
TextField("Enter text", text: $item.text) //gets the text property of the binding
ColorPicker("", selection: $item.color) //gets the color property
}
}
}
struct AddListView: View {
#Binding var showAddListView : Bool
#ObservedObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(text: "", color: .purple)) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in
PreAddTextField(item: viewModel.bindingForId(id: item.id)) //parameter is changed here
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}

SwiftUI: Index out of range when deleting TextField() but not Text()

The code below throws an index out of range error when deleting TextField() but not when deleting Text().
Here is the full error: Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
import SwiftUI
struct Information: Identifiable {
let id: UUID
var title: String
}
struct ContentView: View {
#State var infoList = [
Information(id: UUID(), title: "Word"),
Information(id: UUID(), title: "Words"),
Information(id: UUID(), title: "Wording"),
]
var body: some View {
Form {
ForEach(0..<infoList.count, id: \.self){ item in
Section{
// Text(infoList[item].title) //<-- this doesn't throw error when deleted
TextField(infoList[item].title, text: $infoList[item].title)
}
}.onDelete(perform: deleteItem)
}
}
private func deleteItem(at indexSet: IndexSet) {
self.infoList.remove(atOffsets: indexSet)
}
}
#Asperi's response about creating a dynamic container was correct. I just had to tweak a bit to make it compatible with Identifiable. Final code below:
import SwiftUI
struct Information: Identifiable {
let id: UUID
var title: String
}
struct ContentView: View {
#State var infoList = [
Information(id: UUID(), title: "Word"),
Information(id: UUID(), title: "Words"),
Information(id: UUID(), title: "Wording"),
]
var body: some View {
Form {
ForEach(0..<infoList.count, id: \.self){ item in
EditorView(container: self.$infoList, index: item, text: infoList[item].title)
}.onDelete(perform: deleteItem)
}
}
private func deleteItem(at indexSet: IndexSet) {
self.infoList.remove(atOffsets: indexSet)
}
}
struct EditorView : View {
var container: Binding<[Information]>
var index: Int
#State var text: String
var body: some View {
TextField("", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = Information(id: UUID(), title: text)
})
}
}

How to update a single child view in SwiftUI?

I am trying to establish a SwiftUI connection between the child view and the parent view. By clicking on any child view, I want to redraw only the view that has been tapped, not the entire parent view.
The current implementation below does not allow you to redraw the view when you click on it, as it has a derived value.
I tried different scenarios by adding the BindableObject protocol to CustomColor, but without success.
class CustomColor: Identifiable {
let id = UUID()
var color: Color
init(color: Color) {
self.color = color
}
func change(to color: Color) {
self.color = color
}
}
class ColorStore: BindableObject {
var colors: [CustomColor] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<ColorStore, Never>()
init() {
self.colors = Array.init(repeating: CustomColor(color: .red), count: 10)
}
}
struct ContentView: View {
#EnvironmentObject var colorStore: ColorStore
var body: some View {
NavigationView {
List {
ForEach(colorStore.colors) { color in
ColorShape(color: color)
}
}.navigationBarTitle(Text("Colors"))
}
}
}
struct ColorShape: View {
var color: CustomColor
var body: some View {
Button(action:
{ self.color.change(to: .blue) }
, label: {
ShapeView(shape: Circle(), style: color.color)
})
}
}
I think I've found a solution.
The first problem was that I initialized array of colors by repeating the same element instead of adding independent ones.
What is more CustomColor itself should have BindableObject conformance, not the model (we don't change the array of colors, we change each color).
Lastly, we don't need to wrap objects in ForEach element (we loose reusability that way), and instead we put them in List element.
With this implementation, only the view that has been changed will be redrawn, not the entire collection.
Here is the code:
class CustomColor: BindableObject, Identifiable {
var didChange = PassthroughSubject<CustomColor, Never>()
let id = UUID()
var color: Color {
didSet {
self.didChange.send(self)
}
}
init(color: Color) {
self.color = color
}
func change(toColor color: Color) {
self.color = color
}
}
class ColorStore {
var colors: [CustomColor] = []
init() {
(0...10).forEach { _ in colors.append(CustomColor(color: .red)) }
}
}
struct ContentView: View {
let colorStore: ColorStore
var body: some View {
NavigationView {
List(colorStore.colors) { color in
ColorShape(color: color)
}.navigationBarTitle(Text("Colors"))
}
}
}
struct ColorShape: View {
#ObjectBinding var color: CustomColor
var body: some View {
Button(action: { self.color.change(toColor: .blue) }, label: {
ShapeView(shape: Circle(), style: color.color)
})
}
}
At now there is no possibility to update specific child view and it cannot be expected I think.
As was told on Data flow Through Swift UI session once you are changing #State property or Bindable object - all the changes flow down through view hierarchy and SwiftUI framework is comparing all the views and rendering again only what has changed.
I can offer three versions with subtle differences.
All Of them toggle individual buttons and keep the whole model - ColorStore var in sync. Allows adding and removing of elements in the array of colours. Also note, we can go without Identifiable conformance for array elements to list them.
Version 1. The closest to the question: all models are classes.
class CustomColor: ObservableObject, Identifiable {
var didChange = PassthroughSubject<CustomColor, Never>()
let id = UUID()
var color: Color {
didSet {
objectWillChange.send()
}
}
init(color: Color) {
self.color = color
}
func change(to color: Color) {
self.color = color
}
}
class ColorStore: ObservableObject {
var didChange = PassthroughSubject<ColorStore, Never>()
var colors: [CustomColor] = [] {
didSet {
objectWillChange.send()
}
}
init() {
(0...10).forEach { _ in colors.append(CustomColor(color: .red)) }
}
}
struct ContentView: View {
#ObservedObject var colorStore: ColorStore = ColorStore()
var body: some View {
NavigationView {
List(colorStore.colors) { c in
ColorShape(color: c)
}
// will work without `Identifiable`
// List(colorStore.colors.indices, id: \.self) { c in
// ColorShape(color: self.colorStore.colors[c])
// }
.navigationBarTitle(Text("Colors"))
.navigationBarItems(leading:
Button(action: { self.colorStore.colors.append(CustomColor(color: .green)) }) {
Text("Add")
}, trailing:
Button(action: {
self.colorStore.colors.removeLast()
print(self.colorStore.colors)
}, label: { Text("Remove") }))
}
}
}
struct ColorShape: View {
#ObservedObject var color: CustomColor
var body: some View {
Button(action:
{ self.color.change(to: .blue)
print(self.color)
}
, label: {
Circle().fill(color.color)
})
}
}
Version 2. The CustomColor is rewritten as struct.
// No need for manual `ObservableObject, Identifiable` conformance
struct CustomColor /*: Identifiable */ {
// let id = UUID()
var color: Color
init(color: Color) {
self.color = color
}
mutating func change(to color: Color) {
self.color = color
}
}
class ColorStore: ObservableObject {
var didChange = PassthroughSubject<ColorStore, Never>()
// If `CustomColor` is a `struct` i.e. value type, we can populate array with independent values, not with the same reference by using `repeating:` init.
var colors: [CustomColor] = Array(repeating: CustomColor(color: .red), count: 10) {
didSet {
objectWillChange.send()
}
}
/* init() {
(0...10).forEach { _ in colors.append(CustomColor(color: .red)) }
} */
}
struct ContentView: View {
#ObservedObject var colorStore: ColorStore = ColorStore()
var body: some View {
NavigationView {
List {
// Strange, bu if we omit ForEach, we will get an error on element removal from array.
ForEach(colorStore.colors.indices, id: \.self)
{ c in
ColorShape(color: self.$colorStore.colors[c])
}
}
.navigationBarTitle(Text("Colors"))
.navigationBarItems(leading:
Button(action: { self.colorStore.colors.append(CustomColor(color: .green)) }) {
Text("Add")
}, trailing:
Button(action: {
self.colorStore.colors.removeLast()
print(self.colorStore.colors)
}, label: { Text("Remove") }))
}
}
}
struct ColorShape: View {
#Binding var color: CustomColor
var body: some View {
Button(action:
{ self.color.change(to: .blue)
print(self.color)
}
, label: {
Circle().fill(color.color)
})
}
}
Version 3. The main model ColorStore and it's subtype CustomColor are rewritten as structs. No need to manually conform to ObservableObject.
struct CustomColor /* : Identifiable */ {
// let id = UUID()
var color: Color
init(color: Color) {
self.color = color
}
mutating func change(to color: Color) {
self.color = color
}
}
struct ColorStore {
// If `CustomColor` is a `struct` i.e. value type, we can populate array with independent values, not with the same reference by using `repeating:` init.
var colors: [CustomColor] = Array(repeating: CustomColor(color: .red), count: 10)
}
struct ContentView: View {
#State var colorStore: ColorStore = ColorStore()
var body: some View {
NavigationView {
List{
ForEach(colorStore.colors.indices, id: \.self) { i in
return ColorShape(color: self.$colorStore.colors[i])
}
}
.navigationBarTitle(Text("Colors"))
.navigationBarItems(leading:
Button(action: { self.colorStore.colors.append(CustomColor(color: .green)) }) {
Text("Add")
}, trailing:
// Removing causes index out of bound error (bug?)
Button(action: {
self.colorStore.colors.removeLast()
print(self.colorStore.colors)}) {
Text("Remove") })
}
}
}
struct ColorShape: View {
#Binding var color: CustomColor
var body: some View {
Button(action: {
self.color.change(to: .blue)
print(self.color)
}) {
Circle().fill(color.color)
}
}
}