SwiftUI: Singleton class not updating view - swift

New to SwiftUI... I have the following simplified code. The intended functionality is to be able to navigate between View1() and View2(), using a singleton class to keep track of this navigation.
I suspect that maybe I needed to add #Published to my show_view_2 variable, but I want to keep it in App Storage. Also, I understand this isn't the best way to switch between views, but I only am using this method because it's a minimally reproduced example.
Why isn't the below code working, and how can I make it work?
class Player {
static var player = Player()
#AppStorage("show_view_2") var show_view_2: Bool = false
}
struct ContentView: View {
var body: some View {
if(Player.player.show_view_2) {
View2()
} else {
Text("Go to View2")
.onTapGesture {
Player.player.show_view_2 = true
}
}
}
}
struct View2: View {
var body: some View {
Text("Back to View1")
.onTapGesture {
Player.player.show_view_2 = false
}
}
}

For SwiftUI to know to update, Player should be an ObservableObject. Also, it'll need to be accessed using a property wrapper (either #StateObject or #ObservedObject) on the View:
class Player : ObservableObject {
static var player = Player()
#AppStorage("show_view_2") var show_view_2: Bool = false
}
struct ContentView: View {
#StateObject private var player = Player.player
var body: some View {
if player.show_view_2 {
View2()
} else {
Text("Go to View2")
.onTapGesture {
player.show_view_2 = true
}
}
}
}
struct View2: View {
#StateObject private var player = Player.player
var body: some View {
Text("Back to View1")
.onTapGesture {
player.show_view_2 = false
}
}
}
In general, I'd recommend not using a singleton and instead passing an instance of the object explicitly between views -- this will be more testable and flexible over time:
class Player : ObservableObject {
#AppStorage("show_view_2") var show_view_2: Bool = false
}
struct ContentView: View {
#StateObject private var player = Player()
var body: some View {
if player.show_view_2 {
View2(player: player)
} else {
Text("Go to View2")
.onTapGesture {
player.show_view_2 = true
}
}
}
}
struct View2: View {
#ObservedObject var player : Player
var body: some View {
Text("Back to View1")
.onTapGesture {
player.show_view_2 = false
}
}
}

Related

Show new view and completely destroy old view when clicking on Text

So I have been wanting to do this for some time, but I can't figure out how to approach this, so I'm reaching out to see if someone might be able to help me.
So let's say that I have the following code, which when the app loads, loads the "MainView":
struct MapGlider: App {
#ObservedObject var mainViewModel = MainViewModel()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(mainViewModel)
}
}
}
This loads the map as soon as the app is opened, which is great! All works great there.
Now I will be switching that out to show the OnboardingView() when the app loads such as:
struct MapGlider: App {
#ObservedObject var mainViewModel = MainViewModel()
var body: some Scene {
WindowGroup {
OnboardingView()
}
}
}
Now, I have a OnboardingView that shows a ZStack with some options, as show in this code below:
struct OnboardingView: View {
#State private var showGetStartedSheet = false
#ObservedObject var mainViewModel = MainViewModel()
var body: some View {
if #available(iOS 16.0, *) {
NavigationStack {
ZStack(alignment: .top) {
VStack {
LazyVGrid(columns: [GridItem(), GridItem(), GridItem(alignment: .topTrailing)], content: {
Spacer()
Image("onboarding-logo")
.border(.red)
NavigationLink(destination: MainView().environmentObject(mainViewModel), label: {
Text("Skip")
})
})
.border(.red)
}
}
.border(.blue)
}
} else {
// Fallback on earlier versions
}
}
}
Which outputs the following:
What I'm trying to achieve:
When someone clicks on the "Skip" text, to kill the OnboardingView and show the MainView().
The closest I got is setting a NavigationLink, but that had a back button and doesn't work so well, I want to be able to go to the MainView and not be able to go back to OnboardingView.
All help will be appreciated!
You could use a container view that conditionally displays the onboarding or main views, depending on the state of a variable (stored at the parent level). That variable can be passed down via a Binding:
Simplified example that should be easily applicable to your code:
class AppState: ObservableObject {
#Published var showOnboarding = true
}
#main
struct CustomCardViewApp: App {
#StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
MainScreenContainer(showOnboarding: $appState.showOnboarding)
}
}
}
struct MainScreenContainer: View {
#Binding var showOnboarding: Bool
var body: some View {
if showOnboarding {
OnboardingView(showOnboarding: $showOnboarding)
} else {
MainView()
}
}
}
struct OnboardingView: View {
#Binding var showOnboarding: Bool
var body: some View {
Text("Onboarding")
Button("Skip") {
showOnboarding = false
}
}
}
struct MainView: View {
var body: some View {
Text("Main")
}
}

