Is there a way to carry a variable/bool up and into a View's SuperView? - swift

Say I were to have the follow sudocode as a View named Item
Item{
Rectangle()
.frame(width: 100, height: 50)
.onTapGesture{
isTapped.toggle()
}
}
And there were multiple Items in it's superview, Content
Content{
VStack{
Item()
Item()
Item()
}
}
Would there be a way for me to relay the variable/bool isTapped from Item, up and into it's superview with it still being Item specific? (So I know which Item has what isTapped value) so that for example, this would be possible?...
Content{
VStack{
Item().padding(.bottom, Item.isTapped ? 20 : 0)
Item().padding(.bottom, Item.isTapped ? 20 : 0)
Item().padding(.bottom, Item.isTapped ? 20 : 0)
}
}
edit: A key detail of note is that the number of Items would be generated dynamically by the user, so I can't for instance make a variable for each item

Declare the variable as #State in the parent and pass it into the child as #Binding.

A possible solution is:
First define a "Source of truth" Model
import SwiftUI
import Combine
import Foundation
enum itemType{
case itemTypeA
case itemTypeB
case itemTypeC
}
struct ItemModel: Identifiable {
var id: Int
var type: itemType
var isTapped: Bool = false
}
class ObserverModel: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
#Published var itemA: ItemModel = ItemModel(id: 1, type: .itemTypeA)
#Published var itemB: ItemModel = ItemModel(id: 2, type: .itemTypeB)
#Published var itemC: ItemModel = ItemModel(id: 3, type: .itemTypeC)
func toggleItem(itemType: itemType) {
switch itemType {
case .itemTypeA:
itemA.isTapped.toggle()
case .itemTypeB:
itemB.isTapped.toggle()
case .itemTypeC:
itemC.isTapped.toggle()
}
}
}
Your single ItemView should look like this:
import SwiftUI
struct ItemView: View {
#EnvironmentObject var observer: ObserverModel
#Binding var itemBinding: ItemModel
#State private var toggleColor = Color.red
var body: some View {
Rectangle()
.fill(toggleColor)
.frame(width: 100, height: 50)
.onTapGesture{
self.observer.toggleItem(itemType: self.itemBinding.type)
self.toggleColor = self.itemBinding.isTapped ? Color.green : Color.red
print("Item Bool: \(self.itemBinding.isTapped)")
}
}
}
struct ItemView_Previews: PreviewProvider {
static var previews: some View {
ItemView(itemBinding: .constant(ItemModel(id: 1, type: .itemTypeA))).environmentObject(ObserverModel())
}
}
Finally you put it all together:
import SwiftUI
struct ItemsView: View {
#EnvironmentObject var observer: ObserverModel
var body: some View {
HStack {
ItemView(itemBinding: $observer.itemA)
ItemView(itemBinding: $observer.itemB)
ItemView(itemBinding: $observer.itemC)
}
}
}
struct ItemsView_Previews: PreviewProvider {
static var previews: some View {
ItemsView().environmentObject(ObserverModel())
}
}
I uploaded this to github: https://github.com/ppoh71/SwiftUIButtonTest
(Built with GM2)
And I would recommend this video about the data flow in SwiftUI.
this should explain everything: https://developer.apple.com/videos/play/wwdc2019/226/

Related

#EnvironmentObject property not working properly in swiftUI

