Toggling #State variables using .OnTapGesture in SwiftUI - swift

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.

Related

Is there a way to solve the no-data when I click on the view for the first time? In Swift

I want to display the content of a Card with a Modal View that appears when I click on the Custom Card, but when I click only one card (not different card) , it displays a Model View with no data in it. After that, it works good for every card.
This is the code where the error is:
import SwiftUI
struct GoalsView: View {
#Binding var rootActive: Bool
#State private var showSheet = false
#Binding var addGoalIsClicked: Bool
#State private var selectedGoal : Goal = Goal(title: "", description: "")
var body: some View {
ScrollView {
VStack(){
HStack{
Text("Add goals")
.fontWeight(.bold)
.font(.system(size: 28))
Spacer()
}
.padding()
ForEach(goalDB) {goal in
Button() {
selectedGoal = goal
showSheet = true
}label: {
Label(goal.title, systemImage: "")
.frame(maxWidth: .infinity)
.foregroundColor(.black)
}
.padding()
.fullScreenCover(isPresented: $showSheet) {
ModalView(rootActive: $rootActive, goal: selectedGoal)
}
.background {
RoundedRectangle(cornerRadius: 10)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
}
}
.padding(4)
}
}.background(Color.gray.opacity(0.15))
}
}
I tried to find a solution on the Internet (someone has my same problem), reading the documentation of the FullScreenCover but I can not find out how to do it
Instead of using a Boolean to control whether or not to show a sheet and/or full screen cover (I'm going to use "sheet" throughout, the principle is identical for both), you can instead use an optional object.
In this scenario, you start with an optional value that is nil. When you want your sheet to display, you set the object to an actual value. Dismissing the sheet will return your state object to nil, just as dismissing a sheet managed by a Boolean resets the state to false.
In your code, you can remove your showSheet state value, and convert selectedGoal to an optional:
#State private var selectedGoal: Goal?
Your button action now only needs to set this value:
Button {
selectedGoal = goal
} label: {
// ...
}
And then in your modifier, you use the item: form instead. The closure receives the unwrapped version of the state object – i.e., it'll always get a Goal, rather than a Goal?. This is the value you should pass into the view being presented:
.fullScreenCover(item: $selectedGoal) { goal in
ModalView(rootActive: $rootActive, goal: goal)
}

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.

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.

Swift: How to modify struct property outside of the struct's scope

Programming in Swift/SwiftUI, and came across this problem when trying to enable a view to modify properties of a different struct.
Is there a way to modify a property, belonging to a struct, without creating an object for the struct? If so, what is it?
Right now, you're trying to access showOverlap as if it is a static variable on MainView -- this won't work since it is not a static property and even if it were, you would need a reference to the specific instance of MainView you were showing -- something that in SwiftUI we generally avoid since Views are transitive.
Instead, you can pass a Binding -- this is one of the ways of passing state for parent to child views in SwiftUI.
struct MainView: View {
#State var showOverlap = false
var body: some View {
ZStack {
Button(action: {
showOverlap = true
}) {
Text("Button")
}
if showOverlap {
Overlap(showOverlap: $showOverlap) //<-- Here
}
}
}
}
struct Overlap: View {
#Binding var showOverlap : Bool //<-- Here
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 40)
.aspectRatio(130/200, contentMode: .fit)
.foregroundColor(.gray)
Button(action: {
showOverlap = false //<-- Here
}, label: {
Text("Back")
})
}
}
}

Modifying a #State var from a #Binding var isn't refreshing the view in SwiftUI

So I have a ParentView which contains a FilterBar and a List. It looks something like this:
struct ParentView: View {
#State var listCellModels: [ListCellModel]
// Both these vars are passed to the FilterBar and adjusted by the FilterBar
#State var isEditing: Bool = false
#State var selectedType: FilterType = .none
// When selected type is changed, I need to reload the models in the list with the new filter
private var filteredModels: [ListCellModel] {
return listCellModels.filter{
(selectedType.rawValue == 0 || $0.approved.rawValue == selectedType.rawValue)
}
}
var body: some View {
VStack {
FilterBar(isEditing: $isEditing, selectedType: $selectedType)
// All the items in the list are buttons that display a custom view I had
// this works fine, and when isEditing is changed the view DOES update
List(filteredModels) { model in
Button(action: {
// Does a thing
}, label: {
ListViewCell(model: model, isEditing: self.$isEditing)
})
}
}
}
}
My Filter bar is just a simple HStack with a couple buttons that modify the variables
struct FilterBar: View {
#Binding var isEditing: Bool
#Binding var selectedType: FilterType
var body: some View {
HStack(alignment: .center) {
Button(action: {
self.selectedType = FilterType.init(rawValue: (self.selectedType.rawValue + 1) % 4)!
}, label: {
Text("Filter: \(selectedType.name)")
}).padding(.top).padding(.leading)
Spacer()
Button(action: {
self.isEditing = !self.isEditing
}, label: {
Text(!isEditing ? "Edit" : "Done")
}).padding(.top).padding(.trailing)
}
}
}
When I tap the button that changes isEditing, all of the cells in the list update to show their "Editing" states, but when i tap the button to change selectedType, the variable in the parent view does get updated, as I've observed in the debugger - however the view does not reload. So it appears as if the old filter is still being applied.
Is there any reason why updating this #State var is not reloading the view?
Are there any workarounds?
Well, it is like... workaround... but for testing, try
FilterBar(isEditing: $isEditing, selectedType: $selectedType)
if selectedType != .none {
EmptyView()
}
In general, it would be correct to introduce view model as ObservableObject and have filteredModels in it as #Published, so your FilterBar changed that property, which will automatically refreshed the ParentView.