SwiftUI binding a variable of another object is not working - swift

I want for my view model to have state to bind not in main view.
In the code below, I Binded view model's #GestureState variable to DragGesture in my main view.
But the problem is, without any compile error, it does not updating my #GestureState variable at all. If I define the #GestureState variable in my main view, it work.
Any idea?
class ViewModel {
#GestureState var dragOffset = CGSize.zero
}
struct ExampleView: View {
var viewModel = ViewModel()
var body: some View {
Image(systemName: "star.circle.fill")
.font(.system(size: 100))
.offset(x: viewModel.dragOffset.width, y: viewModel.dragOffset.height)
.animation(.easeInOut)
.foregroundColor(.green)
.gesture(
DragGesture()
.updating(viewModel.$dragOffset, body: { (value, state, transaction) in
print(state)
state = value.translation
print(state)
})
)
}
}

Related

Animate view every time an Observed property changes in SwiftUI

In the following code, an Image is animated when you tap on it by changing the #State property value. What I would like to be able to do is animate the image every time a #Published property value changes, this property is located inside an ObservableObject and is dynamically changing.
Using Local #State property wrappers, works fine.
struct ContentView: View {
#State private var scaleValue = 1.0
#State private var isAnimating: Bool = true
var body: some View {
Image(systemName: "circle.fill")
.scaleEffect(scaleValue)
.font(.title)
.foregroundColor(.green)
.animation(.easeOut(duration: 0.3), value: isAnimating)
.onTapGesture {
self.isAnimating.toggle()
self.scaleValue = 1.5
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3){
self.scaleValue = 1.0
}
}
}
}
Using Observed #Published property wrappers, not working. Not sure where to place the animation.
struct ContentView: View {
#State private var scaleValue = 1.0
#StateObject private var contentVM = ContentViewModel()
var body: some View {
Image(systemName: "circle.fill")
.scaleEffect(scaleValue)
.font(.title)
.foregroundColor(.green)
.animation(.easeOut(duration: 0.3), value: contentVM.isAnimating)
// not sure where to add the animation
}
}
EDIT: Here is the Working Solution:
Thanks to #Fogmeister and #burnsi
class ContentViewModel: ObservableObject{
#Published var isAnimatingImage = false
}
struct ContentView2: View {
#StateObject private var contentVM = ContentViewModel()
var body: some View {
#State private var scaleValue = 1.0
Image(systemName: "circle.fill")
.scaleEffect(scaleValue)
.font(.title)
.foregroundColor(.green)
.animation(.easeOut(duration: 0.3), value: scaleValue)
.onChange(of: contentVM.isAnimatingImage) {newValue in
animateImage()
}
}
func animateImage(){
scaleValue = 1.5
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3){
scaleValue = 1.0
}
}
}
I think the issue here is the wrong usage of the .animation modifier.
You don´t need to trigger the animation with a different boolean value. If the animation is "driven" by changing a value, in this case scaleValue, use that value in the modifier.
Take a look at your animation in your first example. It doesn´t complete. It scales to the desired size and then shrinks. But it jumps in the middle of the animation while shrinking back.
This would be a proper implementation in your first example:
struct ContentView: View {
#State private var scaleValue = 1.0
var body: some View {
Image(systemName: "circle.fill")
.scaleEffect(scaleValue)
.font(.title)
.foregroundColor(.green)
.animation(.easeOut(duration: 0.3), value: scaleValue)
.onTapGesture {
self.scaleValue = 1.5
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3){
self.scaleValue = 1.0
}
}
}
}
and in your second example:
class ContentViewModel: ObservableObject{
#Published var scaleValue = 1.0
}
struct ContentView2: View {
#StateObject private var contentVM = ContentViewModel()
var body: some View {
Image(systemName: "circle.fill")
.scaleEffect(contentVM.scaleValue)
.font(.title)
.foregroundColor(.green)
.animation(.easeOut(duration: 0.3), value: contentVM.scaleValue)
.onTapGesture {
contentVM.scaleValue = 1.5
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3){
contentVM.scaleValue = 1.0
}
}
}
}
This is why we shouldn't be using view model objects in SwiftUI and this is not what #StateObject is designed for - it's designed for when we need a reference type in an #State which is not the case here. If you want to group your view data vars together you can put them in a struct, e.g.
struct ContentViewConfig {
var isAnimating = false
var scaleValue = 1.0
// mutating func someLogic() {}
}
// then in the View struct:
#State var config = ContentViewConfig()

