SwiftUI: Binded property does not change the views - swift

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.

Related

#AppStorage property wrapper prevents from dismissing views

I have an app with four (4) views, on the first view I'm showing a list of cars pulled from CoreData, the second view is presented when a car is tapped and it shows the services for each car. The third view is presented when tapping on a service, and it shows the details of the selected service. The fourth view is presented when tapping a button and it shows records for the specified service.
The issue I'm having is that for some reason if I use an #AppStorage property wrapper within the ServicesView I cannot dismiss the fourth view (RecordsView). I don't think the issue is with CoreData but let me know if you need to see the code for Core Data.
Any idea why adding an #AppStorage property wrapper in the ServicesView would affect other views?
CarsView
struct CarsView: View {
#ObservedObject var carViewModel:CarViewModel
#State private var carInfoIsPresented = false
var body: some View {
NavigationView{
VStack{
List {
ForEach(carViewModel.cars) { car in
HStack{
VStack(alignment:.leading){
Text(car.model ?? "")
.font(.title2)
Text(car.make ?? "")
.foregroundColor(Color(UIColor.systemGray))
}
NavigationLink(destination: ServicesView(carViewModel: carViewModel, selectedCar: car)){
Spacer()
Text("Services")
.frame(width: 55)
.font(.caption)
.foregroundColor(Color.systemGray)
}
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Cars")
.accentColor(.white)
.padding(.top, 20)
}
}
}
}
ServicesView
struct ServicesView: View {
#ObservedObject var carViewModel: CarViewModel
var selectedCar: Car
// ISSUE: No issues dismissing the RecordsView if I comment this out
#AppStorage("sortByNameKey") private var sortByName = true
#State private var selectedService: CarService?
var body: some View {
VStack{
List {
ForEach(carViewModel.carServices) { service in
HStack{
Text(service.name ?? "")
.font(.title3)
NavigationLink(destination: ServiceInfoView(carViewModel: carViewModel, selectedCar: selectedCar, selectedService: service)){
Spacer()
Text("Details")
.font(.caption)
.foregroundColor(Color.systemGray)
}
}
}
}
.navigationBarTitle(Text("\(selectedCar.model ?? "Services") - Services"))
.listStyle(GroupedListStyle())
}
.onAppear{
carViewModel.getServices(forCar: selectedCar)
}
}
}
ServiceInfoView
struct ServiceInfoView: View {
#ObservedObject var carViewModel: CarViewModel
#State private var recordsViewIsPresented = false
#State var selectedCar: Car
#State var selectedService: CarService
var body: some View {
VStack{
Text(selectedService.name ?? "")
.font(.largeTitle)
.padding(.bottom)
VStack{
Button(action: openRecordsView) {
Text("Service History")
}
.padding(10)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(15)
}
}
.sheet(isPresented: $recordsViewIsPresented){
RecordsView(carViewModel: carViewModel, selectedService: selectedService)
}
}
func openRecordsView(){
recordsViewIsPresented.toggle()
}
}
RecordsView
struct RecordsView: View {
#ObservedObject var carViewModel: CarViewModel
#State var selectedService: CarService
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack{
List {
Section(header: Text("Records")) {
ForEach(carViewModel.serviceRecords) { record in
HStack{
Text("Service Date:")
Text("\(record.serviceDate ?? Date(), style: .date)")
.foregroundColor(Color(UIColor.systemGray))
}
}
}
}
.background(Color.purple)
.listStyle(GroupedListStyle())
}
.navigationBarTitle("Records for \(selectedService.name ?? "")", displayMode: .inline)
.navigationBarItems(leading: Button("Cancel", action: dismissView))
.onAppear{
carViewModel.getRecords(forService: selectedService)
}
}
}
func dismissView(){
presentationMode.wrappedValue.dismiss()
}
}
NavigationView can only push one detail screen unless you set .isDetailLink(false) on the NavigationLink.
FYI we don't use view model objects in SwiftUI, you have to learn to use the View struct correctly along with #State, #Binding, #FetchRequest etc. that make the safe and efficient struct behave like an object. If you ignore this and use an object you'll experience the bugs that Swift with its value types was designed to prevent. For more info see this answer MVVM has no place in SwiftUI.

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

SwiftUI -> Thread 1: Fatal error: No ObservableObject of type ModelData found

I created an app with SwiftUI and when I try to display a button this error message appears:
Thread 1: Fatal error: No ObservableObject of type ModelData found. A View.environmentObject(_:) for ModelData may be missing as an ancestor of this view.
This occurs when I try to use an #EnvironmentObject when trying to display one of the views of my app.
My code is
struct OpportunityDetail: View {
#EnvironmentObject var modelData: ModelData
var opportunity: Opportunity
var opportunityIndex: Int {
modelData.opportunities.firstIndex(where: { $0.id == opportunity.id })!
}
var body: some View {
ScrollView {
MapView(coordinate: opportunity.locationCoordinate)
.frame(height: 300)
.ignoresSafeArea(edges: .top)
CircleImage(opportunity: opportunity)
.offset(y: -130)
.padding(.bottom, -130)
VStack {
VStack(alignment: .leading) {
Text(opportunity.position)
.font(.title)
HStack {
Text(opportunity.name)
.font(.subheadline)
Spacer()
Text(opportunity.city)
.font(.subheadline)
}
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
Text("About the Opportunity")
.font(.title2)
Text(opportunity.description)
ApplyButton(isSet: $modelData.opportunities[opportunityIndex].isApplied)
}
.padding()
}
}
.navigationTitle(opportunity.name)
.navigationBarTitleDisplayMode(.inline)
}
}
However, the preview and live preview of the View work fine
struct OpportunityDetail_Previews: PreviewProvider {
static let modelData = ModelData()
static var previews: some View {
OpportunityDetail(opportunity: modelData.opportunities[0])
.environmentObject(modelData)
}
}
I know I have to pass my environment object somewhere but don't know how to. I would greatly appreciate anyone's help.
My issue was solved by putting .environmentObject(modelData) into ContentView as it was the root view of all of my subviews as New Dev said:
You have to use .environmentObject(modelData), just like you did for a preview, somewhere in the view hierarchy. For example, the parent view of OpportunityDetail could do it, or you can create it at the root level. For example, if ContentView is your root view, you can set it there.

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.

How to modify List Data in SwiftUI

I want to change the value of a object associated to an item in a List when the user tap in that item and I get the next error "Cannot assign to property: 'student' is a 'let' constant". How do I prevente this to happened?
Here is my code:
import SwiftUI
var students: [Student] = load("studentsData.json")
struct StudentsListView: View {
#EnvironmentObject var viewRouter:ViewRouter
var body: some View {
NavigationView{
List(students){student in
Button(action: {
student.isInClass = true
}){
StudentItemListView(student: student)
}
}
}
}
}
struct StudentsListView_Previews: PreviewProvider {
static var previews: some View {
StudentsListView().environmentObject(ViewRouter())
}
}
struct StudentItemListView: View {
var student:Student
var body: some View {
HStack {
Image("profile")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width:50, height:50)
.clipShape(Circle())
VStack(alignment: .leading){
Text(student.firstName + " " + student.lastName)
.font(.headline)
if(student.isInClass == true){
Text("Asistió")
.font(.subheadline)
.foregroundColor(.green)
} else {
Text("Faltó")
.font(.subheadline)
.foregroundColor(.red)
}
}
Spacer()
}.padding()
}
}
As #koen mentioned in the comment, you should be using #State or #EnvironmentObject for accessing/mutating variables and state in your SwiftUI views.
Unlike UIKit, you might notice how SwiftUI relies on the heavy usage of structs. Structs are immutable by default, this means that you cannot modify any part of a struct without clearly telling the compiler that the struct is mutable (using var keyword, etc.) The way to manage state in SwiftUI is with the use of the Combine framework and the use of #State properties, etc. From a brief look at your code, it looks like all you need to do is add #State var student: Student to your StudentItemListView.
If this at all sounds confusing or you are unaware of what #State might be, I highly recommend either watching the 2019 WWDC videos and look at the Apple provided SwiftUI tutorial.