1

Ever since the advent of swiftUI 2.0, I have been unable to update a view according to a change done in another modally-presented view (the settings view).

I display a string on my main ContentView that derives its content from a segmented Picker value on the SettingsView. The problem is that after the user changes the setting and discards the SettingsView, the string in ContentView is not updated. The body is not redrawn.

I am making use of @ObservableObject and @StateObject so every change to it should trigger a redraw, but I can't make it work...

I created a class that conforms to the ObservableObject protocol : AppState I am using that class to try and pass data and -more importantly- data changes between the views in order to have my ContentView redrawn according to the the user's settings. In order to instantiate this class, I registered a single UserDefaults in my AppDelegate file.

I also imported the Combine Framework into my project and added the import Combine line in each and every file !

I've simplified my code as much as possible, in order to illustrate the issue, so the following might seem a bit circumvolutated, but it is derived from a much more complex app, sorry about that.

Here is my ContentView code :

import SwiftUI
import Combine


struct ContentView: View {
   
   @StateObject var appState: AppState
   
   @State var modalViewCaller = 0 // used to present correct modalView
   @State var modalIsPresented = false // to present the modal views
   
   var body: some View {
       
       let stringArray = generateString() // func to generate string according to user's pref
       let recapString = stringArray[0]
       
       return ZStack {
               NavigationView {
                   VStack {
 // MARK: -  texts :
                   VStack {
                   Text(recapString)
                       .bold()
                       .multilineTextAlignment(/*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)

                   } // end of VStack
                       .padding()
                       .overlay(RoundedRectangle(cornerRadius: 10)
                       .stroke(Color(UIColor.systemBlue), lineWidth: 4))
                       .padding()
                           } // END of VStack
           .onAppear() {
               self.modalViewCaller = 0
               print("\n\n*********** Content View onAppear triggered ! ************\n")
           }
           .navigationBarTitle("DataFun", displayMode: .inline)
           .navigationBarItems(leading: (
               Button(action: {
                   self.modalViewCaller = 1 // SettingsView
                   self.modalIsPresented = true
               }
                   ) {
                       Image(systemName: "gear")
                           .imageScale(.large)
                    }
           ))
       } // END of NavigationView
               .onAppear() {
                   self.appState.updateValues()
               }
       } // End of ZStack
       .sheet(isPresented: $modalIsPresented) {
           sheetContent(modalViewCaller: $modalViewCaller, appState: AppState())
               }
       .navigationViewStyle(StackNavigationViewStyle())
   }
   
   // MARK: - struct sheetContent() :
     
     struct sheetContent: View {
         @Binding var modalViewCaller: Int // Binding to the @State modalViewCaller variable from ContentView
         @StateObject var appState: AppState
         
         var body: some View {
           if modalViewCaller == 1 { // The settings view is called
               SettingsView(appState: AppState())
                     .navigationViewStyle(StackNavigationViewStyle())
                     .onDisappear { self.modalViewCaller = 0 }
             } else if modalViewCaller == 2 { // the "other view" is called
                     OtherView()
                     .navigationViewStyle(StackNavigationViewStyle())
                     .onDisappear { self.modalViewCaller = 0 }
             }
         }
     } // END of func sheetContent
   
   // MARK: - generateString()
 func generateString() -> [String] {
       
       var recapString = "" // The recap string
       var myArray = [""]
   
       // We create the recap string :
   if UserDefaults.standard.integer(forKey: "rules selection") == 0 { // ICAO
             recapString = "User chose LEFT"
   } else if UserDefaults.standard.integer(forKey: "rules selection") == 1 { // AF Rules
           recapString = "User chose RIGHT"
       }
   
   myArray = [recapString]
   
           return myArray
   } // End of func generateString()
}

struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
       ContentView(appState: AppState())
   }
}

Here is my AppState code :

import Foundation
import SwiftUI
import Combine

class AppState: ObservableObject {

   @Published var rulesSelection: Int = UserDefaults.standard.integer(forKey: "rules selection")
   
   func updateValues() { // When the user changes a setting, the UserDefault is updated. Here, we align the AppState's value with what is now in the UserDefaults
       self.rulesSelection = UserDefaults.standard.integer(forKey: "rules selection")
       print("\nappState value (ruleSelection) updated from Appstate class func \"updateValues")
   }
}

Here is my SettingsView code :

import SwiftUI
import Combine


struct SettingsView: View {
   
   @Environment(\.presentationMode) var presentationMode // in order to dismiss the Sheet

   @StateObject var appState: AppState
   @State private var rulesSelection = UserDefaults.standard.integer(forKey: "rules selection") // 0 is LEFT, 1 is RIGHT

   var body: some View {
       NavigationView {
           VStack {
               Spacer()
       Text("Choose a setting below")
           .padding()
       Picker("", selection: $rulesSelection) {
                   Text("LEFT").tag(0)
                   Text("RIGHT").tag(1)
               }
       .pickerStyle(SegmentedPickerStyle())
       .padding()
               Spacer()
           }
           .navigationBarItems(
               leading:
               Button("Done") {
                       self.saveDefaults() // We set the UserDefaults
                       self.presentationMode.wrappedValue.dismiss() // This dismisses the view
                //   self.modalViewCaller = 0
               }
           ) // END of NavBarItems
       } // END of NavigationBiew
   } // END of body
   
   func saveDefaults() {
       
       UserDefaults.standard.set(rulesSelection, forKey: "rules selection")
       
       self.appState.updateValues() // This is a func from the AppState class that will align the appState's value to the UserDefaults
       
   }
}

struct SettingsView_Previews: PreviewProvider {
   static var previews: some View {
       SettingsView(appState: AppState())
   }
}

And a working project if anyone has the time to check this "live" :

https://github.com/Esowes/dataFun

Thanks for any pointers.

Regards.

Esowes
  • 287
  • 2
  • 13

1 Answers1

1

Well... it was... in short many changes, so here is complete ContentView.swift with fixes.

Note: you need only one StateObject, and one instance set into it, and you need to have published property of observable object in view otherwise it is not refreshed, and changes in UserDefaults do not refresh view until you use AppStorage, etc.

Verified with Xcode 12.1 / iOS 14.1

import SwiftUI
import Combine


struct ContentView: View {
    
    @StateObject var appState: AppState
    
    @State var modalViewCaller = 0 // used to present correct modalView
    @State var modalIsPresented = false // to present the modal views
    
    var body: some View {
        
        return ZStack {
                NavigationView {
                    VStack {
  // MARK: -  texts :
                    VStack {
                            RecapStringView(appState: appState)

                    } // end of VStack
                        .padding()
                        .overlay(RoundedRectangle(cornerRadius: 10)
                        .stroke(Color(UIColor.systemBlue), lineWidth: 4))
                        .padding()
                            } // END of VStack
            .onAppear() {
                self.modalViewCaller = 0
                print("\n\n*********** Content View onAppear triggered ! ************\n")
            }
            .navigationBarTitle("DataFun", displayMode: .inline)
            .navigationBarItems(leading: (
                Button(action: {
                    self.modalViewCaller = 1 // SettingsView
                    self.modalIsPresented = true
                }
                    ) {
                        Image(systemName: "gear")
                            .imageScale(.large)
                     }
            ))
        } // END of NavigationView
                .onAppear() {
                    self.appState.updateValues()
                }
        } // End of ZStack
        .sheet(isPresented: $modalIsPresented) {
            sheetContent(modalViewCaller: $modalViewCaller, appState: appState)
                }
        .navigationViewStyle(StackNavigationViewStyle())
    }
    
    // MARK: - struct sheetContent() :
      
      struct sheetContent: View {
          @Binding var modalViewCaller: Int // Binding to the @State modalViewCaller variable from ContentView
          @ObservedObject var appState: AppState
          
          var body: some View {
            if modalViewCaller == 1 { // The settings view is called
                SettingsView(appState: appState)
                      .navigationViewStyle(StackNavigationViewStyle())
                      .onDisappear { self.modalViewCaller = 0 }
              } else if modalViewCaller == 2 { // the "other view" is called
                      OtherView()
                      .navigationViewStyle(StackNavigationViewStyle())
                      .onDisappear { self.modalViewCaller = 0 }
              }
          }
      } // END of func sheetContent
}

struct RecapStringView: View {
    @ObservedObject var appState: AppState
    var body: some View {
        Text("User chose " + "\(appState.rulesSelection == 0 ? "LEFT" : "RIGHT")")
        .bold()
        .multilineTextAlignment(.center)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(appState: AppState())
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690