Save onboarding in storage using Swift

So I need to utilize app storage to save the state of onboarding, but I can't seem to figure it out with a #Published variable which I have, so I wanted to reach out and see if anyone know what I can do to switch things up.
So here is the code:
class MainViewModel: ObservableObject {
#Published var showOnboarding = true
}
#main
struct MapGlider: App {
#StateObject private var model = MainViewModel()
var body: some Scene {
WindowGroup {
MainScreenContainer(
model: model,
showOnboarding: $model.showOnboarding
)
}
}
}
struct MainScreenContainer: View {
#ObservedObject var model: MainViewModel
#Binding var showOnboarding: Bool
var body: some View {
if showOnboarding {
OnboardingView(
model: model,
showOnboarding: $model.showOnboarding
)
} else {
MainView(model: model)
}
}
}
struct OnboardingView: View {
#Binding var showOnboarding: Bool
var body: some View {
NavigationStack {
ZStack(alignment: .top) {
VStack {
LazyVGrid(columns: [GridItem(), GridItem(), GridItem(alignment: .topTrailing)], content: {
Button(action: {
showOnboarding = false
}, label: {
Image(systemName: "xmark")
})
})
}
}
}
}
}
I want to be able to click on the button inside OnboardingView and then set the app storage of showOnboarding to false, so next time the app runs, it can check MainScreenContainer and go directly to MainView if the storage is set to false.
In your use case i would actually use #AppStorage property wrapper link which is a wrapper over UserDefaults, and i would get rid of that ViewModel. The code would look something like this:
struct MainScreenContainer: View {
#AppStorage("show_onboarding") var showOnboarding: Bool = false
var body: some View {
if showOnboarding {
OnboardingView(
model: model
)
} else {
MainView(model: model)
}
}
}
struct OnboardingView: View {
#AppStorage("show_onboarding") var showOnboarding: Bool = false
var body: some View {
NavigationStack {
ZStack(alignment: .top) {
VStack {
LazyVGrid(columns: [GridItem(), GridItem(), GridItem(alignment: .topTrailing)], content: {
Button(action: {
showOnboarding = false
}, label: {
Image(systemName: "xmark")
})
})
}
}
}
}
}

Swiftui connect command to value in main view