Updating cartArray from ViewModel doesn't append to the current elements, but adds object everytime freshly. I need to maintain cartArray as global array so that it can be accessed from any view of the project. I'm adding elements to cartArray from ViewModel. I took a separate class DataStorage which has objects that can be accessible through out the project
Example_AppApp.swift
import SwiftUI
#main
struct Example_AppApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(DataStorage())
}
}
}
DataStorage.swift
import Foundation
class DataStorage: ObservableObject {
#Published var cartArray = [Book]()
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var showSheetView = false
var body: some View {
NavigationView{
ListViewDisplay()
.navigationBarItems(trailing:
Button(action: {
self.showSheetView.toggle()
}) {
Image(systemName: "cart.circle.fill")
.font(Font.system(.title))
}
)
}.sheet(isPresented: $showSheetView) {
View3()
}
}
}
struct ListViewDisplay: View{
var book = [
Book(bookId: 1 ,bookName: "Catch-22"),
Book(bookId: 2 ,bookName: "Just-Shocking" ),
Book(bookId: 3 ,bookName: "Stephen King" ),
Book(bookId: 4,bookName: "A Gentleman in Moscow"),
]
var body: some View {
List(book, id: \.id) { book in
Text(book.bookName)
NavigationLink(destination: View1(book: book)) {
}
}
}
}
View1Modal.swift
import Foundation
struct Book: Codable, Identifiable {
var id:String{bookName}
var bookId : Int
var bookName: String
}
struct BookOption: Codable{
var name: String
var price: Int
}
View1ViewModel.swift
import Foundation
import Combine
class View1ViewModel : ObservableObject{
var dataStorage = DataStorage()
func addBook (bookId:Int ,bookName : String){
dataStorage.cartArray.append(Book(bookId:bookId, bookName: bookName)) // Adding to global array
print(dataStorage.cartArray)
}
}
View1.swift
import SwiftUI
struct View1: View {
#ObservedObject var vwModel = View1ViewModel()
#EnvironmentObject var datastrg: DataStorage
var book:Book
var body: some View {
Text(book.bookName).font(.title)
Spacer()
Button(action: {
vwModel.addBook(bookId: book.bookId, bookName: book.bookName)
}, label: {
Text("Add Book to Cart")
.frame(maxWidth: .infinity, minHeight: 60)
.background(Color.red)
.foregroundColor(Color.white)
.font(.custom("OpenSans-Bold", size: 24))
})
}
}
View3.swift
import SwiftUI
struct View3: View {
#EnvironmentObject var datastorage : DataStorage
var body: some View {
NavigationView {
List(datastorage.cartArray,id:\.id){book in
VStack{
Text(book.bookName)
.font(.custom("OpenSans-Bold", size: 20))
}
}
.navigationBarTitle(Text("Cart"), displayMode: .inline)
}
}
}
When addBook func is called for the first time it prints as
[Example_App.Book(bookId: 1, bookName: "Catch-22")]
When I go back and come back to this View1 and add another book by calling addBook func it adds as new object to cartArray
[Example_App.Book(bookId: 3, bookName: "Stephen King")]
Printing number of elements in cartArray gives as 1 element instead of 2 elements. When I go to View3 and display the Books in list, cartArray shows as empty(0 elements)
I think there is something wrong with var dataStorage = DataStorage() in ViewModel class. Everytime this is being created freshly, so the prevoius values are not stored. But I couldn't understand how to preserve its state
How to display List in View3 ? Any ideas/ suggestions will be helpful
You need to have one instance of DataStorage that gets passed around. Any time you write DataStorage() that creates a new instance.
.environmentObject will let you inject that one instance into the view hierarchy. Then, you can use the #EnvironmentObject property wrapper to access it within a View.
Inside View1, I used onAppear to set the dataStorage property on View1ViewModel -- that means that it has to be an optional on View1ViewModel since it will not be set in init. The reason I'm avoiding setting it in init is because an #EnvironmentObject is not set as of the init of the View -- it gets injected at render time.
#main
struct Example_AppApp: App {
var dataStorage = DataStorage()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(dataStorage)
}
}
}
class DataStorage: ObservableObject {
#Published var cartArray = [Book]()
}
struct ContentView: View {
#State var showSheetView = false
var body: some View {
NavigationView{
ListViewDisplay()
.navigationBarItems(trailing:
Button(action: {
self.showSheetView.toggle()
}) {
Image(systemName: "cart.circle.fill")
.font(Font.system(.title))
}
)
}.sheet(isPresented: $showSheetView) {
View3()
}
}
}
struct ListViewDisplay: View {
var book = [
Book(bookId: 1 ,bookName: "Catch-22"),
Book(bookId: 2 ,bookName: "Just-Shocking" ),
Book(bookId: 3 ,bookName: "Stephen King" ),
Book(bookId: 4,bookName: "A Gentleman in Moscow"),
]
var body: some View {
List(book, id: \.id) { book in
Text(book.bookName)
NavigationLink(destination: View1(book: book)) {
}
}
}
}
struct Book: Codable, Identifiable {
var id:String{bookName}
var bookId : Int
var bookName: String
}
struct BookOption: Codable{
var name: String
var price: Int
}
class View1ViewModel : ObservableObject{
var dataStorage : DataStorage?
func addBook (bookId:Int ,bookName : String) {
guard let dataStorage = dataStorage else {
fatalError("DataStorage not set")
}
dataStorage.cartArray.append(Book(bookId:bookId, bookName: bookName)) // Adding to global array
print(dataStorage.cartArray)
}
}
struct View1: View {
#ObservedObject var vwModel = View1ViewModel()
#EnvironmentObject var datastrg: DataStorage
var book:Book
var body: some View {
Text(book.bookName).font(.title)
Spacer()
Button(action: {
vwModel.addBook(bookId: book.bookId, bookName: book.bookName)
}, label: {
Text("Add Book to Cart")
.frame(maxWidth: .infinity, minHeight: 60)
.background(Color.red)
.foregroundColor(Color.white)
.font(.custom("OpenSans-Bold", size: 24))
})
.onAppear {
vwModel.dataStorage = datastrg
}
}
}
struct View3: View {
#EnvironmentObject var datastorage : DataStorage
var body: some View {
NavigationView {
List(datastorage.cartArray,id:\.id){book in
VStack{
Text(book.bookName)
.font(.custom("OpenSans-Bold", size: 20))
}
}
.navigationBarTitle(Text("Cart"), displayMode: .inline)
}
}
}
You are not calling your function addBook anywhere, add an onappear to your view3 calling the function and your list will populate with data.

