How to modify List Data in SwiftUI - swift

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.

Related

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.

#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.

Broken Animation SwiftUI

I initially had this question here. The solution proposed by #arsenius was working for this toy example. However, my application is more complex and it took me forever to find out where the animation breaks. In the example I used two animated HStack. But if I replace these HStack with two different (!) custom views, the animation is broken again.
Here is the code:
class State:ObservableObject{
#Published var showSolution = false
}
struct ContentView: View {
#EnvironmentObject var state:State
var body:some View {
VStack {
if state.showSolution{
CustomToggleOne()
.background(Color.red)
.id("one")
.animation(Animation.default)
.transition(.slide)
} else {
CustomToggleTwo()
.background(Color.yellow)
.id("two")
.animation(Animation.default.delay(2))
.transition(.slide)
}
}
}
}
struct CustomToggleOne: View{
#EnvironmentObject var state:State
var body:some View{
HStack{
Spacer()
Button(action:{
withAnimation {
self.state.showSolution.toggle()
}
}){
Text("Next")
}.padding()
Spacer()
}
}
}
struct CustomToggleTwo: View{
#EnvironmentObject var state:State
var body:some View{
HStack{
Spacer()
Button(action:{
withAnimation {
self.state.showSolution.toggle()
}
}){
Text("Next")
}.padding()
Spacer()
}
}
}
I added an instance of State to the ContentView in SceneDelegate.swift as an EnvironmentObject as follows:
let contentView = ContentView().environment(\.managedObjectContext, context).environmentObject(State())
The expected animation can be seen when we use CustomToggleOne() twice in the ContentView instead of CustomToggleTwo().
Here is the approach that is correct, some explanations below and in comments. Tested with Xcode 11.2 / iOS 13.2
Reminder: don't test transitions in Preview - only on Simulator or Device
// !! Don't name your types as API,
// State is SwiftUI type, might got unpredictable weird issues
class SolutionState:ObservableObject{
#Published var showSolution = false
}
struct TestBrokenAnimation: View {
#EnvironmentObject var state:SolutionState
var body:some View {
VStack {
if state.showSolution{
CustomToggleOne()
.background(Color.red)
.id("one")
.transition(.slide) // implicit animations confuse transition, don't use it
} else {
CustomToggleTwo()
.background(Color.yellow)
.id("two")
.transition(.slide)
}
}
}
}
public func withAnimation<Result>(_ animation: Animation? = .default,
_ body: () throws -> Result) rethrows -> Result
as it seen withAnimation is not state, which just activate animations defined via modifier, it explicitly applies specified animation, own, so having more in modifiers will definitely result in some conflicts.
So using only explicit animations with transitions.
struct CustomToggleOne: View{
#EnvironmentObject var state:SolutionState
var body:some View{
HStack{
Spacer()
Button(action:{
withAnimation(Animation.default.delay(2)) {
self.state.showSolution.toggle()
}
}){
Text("Next")
}.padding()
Spacer()
}
}
}
struct CustomToggleTwo: View{
#EnvironmentObject var state:SolutionState
var body:some View{
HStack{
Spacer()
Button(action:{
withAnimation() {
self.state.showSolution.toggle()
}
}){
Text("Next")
}.padding()
Spacer()
}
}
}