Does someone know how to connect commands to the rest of the project?
For example: I want to toggle the AddNew variable in the content view to show the add new item sheet by using the command.
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandGroup(after: CommandGroupPlacement.newItem) {
Button("Add new", action: {
self.AddNew.toggle() // should toggle variable in content View
})
}
}
}
}
struct ContentView: View {
#State var AddNew = false
var body: some View {
Button(action: {
self.AddNew.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $AddNew) {
AddNew(dimiss: $AddNew)
}
}
}
A solution could be to have a #Published var in a class conforming to ObservableObject.
You would toggle the boolean in the class and access it from wherever you want (as an #EnvironmentObject for example).
Like this:
class AppModel: ObservableObject {
#Published var addNew: Bool = false
}
struct SampleApp: App {
#ObservedObject var model = AppModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(model)
}
.commands {
CommandGroup(after: CommandGroupPlacement.newItem) {
Button("Add new", action: {
self.model.addNew.toggle()
})
}
}
}
}
struct ContentView: View {
#EnvironmentObject var model: AppModel
var body: some View {
Button(action: {
self.model.addNew.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $model.addNew) {
AddNew(dimiss: $model.addNew)
}
}
}

SwiftUI Conditional View Transitions are not working

Consider the following code:
class ApplicationHostingView: ObservableObject {
#Published var value: Bool
}
struct ApplicationHostingView: View {
// view model env obj
var body: some View {
Group {
if applicationHostingViewModel.value {
LoginView()
.transition(.move(edge: .leading)) // <<<< Transition for Login View
} else {
IntroView()
}
}
}
}
struct IntroView: View {
// view model env obj
var body: some View {
Button(action: { applicationHostingViewModel.value = true }) {
Text("Continue")
}
}
}
struct LoginView: View {
var body: some View {
Text("Hello World")
}
}
ISSUE
In this case, I see my transition from IntroView to LoginView work fine except for any of the animations. Animations inside IntroView based on the conditionals seem to be working fine but transitions that change the entire screen don't seem to work.
change group to ZStack
add animation somewhere.
class ApplicationHostingViewModel: ObservableObject {
#Published var value: Bool = false
}
struct ApplicationHostingView: View {
// view model env obj
#ObservedObject var applicationHostingViewModel : ApplicationHostingViewModel
var body: some View {
ZStack {
if applicationHostingViewModel.value {
LoginView()
.transition(.move(edge: .leading))
} else {
IntroView(applicationHostingViewModel:applicationHostingViewModel)
}
}
}
}
struct IntroView: View {
// view model env obj
#ObservedObject var applicationHostingViewModel : ApplicationHostingViewModel
var body: some View {
Button(action: {
withAnimation(.default){
self.applicationHostingViewModel.value = true} }) {
Text("Continue")
}
}
}
struct LoginView: View {
var body: some View {
Text("Hello World").frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

SwiftUI - Using 'ObservableObject' and #EnvironmentObject to Conditionally Display View

I would like to conditionally display different views in my application - if a certain boolean is true, one view will be displayed. If it is false, a different view will be displayed. This boolean is within an ObservableObject class, and is changed from one of the views that will be displayed.
PracticeStatus.swift (the parent view)
import Foundation
import SwiftUI
import Combine
class PracticeStatus: ObservableObject {
#Published var showResults:Bool = false
}
PracticeView.swift (the parent view)
struct PracticeView: View {
#EnvironmentObject var practiceStatus: PracticeStatus
var body: some View {
VStack {
if practiceStatus.showResults {
ResultView()
} else {
QuestionView().environmentObject(PracticeStatus())
}
}
}
}
QuestionView.swift
struct QuestionView: View {
#EnvironmentObject var practiceStatus: PracticeStatus
var body: some View {
VStack {
...
Button(action: {
self.practiceStatus.showResults = true
}) { ... }
...
}
}
}
However, this code doesn't work. When the button within QuestionView is pressed, ResultView is not displayed. Does anybody have a solution? Thanks!
Have you tried to compile your code? There are several basic errors:
Variable practice does not exist in PracticeView. Did you mean practiceStatus?
Variable userData does not exist in QuestionView. Did you mean practiceStatus?
You are calling PracticeView from inside PracticeView! You'll definetely get a stack overflow ;-) Didn't you mean QuestionView?
Below is a working code:
import Foundation
import SwiftUI
import Combine
class PracticeStatus: ObservableObject {
#Published var showResults:Bool = false
}
struct ContentView: View {
#State private var flag = false
var body: some View {
PracticeView().environmentObject(PracticeStatus())
}
}
struct PracticeView: View {
#EnvironmentObject var practiceStatus: PracticeStatus
var body: some View {
VStack {
if practiceStatus.showResults {
ResultView()
} else {
QuestionView()
}
}
}
}
struct QuestionView: View {
#EnvironmentObject var practiceStatus: PracticeStatus
var body: some View {
VStack {
Button(action: {
self.practiceStatus.showResults = true
}) {
Text("button")
}
}
}
}
struct ResultView: View {
#State private var flag = false
var body: some View {
Text("RESULTS")
}
}