How to create bindable custom objects in SWIFT? (conform with ObservableObject)

XCode Version 12.5 (12E262) - Swift 5
To simplify this example, I've created a testObj class and added a few items to an array.
Let's pretend that I want to render buttons on the screen (see preview below), once you click on the button, it should set testObj.isSelected = true which triggers the button to change the background color.
I know it's changing the value to true, however is not triggering the button to change its color.
Here's the example:
//
// TestView.swift
//
import Foundation
import SwiftUI
import Combine
struct TestView: View {
#State var arrayOfTestObj:[testObj] = [
testObj(label: "test1"),
testObj(label: "test2"),
testObj(label: "test3")
]
var body: some View {
VStack {
ForEach(arrayOfTestObj, id: \.id) { o in
HStack {
Text(o.label)
.width(200)
.padding(20)
.background(Color.red.opacity(o.isSelected ? 0.4: 0.1))
.onTapGesture {
o.isSelected.toggle()
}
}
}
}
}
}
class testObj: ObservableObject {
let didChange = PassthroughSubject<testObj, Never>()
var id:String = UUID().uuidString {didSet {didChange.send((self))}}
var label:String = "" {didSet {didChange.send((self))}}
var value:String = "" {didSet {didChange.send((self))}}
var isSelected:Bool = false {didSet {didChange.send((self))}}
init (label:String? = "") {
self.label = label!
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
If I update the ForEach as...
ForEach($arrayOfTestObj, id: \.id) { o in
... then I get this error:
Key path value type '_' cannot be converted to contextual type '_'
How can I change testObj to make it bindable?
Any help is greatly appreciated.
struct TestView: View {
#State var arrayOfTestObj:[TestObj] = [
TestObj(label: "test1"),
TestObj(label: "test2"),
TestObj(label: "test3")
]
var body: some View {
VStack {
ForEach(arrayOfTestObj, id: \.id) { o in
//Use a row view
TestRowView(object: o)
}
}
}
}
//You can observe each object by creating a RowView
struct TestRowView: View {
//And by using this wrapper you observe changes
#ObservedObject var object: TestObj
var body: some View {
HStack {
Text(object.label)
.frame(width:200)
.padding(20)
.background(Color.red.opacity(object.isSelected ? 0.4: 0.1))
.onTapGesture {
object.isSelected.toggle()
}
}
}
}
//Classes and structs should start with a capital letter
class TestObj: ObservableObject {
//You don't have to declare didChange if you need to update manually use the built in objectDidChange
let id:String = UUID().uuidString
//#Published will notify of changes
#Published var label:String = ""
#Published var value:String = ""
#Published var isSelected:Bool = false
init (label:String? = "") {
self.label = label!
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}

Placing SwiftUI Data Sources Somewhere Else

I'm trying to use SwiftUI in a project but beyond the very basic version of using #States and #Bindings that can be found in every tutorial, so I need some help on what I'm doing wrong here.
Environment Setup:
I have following files involved with this problem:
CustomTextField: It's a SwiftUI View that contains an internal TextField along with bunch of other things (According to the design)
CustomTextFieldConfiguration: Contains the things that I need to configure on my custom textfield view
RootView: It's a SwiftUI View that is using CustomTextField as one of it's subviews
RootPresenter: This is where the UI Logic & Presentation Logic goes (Between the view and business logic)
RootPresentationModel: It's the viewModel through which the Presenter can modify view's state
RootBuilder: It contains the builder class that uses the builder pattern to wire components together
The Problem:
The textField value does not update in the textValue property of rootPresentationModel
Here are the implementations (Partially) as I have done and have no idea where I have gone wrong:
CustomTextField:
struct CustomTextField: View {
#Binding var config: CustomTextFieldConfiguration
var body: some View {
ZStack {
VStack {
VStack {
ZStack {
HStack {
TextField($config.placeHolder,
value: $config.textValue,
formatter: NumberFormatter(),
onEditingChanged: {_ in },
onCommit: {})
.frame(height: 52.0)
.padding(EdgeInsets(top: 0, leading: 16 + ($config.detailActionImage != nil ? 44 : 0),
bottom: 0, trailing: 16 + ($config.contentAlignment == .center && $config.detailActionImage != nil ? 44 : 0)))
.background($config.backgroundColor)
.cornerRadius($config.cornerRedius)
.font($config.font)
...
...
...
...
CustomTextFieldConfiguration:
struct CustomTextFieldConfiguration {
#Binding var textValue: String
...
...
...
...
RootView:
struct RootView: View {
#State var configuration: CustomTextFieldConfiguration
var interactor: RootInteractorProtocol!
#Environment(\.colorScheme) private var colorScheme
var body: some View {
HStack {
Spacer(minLength: 40)
VStack(alignment: .trailing) {
CustomTextField(config: $configuration)
Text("\(configuration.textValue)")
}
Spacer(minLength: 40)
}
}
}
RootPresenter:
class RootPresenter: BasePresenter {
#ObservedObject var rootPresentationModel: RootPresentationModel
init(presentationModel: RootPresentationModel) {
rootPresentationModel = presentationModel
}
...
...
...
RootPresentationModel:
class RootPresentationModel: ObservableObject {
var textValue: String = "" {
didSet {
print(textValue)
}
}
}
RootBuilder:
class RootBuilder: BaseBuilder {
class func build() -> (RootView, RootInteractor) {
let interactor = RootInteractor()
let presenter = RootPresenter(presentationModel: RootPresentationModel())
let view: RootView = RootView(configuration: CustomTextFieldConfiguration.Presets.priceInput(textValue: presenter.$rootPresentationModel.textValue, placeholder: "", description: ""), interactor: interactor)
let router = RootRouter()
interactor.presenter = presenter
interactor.router = router
return (view, interactor)
}
}
(That Presets method doesn't do anything important, but just to make sure it will not raise an irrelevant question, here's the implementation):
static func priceInput(textValue: Binding<String>, placeholder: String, description: String) -> CustomTextFieldConfiguration {
return CustomTextFieldConfiguration(textValue: textValue,
placeHolder: placeholder,
description: description,
defaultDescription: description,
textAlignment: .center,
descriptionAlignment: .center,
contentAlignment: .center,
font: CustomFont.headline1))
}
import SwiftUI
struct CustomTextField: View {
#EnvironmentObject var config: CustomTextFieldConfiguration
#Binding var textValue: Double
var body: some View {
ZStack {
VStack {
VStack {
ZStack {
HStack {
//Number formatter forces the need for Double
TextField(config.placeHolder,
value: $textValue,
formatter: NumberFormatter(),
onEditingChanged: {_ in },
onCommit: {})
.frame(height: 52.0)
//.padding(EdgeInsets(top: 0, leading: 16 + (Image(systemName: config.detailActionImageName) != nil ? 44 : 0),bottom: 0, trailing: 16 + (config.contentAlignment == .center && Image(systemName: config.detailActionImageName) != nil ? 44 : 0)))
.background(config.backgroundColor)
.cornerRadius(config.cornerRedius)
.font(config.font)
}
}
}
}
}
}
}
class CustomTextFieldConfiguration: ObservableObject {
#Published var placeHolder: String = "place"
#Published var detailActionImageName: String = "checkmark"
#Published var contentAlignment: UnitPoint = .center
#Published var backgroundColor: Color = Color(UIColor.secondarySystemBackground)
#Published var font: Font = .body
#Published var cornerRedius: CGFloat = CGFloat(5)
#Published var description: String = ""
#Published var defaultDescription: String = ""
#Published var textAlignment: UnitPoint = .center
#Published var descriptionAlignment: UnitPoint = .center
init() {
}
init(placeHolder: String, description: String, defaultDescription: String, textAlignment: UnitPoint,descriptionAlignment: UnitPoint,contentAlignment: UnitPoint, font:Font) {
self.placeHolder = placeHolder
self.description = description
self.defaultDescription = defaultDescription
self.textAlignment = textAlignment
self.descriptionAlignment = descriptionAlignment
self.contentAlignment = contentAlignment
self.font = font
}
struct Presets {
static func priceInput(placeholder: String, description: String) -> CustomTextFieldConfiguration {
return CustomTextFieldConfiguration(placeHolder: placeholder, description: description,defaultDescription: description,textAlignment: .center,descriptionAlignment: .center,contentAlignment: .center, font:Font.headline)
}
}
}
struct RootView: View {
#ObservedObject var configuration: CustomTextFieldConfiguration
//var interactor: RootInteractorProtocol!
#Environment(\.colorScheme) private var colorScheme
#Binding var textValue: Double
var body: some View {
HStack {
Spacer(minLength: 40)
VStack(alignment: .trailing) {
CustomTextField(textValue: $textValue).environmentObject(configuration)
Text("\(textValue)")
}
Spacer(minLength: 40)
}
}
}
//RootPresenter is a class #ObservedObject only works properly in SwiftUI Views/struct
class RootPresenter//: BasePresenter
{
//Won't work can't chain ObservableObjects
// var rootPresentationModel: RootPresentationModel
//
// init(presentationModel: RootPresentationModel) {
// rootPresentationModel = presentationModel
// }
}
class RootPresentationModel: ObservableObject {
#Published var textValue: Double = 12 {
didSet {
print(textValue)
}
}
}
struct NewView: View {
//Must be observed directly
#StateObject var vm: RootPresentationModel = RootPresentationModel()
//This cannot be Observed
let presenter: RootPresenter = RootPresenter()
var body: some View {
RootView(configuration: CustomTextFieldConfiguration.Presets.priceInput(placeholder: "", description: ""), textValue: $vm.textValue//, interactor: interactor
)
}
}
struct NewView_Previews: PreviewProvider {
static var previews: some View {
NewView()
}
}

How to pass data object among views so its values can be modified?

I have created an object that represents the current state of drawing:
class ColoringImageViewModel: ObservableObject {
#Published var shapeItemsByKey = [UUID: ShapeItem]()
var shapeItemKeys: [UUID] = []
var scale: CGFloat = 0
var offset: CGSize = CGSize.zero
var dragGestureMode: DragGestureEnum = DragGestureEnum.FillAreas
#Published var selectedColor: Color?
var selectedImage: String?
init(selectedImage: String) {
let svgURL = Bundle.main.url(forResource: selectedImage, withExtension: "svg")!
let _paths = SVGBezierPath.pathsFromSVG(at: svgURL)
for (index, path) in _paths.enumerated() {
let scaledBezier = ScaledBezier(bezierPath: path)
let shapeItem = ShapeItem(path: scaledBezier)
shapeItemsByKey[shapeItem.id] = shapeItem
shapeItemKeys.append(shapeItem.id)
}
}
}
The main view is composed of multiple views - one for image and one for color palette among others:
struct ColoringScreenView: View {
#ObservedObject var coloringImageViewModel : ColoringImageViewModel = ColoringImageViewModel(selectedImage: "tiger")
var body: some View {
VStack {
ColoringImageView(coloringImageViewModel: coloringImageViewModel)
ColoringImageButtonsView(coloringImageViewModel: coloringImageViewModel)
}
}
}
The ColoringImageButtonsView is supposed to modify the selected color depending on selected color:
import SwiftUI
struct ColoringImageButtonsView: View {
#ObservedObject var coloringImageViewModel : ColoringImageViewModel
var paletteColors: [PaletteColorItem] = [PaletteColorItem(color: .red), PaletteColorItem(color: .green), PaletteColorItem(color: .blue), PaletteColorItem(color: .yellow), PaletteColorItem(color: .purple), PaletteColorItem(color: .black), PaletteColorItem(color: .red), PaletteColorItem(color: .red), PaletteColorItem(color: .red)]
var body: some View {
HStack {
ForEach(paletteColors) { colorItem in
Button("blue", action: {
self.coloringImageViewModel.selectedColor = colorItem.color
print("Selected color: \(self.coloringImageViewModel.selectedColor)")
}).buttonStyle(ColorButtonStyle(color: colorItem.color))
}
}
}
}
struct ColorButtonStyle: ButtonStyle {
var color: Color
init(color: Color) {
self.color = color
}
func makeBody(configuration: Configuration) -> some View {
Circle()
.fill(color)
.frame(width: 40, height: 40, alignment: .top)
}
}
struct ColoringImageButtonsView_Previews: PreviewProvider {
static var previews: some View {
var coloringImageViewModel : ColoringImageViewModel = ColoringImageViewModel(selectedImage: "tiger")
ColoringImageButtonsView(coloringImageViewModel: coloringImageViewModel)
}
}
In ShapeView (subview of ImageView), it seels that coloringImageViewModel.selectedColor is always nil:
struct ShapeView: View {
var id: UUID
#Binding var coloringImageViewModel : ColoringImageViewModel
var body: some View {
ZStack {
var shapeItem = coloringImageViewModel.shapeItemsByKey[id]!
shapeItem.path
.fill(shapeItem.color)
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged { gesture in
print("Tap location: \(gesture.startLocation)")
guard let currentlySelectedColor = coloringImageViewModel.selectedColor else {return}
shapeItem.color = currentlySelectedColor
}
)
.allowsHitTesting(coloringImageViewModel.dragGestureMode == DragGestureEnum.FillAreas)
shapeItem.path.stroke(Color.black)
}
}
}
I have been reading about #Binding, #State and #ObservedObject but I haven't managed to use the property wrappers correctly in order to hold the states in a single instance of an object (ColoringImageViewModel) and modify/pass its values among multiple views. Does anyone know what is the right way to do so?
I made a Swift Playground with what I think is a simplified version of your problem. It shows how you can leverage #ObservedObject, #EnvironmentObject, #State and #Binding depending the context to achieve your goal.
If you run it you should see something like this:
Notice in the code below how the instance of ColoringImageViewModel is actually created outside of any views so that it does not get caught in the view's lifecycle.
Also check out the comments next to each piece of state data that explain the different usage scenarios.
import SwiftUI
import PlaygroundSupport
// Some global constants
let images = ["circle.fill", "triangle.fill", "square.fill"]
let colors: [Color] = [.red, .green, .blue]
/// Simplified model
class ColoringImageViewModel: ObservableObject {
#Published var selectedColor: Color?
// Use singleton pattern to manage instance outside view hierarchy
static let shared = ColoringImageViewModel()
}
/// Entry-point for the coloring tool
struct ColoringTool: View {
// We bring
#ObservedObject var model = ColoringImageViewModel.shared
var body: some View {
VStack {
ColorPalette(selection: $model.selectedColor)
// We pass a binding only to the color selection
CanvasDisplay()
.environmentObject(model)
// Inject model into CanvasDisplay's environment
Text("Tap on an image to color it!")
}
}
}
struct ColorPalette: View {
// Bindings are parameters that NEED to be modified
#Binding var selection: Color?
var body: some View {
HStack {
Text("Select a color:")
ForEach(colors, id: \.self) { color in
Rectangle()
.frame(width: 50, height: 50)
.foregroundColor(color)
.border(Color.white, width:
color == self.selection ? 3 : 0
)
.onTapGesture {
self.selection = color
}
}
}
}
}
/// Displays all images
struct CanvasDisplay: View {
// Environment objects are injected by some ancestor
#EnvironmentObject private var model: ColoringImageViewModel
var body: some View {
HStack {
ForEach(images, id: \.self) {
ImageDisplay(imageName: $0, selectedColor: self.model.selectedColor)
}
}
}
}
/// A single colored, tappable image
struct ImageDisplay: View {
let imageName: String // Constant parameter
let selectedColor: Color? // Constant parameter
#State private var imageColor: Color? // Internal variable state
var body: some View {
Image(systemName: imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(
imageColor == nil ? nil : imageColor!
)
.onTapGesture {
self.imageColor = self.selectedColor
}
}
}
PlaygroundPage.current.setLiveView(ColoringTool())
You don't show where you use ShapeView in ImageView, but taking into account logic of other provided code model should be ObservedObject
struct ShapeView: View {
var id: UUID
#ObservedObject var coloringImageViewModel : ColoringImageViewModel
// ... other code

How do I change the bool value of an item that comes from a struct and hence update a checklist?

Background
I am trying to build a list with a checkmark/tick box next to it. A struct is used to create the "data" for each item. This is then passed on to a class which holds an array of the items created by the struct. From here I used the observable object protocol and passed the class into a list.
Objective
I would like to be able to individually mark each item as completed when it is done.
Current Analysis
I know the image switches when I manually change the 'completed' value from false to true.
I also tested the onTapAction just to be sure it is working.
I think the problem lies in "self.one.completed.toggle()" or the binding or something I am unaware of.
struct One: Identifiable, Codable {
let id = UUID()
var item: String
var completed:Bool = false
}
class OneList: ObservableObject{
#Published var items1 = [One]()
struct ContentView: View {
#ObservedObject var itemss1 = OneList()
#ObservedObject var itemss2 = TwoList()
#ObservedObject var itemss3 = ThreeList()
#ObservedObject var itemss4 = FourList()
#State private var showingAdditem: Bool = false
#Binding var one:One
var body: some View {
NavigationView{
ZStack{
List{
Section(header: Text("Vital")){
ForEach(itemss1.items1){ item in
HStack{
Image(systemName: self.one.completed ? "checkmark.circle":"circle")
.onTapGesture {
self.one.completed.toggle()
}
Text(item.item)}
P.S. I am relatively new to Swift and Stack overflow so any other suggestions would be appreciated
In my other answer I achieved something like this with ObservableObject protocol for needed object and then playing with EnvironmentObject. Actually I didn't try to do this with other wrappers. Here is the code, where you can see switching images:
import SwiftUI
class One: Identifiable, ObservableObject { // ObservableObject requires class
let id: UUID
var item: String = "[undefined]"
#Published var completed: Bool = false // this will affect the UI
init(item: String, completed: Bool) {
id = UUID()
self.item = item
self.completed = completed
}
}
class OneList: ObservableObject{
#Published var items = [One(item: "first", completed: false),
One(item: "second", completed: false),
One(item: "third", completed: false)]
}
struct CheckboxList: View {
#EnvironmentObject var itemList: OneList
var body: some View {
List {
Section(header: Text("Vital")) {
ForEach(itemList.items.indices) { index in
VitalRow()
.environmentObject(self.itemList.items[index])
.onTapGesture {
self.itemList.items[index].completed.toggle()
}
}
}
}
}
}
struct VitalRow: View {
#EnvironmentObject var item: One
var body: some View {
HStack{
Image(systemName: item.completed ? "checkmark.circle" : "circle")
Text("\(item.item)")
}
}
}
struct CheckboxList_Previews: PreviewProvider {
static var previews: some View {
CheckboxList().environmentObject(OneList())
}
}