SwiftUI: Binded property does not change the views

I tried to bind the property to as isFavorite, somehow its value is changing on change but the view is not changing though.
#EnvironmentObject var modelData: ModelData
var landmark:Landmark
var landmarkIndex: Int {
modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView{
MapPreview(coordinate: landmark.locationCoordinate)
.ignoresSafeArea(edges: .top)
.frame(height: 300)
MapProfileImage(image: landmark.image)
.offset(y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading){
HStack{
Text(landmark.name)
.font(.largeTitle)
FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
}
HStack{
Text(landmark.park)
Spacer()
Text(landmark.state)
}
and its binded to a property isSet
struct FavoriteButton: View {
#Binding var isSet: Bool
var body: some View {
Button(action: {
print("isSet \(String(isSet))")
isSet.toggle()
}){
Image(systemName:isSet ? "star.fill" : "star")
.foregroundColor(.black)
}
}
}
Im new to SwitftUI, care to explain whats wrong pls
Usually #Binding is used when you want to bind a #State property in the parent view with another property in the child view.
In your case, you already have your view model in the environment, so just need to read the the environment again in the child view and change the variable directly there.
Here is how you could implement FavoriteButton:
struct FavoriteButton: View {
// Read the environment to get the view model
#EnvironmentObject var modelData: ModelData
// You will need to pass the index from the parent view
let index: Int
var body: some View {
Button(action: {
print("index \(index), isSet \(String(modelData.landmarks[index].isFavorite))")
// Change the view model directly
modelData.landmarks[index].isFavorite.toggle()
}){
Image(systemName: modelData.landmarks[index].isFavorite ? "star.fill" : "star")
.foregroundColor(.black)
}
}
}
In the parent view, call it passing the index:
FavoriteButton(index: landmarkIndex)
Needless to say, ModelData needs to be a class that conforms to ObservableObject and must already be in the environment when you call the parent view.

SwiftUI passing an observed object into a new view and getting updates

I am very new to swift working on my first app and having trouble having a view update. I am passing an object into a new view, however the new view does not update when there is change in the Firebase Database. Is there a way to get updates on the Gridview? I though by passing the observed object from the StyleboardView it would update the GridView however Gridview does not update. I am having trouble finding a way for the new Gridview to update and reload the images.
struct StyleBoardView: View {
#State private var showingSheet = false
#ObservedObject var model = ApiModel()
#State var styleboardname = ""
let userEmail = Auth.auth().currentUser?.email
var body: some View {
NavigationView {
VStack {
Text("Select Style Board")
List (model.list) {item in
Button(item.styleboardname) {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
GridView(item: item)
}
}
struct GridView: View {
var item: Todo
#ObservedObject var model = ApiModel()
#State var newImage = ""
#State var loc = ""
#State var shouldShowImagePicker = false
#State var image: UIImage?
var body: some View {
NavigationView {
var posts = item.styleboardimages
VStack(alignment: .leading){
Text(item.styleboardname)
GeometryReader{ geo in
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 3 ){
ForEach(posts.sorted(by: <), id: \.key) { key, value in
if #available(iOS 15.0, *) {
AsyncImage(url: URL(string: value), transaction: Transaction(animation: .spring())) { phase in
switch phase {
case .empty:
Color.purple.opacity(0.1)
case .success(let image):
image
.resizable()
.scaledToFill()
case .failure(_):
Image(systemName: "exclamationmark.icloud")
.resizable()
.scaledToFit()
#unknown default:
Image(systemName: "exclamationmark.icloud")
}
}
.frame(width: 100, height: 100)
.cornerRadius(20)
You have a few problems with the code. First of all, the original view that creates the view model, or has created for it originally, should own the object. Therefore you declare it as a #StateObject.
struct StyleBoardView: View {
#State private var showingSheet = false
#StateObject var model = ApiModel() // #StateObject here
#State var styleboardname = ""
let userEmail = Auth.auth().currentUser?.email
var body: some View {
NavigationView {
VStack {
Text("Select Style Board")
List ($model.list) { $item in // Change this to pass a Binding
Button(item.styleboardname) {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
GridView(item: $item, model: model)
}
}
}
}
}
}
Since you are passing to a .sheet, that will not automatically be re-rendered when StyleBoardView's model changes, so you have to use a #Binding to cause GridView to re-render. Lastly, once you have your #StateObject, you pass that to your next view. Otherwise, you continually make new models, so updates to one will not update the other.
struct GridView: View {
#Binding var item: Todo // Make this a #Binding so it reacts to the changes.
#ObservedObject var model: ApiModel // Pass the originally created view model in.
...
var body: some View {
NavigationView {
...
}
}
}
Lastly, you did not post a Minimal, Reproducible Example (MRE). You also did not post the complete GridView struct. You may not even need your view model in that view as you do not use it in what you have posted.
The problem is that you're initializing the model in an ObservedObject, and passing it down to another initialized Observed Object.
What you actually wanna do is use an #StateObject for where you initialize the model. And then use #ObservedObject with the type of the model you're passing down so that:
struct StyleBoardView: View {
#StateObject var model = ApiModel()
/** Code **/
struct GridView: View {
#ObservedObject var model: ApiModel
Notice the difference, an #ObservedObject should never initialize the model, it should only "inherit" (#ObservedObject var model: ApiModel) a model from a parent View, in this case, ApiModel.

Why does my SwiftUI view not get onChange updates from a #Binding member of a #StateObject?

Given the setup I've outlined below, I'm trying to determine why ChildView's .onChange(of: _) is not receiving updates.
import SwiftUI
struct SomeItem: Equatable {
var doubleValue: Double
}
struct ParentView: View {
#State
private var someItem = SomeItem(doubleValue: 45)
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { someItem.doubleValue += 10.0 }
.overlay { ChildView(someItem: $someItem) }
}
}
struct ChildView: View {
#StateObject
var viewModel: ViewModel
init(someItem: Binding<SomeItem>) {
_viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.someItem) { _ in
print("Change Detected", viewModel.someItem.doubleValue)
}
}
}
#MainActor
final class ViewModel: ObservableObject {
#Binding
var someItem: SomeItem
public init(someItem: Binding<SomeItem>) {
self._someItem = someItem
}
public func changeItem() {
self.someItem = SomeItem(doubleValue: .zero)
}
}
Interestingly, if I make the following changes in ChildView, I get the behavior I want.
Change #StateObject to #ObservedObject
Change _viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem)) to viewModel = ViewModel(someItem: someItem)
From what I understand, it is improper for ChildView's viewModel to be #ObservedObject because ChildView owns viewModel but #ObservedObject gives me the behavior I need whereas #StateObject does not.
Here are the differences I'm paying attention to:
When using #ObservedObject, I can tap the black area and see the changes applied to both the white text and red rectangle. I can also tap the red rectangle and see the changes observed in ParentView through the white text.
When using #StateObject, I can tap the black area and see the changes applied to both the white text and red rectangle. The problem lies in that I can tap the red rectangle here and see the changes reflected in ParentView but ChildView doesn't recognize the change (rotation does not change and "Change Detected" is not printed).
Is #ObservedObject actually correct since ViewModel contains a #Binding to a #State created in ParentView?
Normally, I would not write such a convoluted solution to a problem, but it sounds like from your comments on another answer there are certain architectural issues that you are required to conform to.
The general issue with your initial approach is that onChange is only going to run when the view has a render triggered. Generally, that happens because some a passed-in property has changed, #State has changed, or a publisher on an ObservableObject has changed. In this case, none of those are true -- you have a Binding on your ObservableObject, but nothing that triggers the view to re-render. If Bindings provided a publisher, it would be easy to hook into that value, but since they do not, it seems like the logical approach is to store the state in the parent view in a way in which we can watch a #Published value.
Again, this is not necessarily the route I would take, but hopefully it fits your requirements:
struct SomeItem: Equatable {
var doubleValue: Double
}
class Store : ObservableObject {
#Published var someItem = SomeItem(doubleValue: 45)
}
struct ParentView: View {
#StateObject private var store = Store()
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(store.someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { store.someItem.doubleValue += 10.0 }
.overlay { ChildView(store: store) }
}
}
struct ChildView: View {
#StateObject private var viewModel: ViewModel
init(store: Store) {
_viewModel = StateObject(wrappedValue: ViewModel(store: store))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.store.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.store.someItem.doubleValue) { _ in
print("Change Detected", viewModel.store.someItem.doubleValue)
}
}
}
#MainActor
final class ViewModel: ObservableObject {
var store: Store
var cancellable : AnyCancellable?
public init(store: Store) {
self.store = store
cancellable = store.$someItem.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
public func changeItem() {
store.someItem = SomeItem(doubleValue: .zero)
}
}
Actually we don't use view model objects at all in SwiftUI, see [Data Essentials in SwiftUI WWDC 2020]. As shown in the video at 4:33 create a custom struct to hold the item, e.g. ChildViewConfig and init it in an #State in the parent. Set the childViewConfig.item in a handler or add any mutating custom funcs. Pass the binding $childViewConfig or $childViewConfig.item to the to the child View if you need write access. It's all very simple if you stick to structs and value semantics.

Toggling #State variables using .OnTapGesture in SwiftUI

Can somebody tell me why this logic does not work? I am trying to create an instance of a view and store it in a variable. Then I use this variable to return a view in var body. My goal is to toggle the isActive variable of the view object on a tap so that the checkmark image is shown.
I can make this work when I put the onTapGesture inside the custom view object, but I can not get a change in state when I toggle the variable from parent view. I hope this makes sense.
struct SensorFamilyView: View {
#State var analogView = FamilyItemView(title: "Analog", isActive: false)
var body: some View {
VStack(alignment: .leading, spacing: 0) {
analogView // Show view instance
.onTapGesture { // I want this tap gesture to work
self.analogView.isActive.toggle()
}
}
}
}
struct FamilyItemView: View { // Custom View
#State var title: String
#State var isActive = false
var body: some View {
HStack {
if ( isActive ) // isActive toggles a checkmark image
{
Image(systemName: "checkmark.circle")
}
else
{
Image(systemName: "circle")
}
Text("\(title)")
}
.padding()
.onTapGesture { // This Tap works, but not what I want
//self.isActive.toggle()
}
}
}
Why won't it work?
You cannot hold an instance of FamilyItemView. Why? Because it is a struct, not a class. When you toggled the isActive property, the view is recreated (because it is using #State).
How can this be fixed?
Use #Binding. Creating a binding means that FamilyItemView will be updated when SensorFamilyView's isActive property changes. It can be used like the following:
struct SensorFamilyView: View {
#State private var isActive = false
var body: some View {
VStack(alignment: .leading, spacing: 0) {
FamilyItemView(title: "Analog", isActive: $isActive)
.onTapGesture {
self.isActive.toggle()
}
}
}
}
struct FamilyItemView: View {
#State var title: String
#Binding var isActive: Bool
var body: some View {
HStack {
if isActive {
Image(systemName: "checkmark.circle")
} else {
Image(systemName: "circle")
}
Text("\(title)")
}.padding()
}
}
Side note: As for the code right now, title does not need to be #State.
Additional clearing up of code
struct FamilyItemView: View {
let title: String
#Binding var isActive: Bool
var body: some View {
HStack {
Image(systemName: isActive ? "checkmark.circle" : "circle")
Text(title)
}.padding()
}
}
#State
To understand this, we need to first touch upon #State. What is it?
SwiftUI manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body.
...
A State instance isn’t the value itself; it’s a means of reading and writing the value. To access a state’s underlying value, use its variable name, which returns the wrappedValue property value.
Ref: https://developer.apple.com/documentation/swiftui/state
But why did we need #State? Well... Structs are value type and it's variables are non-mutating by default so to get around this, #State propertyWrapper was provided that basically wraps a value and stores and maintains it for us in some persitent* storage within the SwiftUI framework.
*See the WWDC on this for more details: https://developer.apple.com/videos/play/wwdc2019/226/
When a #State property is changed within the body of the View's struct in which it was declared, SwiftUI's engine automatically re-renders the body. But if it's modified from outside the view, SwiftUI does not pick up on this.
So then, now what?
That's where #Binding can be used to create a 2-way binding.
#Binding
Use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data. A binding connects a property to a source of truth stored elsewhere, instead of storing data directly. For example, a button that toggles between play and pause can create a binding to a property of its parent view using the #Binding property wrapper.
Ref: https://developer.apple.com/documentation/swiftui/binding
Solution:
struct SensorFamilyView: View {
#State var isActive: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 0) {
FamilyItemView(title: "Title", isActive: $isActive)
.onTapGesture {
self.isActive.toggle()
}
}
}
}
struct FamilyItemView: View {
#State var title: String
#Binding var isActive: Bool
var body: some View {
HStack {
if (isActive) {
Image(systemName: "checkmark.circle")
} else {
Image(systemName: "circle")
}
Text("\(title)")
}
}
}
SensorFamilyView has a state property isActive
FamilyItemView has a binding property isActive
There's a 2-way binding between them so when one changes, the other also changes. Furthermore, this is all within the Combine framework (which SwiftUI is heavily based on) and so the right sequence of event are fired that cause the body to render.