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

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

Related

SwiftUI polymorphic behaviour not working for View

protocol BackgroundContent: View{
}
struct BlueDivider: BackgroundContent {
var body: some View {
Divider()
.frame(minHeight: 1)
.background(.blue)
}
}
struct RedDivider: BackgroundContent {
var body: some View {
Divider()
.frame(minHeight: 1)
.background(.red)
}
}
var p: BackgroundContent = BlueDivider()
// Use of protocol 'BackgroundContent' as a type must be written 'any BackgroundContent'
p = RedDivider()
This always ask me to use
var p: any BackgroundContent = BlueDivider()
Is there any way to use generic type which accept any kind view?
Actually, I want to use view as a state like #State private var bgView: BackgroundContent = BlueDivider() which i want to change at runtime like bgView = RedDivider()
I have made my custome view to place some other view at runtime by using this state.
For your specific problem you can do something like this here:
struct SwiftUIView: View {
#State var isRed = false
var body: some View {
Devider()
.frame(height: 1)
.background(isRed ? Color.red : Color.blue)
}
}
It is complicated but i have found a solution of this problem. First thing i have done with ObservableObject. Here is my example.
protocol BaseBackgroundContent {
var color: Color { get set }
}
class BlueContent: BaseBackgroundContent {
var color: Color = .blue
}
class RedContent: BaseBackgroundContent {
var color: Color = .red
}
And i created a custom view for Divider in this case.
struct CustomDivider: View {
var backgroundContent: any BaseBackgroundContent
var body: some View {
Divider()
.background(backgroundContent.color)
}
}
And now i used a viewModel which can be observable, and the protocol has to be Published.
class ExampleViewModel: ObservableObject {
#Published var backgroundContent: any BaseBackgroundContent = RedContent()
func change() {
backgroundContent = BlueContent()
}
}
Final step is the view. This is a exampleView. If you click the button you will see the BlueContent which was RedContent
struct Example: View {
#ObservedObject var viewModel = ExampleViewModel()
init() {
}
var body: some View {
VStack {
Text("Test")
CustomDivider(backgroundContent: viewModel.backgroundContent)
Button("Change") {
viewModel.change()
}
}
}
}

Blinking symbol with didSet in SwiftUI

This is synthesized from a much larger app. I'm trying to blink an SF symbol in SwiftUI by activating a timer in a property's didSet. A print statement inside timer prints the expected value but the view doesn't update.
I'm using structs throughout my model data and am guessing this will have something to do with value vs. reference types. I'm trying to avoid converting from structs to classes.
import SwiftUI
import Combine
#main
struct TestBlinkApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class Model: ObservableObject {
#Published var items: [Item] = []
static var loadData: Model {
let model = Model()
model.items = [Item("Item1"), Item("Item2"), Item("Item3"), Item("Item4")]
return model
}
}
struct Item {
static let ledBlinkTimer: TimeInterval = 0.5
private let ledTimer = Timer.publish(every: ledBlinkTimer, tolerance: ledBlinkTimer * 0.1, on: .main, in: .default).autoconnect()
private var timerSubscription: AnyCancellable? = nil
var name: String
var isLEDon = false
var isLedBlinking = false {
didSet {
var result = self
print("in didSet: isLedBlinking: \(result.isLedBlinking) isLEDon: \(result.isLEDon)")
guard result.isLedBlinking else {
result.isLEDon = true
result.ledTimer.upstream.connect().cancel()
print("Cancelling timer.")
return
}
result.timerSubscription = result.ledTimer
.sink { _ in
result.isLEDon.toggle()
print("\(result.name) in ledTimer isLEDon: \(result.isLEDon)")
}
}
}
init(_ name: String) {
self.name = name
}
}
struct ContentView: View {
#StateObject var model = Model.loadData
let color = Color(UIColor.label)
public var body: some View {
VStack {
Text(model.items[0].name)
Image(systemName: model.items[0].isLEDon ? "circle.fill" : "circle")
.foregroundColor(model.items[0].isLEDon ? .green : color)
Button("Toggle") {
model.items[0].isLedBlinking.toggle()
}
}
.foregroundColor(color)
}
}
Touching the "Toggle" button starts the timer that's suppose to blink the circle. The print statement shows the value changing but the view doesn't update. Why??
You can use animation to make it blink, instead of a timer.
The model of Item gets simplified, you just need a boolean variable, like this:
struct Item {
var name: String
// Just a toggle: blink/ no blink
var isLedBlinking = false
init(_ name: String) {
self.name = name
}
}
The "hard work" is done by the view: changing the variable triggers or stops the blinking. The animation does the magic:
struct ContentView: View {
#StateObject var model = Model.loadData
let color = Color(UIColor.label)
public var body: some View {
VStack {
Text(model.items[0].name)
.padding()
// Change based on isLedBlinking
Image(systemName: model.items[0].isLedBlinking ? "circle.fill" : "circle")
.font(.largeTitle)
.foregroundColor(model.items[0].isLedBlinking ? .green : color)
// Animates the view based on isLedBlinking: when is blinking, blinks forever, otherwise does nothing
.animation(model.items[0].isLedBlinking ? .easeInOut.repeatForever() : .default, value: model.items[0].isLedBlinking)
.padding()
Button("Toggle: \(model.items[0].isLedBlinking ? "Blinking" : "Still")") {
model.items[0].isLedBlinking.toggle()
}
.padding()
}
.foregroundColor(color)
}
}
A different approach with a timer:
struct ContentView: View {
#StateObject var model = Model.loadData
let timer = Timer.publish(every: 0.25, tolerance: 0.1, on: .main, in: .common).autoconnect()
let color = Color(UIColor.label)
public var body: some View {
VStack {
Text(model.items[0].name)
if model.items[0].isLedBlinking {
Image(systemName: model.items[0].isLEDon ? "circle.fill" : "circle")
.onReceive(timer) { _ in
model.items[0].isLEDon.toggle()
}
.foregroundColor(model.items[0].isLEDon ? .green : color)
} else {
Image(systemName: model.items[0].isLEDon ? "circle.fill" : "circle")
.foregroundColor(model.items[0].isLEDon ? .green : color)
}
Button("Toggle: \(model.items[0].isLedBlinking ? "Blinking" : "Still")") {
model.items[0].isLedBlinking.toggle()
}
}
.foregroundColor(color)
}
}

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

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

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/

