Niko Gamulin
Niko Gamulin

Reputation: 66565

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?

Upvotes: 0

Views: 51

Answers (2)

Diego Lavalle
Diego Lavalle

Reputation: 46

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:

Playground in action

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())

Upvotes: 1

Asperi
Asperi

Reputation: 257711

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

Upvotes: 0

Related Questions