Animate view on property change SwiftUI

I have a view
struct CellView: View {
#Binding var color: Int
#State var padding : Length = 10
let colors = [Color.yellow, Color.red, Color.blue, Color.green]
var body: some View {
colors[color]
.cornerRadius(20)
.padding(padding)
.animation(.spring())
}
}
And I want it to have padding animation when property color changes. I want to animate padding from 10 to 0.
I've tried to use onAppear
...onAppear {
self.padding = 0
}
But it work only once when view appears(as intended), and I want to do this each time when property color changes. Basically, each time color property changes, I want to animate padding from 10 to 0. Could you please tell if there is a way to do this?
As you noticed in the other answer, you cannot update state from within body. You also cannot use didSet on a #Binding (at least as of Beta 4) the way you can with #State.
The best solution I could come up with was to use a BindableObject and sink/onReceive in order to update padding on each color change. I also needed to add a delay in order for the padding animation to finish.
class IndexBinding: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var index: Int = 0 {
didSet {
self.willChange.send()
}
}
}
struct ParentView: View {
#State var index = IndexBinding()
var body: some View {
CellView(index: self.index)
.gesture(TapGesture().onEnded { _ in
self.index.index += 1
})
}
}
struct CellView: View {
#ObjectBinding var index: IndexBinding
#State private var padding: CGFloat = 0.0
var body: some View {
Color.red
.cornerRadius(20.0)
.padding(self.padding + 20.0)
.animation(.spring())
.onReceive(self.index.willChange) {
self.padding = 10.0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
self.padding = 0.0
}
}
}
}
This example doesn't animate in the Xcode canvas on Beta 4. Run it on the simulator or a device.
As of Xcode 12, Swift 5
One way to achieve the desired outcome could be to move the currently selected index into an ObservableObject.
final class CellViewModel: ObservableObject {
#Published var index: Int
init(index: Int = 0) {
self.index = index
}
}
Your CellView can then react to this change in index using the .onReceive(_:) modifier; accessing the Publisher provided by the #Published property wrapper using the $ prefix.
You can then use the closure provided by this modifier to update the padding and animate the change.
struct CellView: View {
#ObservedObject var viewModel: CellViewModel
#State private var padding : CGFloat = 10
let colors: [Color] = [.yellow, .red, .blue, .green]
var body: some View {
colors[viewModel.index]
.cornerRadius(20)
.padding(padding)
.onReceive(viewModel.$index) { _ in
padding = 10
withAnimation(.spring()) {
padding = 0
}
}
}
}
And here's an example parent view for demonstration:
struct ParentView: View {
let viewModel: CellViewModel
var body: some View {
VStack {
CellView(viewModel: viewModel)
.frame(width: 200, height: 200)
HStack {
ForEach(0..<4) { i in
Button(action: { viewModel.index = i }) {
Text("\(i)")
.padding()
.frame(maxWidth: .infinity)
.background(Color(.secondarySystemFill))
}
}
}
}
}
}
Note that the Parent does not need its viewModel property to be #ObservedObject here.
You could use Computed Properties to get this working. The code below is an example how it could be done.
import SwiftUI
struct ColorChanges: View {
#State var color: Float = 0
var body: some View {
VStack {
Slider(value: $color, from: 0, through: 3, by: 1)
CellView(color: Int(color))
}
}
}
struct CellView: View {
var color: Int
#State var colorOld: Int = 0
var padding: CGFloat {
if color != colorOld {
colorOld = color
return 40
} else {
return 0
}
}
let colors = [Color.yellow, Color.red, Color.blue, Color.green]
var body: some View {
colors[color]
.cornerRadius(20)
.padding(padding)
.animation(.spring())
}
}
Whenever there is a single incremental change in the color property this will toggle the padding between 10 and 0
padding = color % 2 == 0 ? 10 : 0