Why update variable in observable object does not update the view? - ios

I am new to SwiftUI. Learning new properties such as #State, #Binding, #EnvironmentObject etc.
I am currently working on the login template, define a binding #Published variable in observable object which allows to switch between login page and main page. However, when I update the variable inside the observable object, the main page does not show up. It is still in the login page. What is missing in my code?
struct ContentView: View {
#EnvironmentObject var loginViewModel: LoginViewModel
var body: some View {
return Group {
if loginViewModel.signInSuccess {
MainPageView()
}
else {
LoginView(signInSuccess: $loginViewModel.signInSuccess).environmentObject(LoginViewModel())
}
}
}
}
final class LoginViewModel: ObservableObject {
#Published var signInSuccess:Bool = false;
func performLogin() {
signInSuccess = true;
}
}
struct LoginView: View {
#EnvironmentObject var loginViewModel: LoginViewModel
#Binding var signInSuccess: Bool;
var body: some View {
Button(action: submit) {
Text("Login")
.foregroundColor(Color.white)
}
}
func submit() {
loginViewModel.performLogin()
// signInSuccess = true;
}
}
If I tried to update binding 'signInSuccess' in the loginView, it can successfully update the view to the mainView. However, is there a way that I can update signInSuccess inside the Observable Object that also update the ContentView to the MainView?

Yes, you just change the view code as the following.
The binding actually is not necessary.
struct ContentView: View {
#EnvironmentObject var loginViewModel: LoginViewModel
var body: some View {
return Group {
if loginViewModel.signInSuccess {
MainPageView()()
}
else {
LoginView(signInSuccess: $loginViewModel.signInSuccess).environmentObject(self.loginViewModel)
}
}
}
}
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(LoginViewModel())

If you want to use an environmental variable, you have to declare it in your SceneDelegate and set it on the ContontentView:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var loginViewModel = LoginViewModel()
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = HostingViewController(rootView: contentView.environmentObject(loginViewModel))
self.window = window
window.makeKeyAndVisible()
}
}
// etc.
Then in your ContenView you don't need to set it in any way on the LoginView as it is held by the enviroment:
struct ContentView: View {
#EnvironmentObject var loginViewModel: LoginViewModel
var body: some View {
return Group {
if loginViewModel.signInSuccess {
MainPageView()
} else {
LoginView()
}
}
}
}
In your model, make sure you declare your signInSuccess as private(set) so it can only be set from within the class and only read from elsewhere:
final class LoginViewModel: ObservableObject {
#Published private(set) var signInSuccess:Bool = false;
func performLogin() {
signInSuccess = true;
}
}
And finally in the LoginView you just need to include the #EnvironmentObject and everything else will work.
struct LoginView: View {
#EnvironmentObject var loginViewModel: LoginViewModel
var body: some View {
Button(action: { self.loginViewModel.performLogin() }) {
Text("Login")
.foregroundColor(Color.white)
}
}
}

Related

SwiftUI, iOS: How to access public function inside #main struct from custom internal View?

How to access achieveTo() inside #main struct from CustomInternalView()?
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
CustomInternalView()
}
}
public func achieveTo() {
// Do stuff
}
}
struct CustomInternalView: View {
var body: some View {
Text("Some")
}
.onDisappear {
// How to call "achieveTo" from here????????????
}
}
In UIKit it is like UIApplication.shared.windows.first!.rootViewController as! YourViewController, How about SwiftUI?
You can add a closure:
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
CustomInternalView {
achieveTo() /// 2. Assign closure
}
}
}
public func achieveTo() {
// Do stuff
}
}
struct CustomInternalView: View {
var callParentTask: (() -> Void) /// 1. Define closure
var body: some View {
Text("Some")
.onDisappear(perform: callParentTask) /// 3. Call closure
}
}
Note: Your onDisappear must be attached to a View inside the var body: some View {, not outside.
Another simpler version of what #aheze answered:
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
CustomInternalView()
.onDisappear() { achieveTo() }
}
}
}
func achieveTo() {
// Do stuff
}
struct CustomInternalView: View {
var body: some View {
Text("Some")
}
}

How to get value on original object with EnvironmentObject in swiftUI

class GameSettings: ObservableObject {
#Published var score = 0
#Published var score1:Int? = 0
}
struct ScoreView: View {
#EnvironmentObject var settings: GameSettings
var body: some View {
NavigationView {
NavigationLink(destination: ContentView3()) {
Text("Score: \(settings.score)")
}
}
}
}
struct ContentView3: View {
#StateObject var settings = GameSettings()
#EnvironmentObject var settings111: GameSettings
var body: some View {
NavigationView {
VStack {
// A button that writes to the environment settings
Text("Current Score--->\(settings.score))")
Text(settings111.score1 == nil ? "nil" : "\(settings111.score1!)")
Button("Increase Score") {
settings.score += 1
}
NavigationLink(destination: ScoreView()) {
Text("Show Detail View")
}
}
.frame(height: 200)
}
.environmentObject(settings)
}
}
So here When user has performed some changes in ContentView3 & from navigation route if user lands to same screen i.e. ContentView3 so how can I get GameSettings object latest value on it ? I tried creating #EnvironmentObject var settings111: GameSettings but crashes.
Did you add .environmentObject() to your YourApp.swift as well?
If not, you have to add it like this
Life Cycle: SwiftUI
#main
struct YourApp: App {
var settings: GameSettings = GameSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings)
}
}
}
Life Cycle: UIKit
In SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
var settings: GameSettings = GameSettings() // added line
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settings))) // added ".environmentObject(settings)" after contentView
self.window = window
window.makeKeyAndVisible()
}
}
Preview
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(GameSettings())
}
}

How to create a singleton that I can update in SwiftUI

I want to create a global variable for showing a loadingView, I tried lots of different ways but could not figure out how to. I need to be able to access this variable across the entire application and update the MotherView file when I change the boolean for the singleton.
struct MotherView: View {
#StateObject var viewRouter = ViewRouter()
var body: some View {
if isLoading { //isLoading needs to be on a singleton instance
Loading()
}
switch viewRouter.currentPage {
case .page1:
ContentView()
case .page2:
PostList()
}
}
}
struct MotherView_Previews: PreviewProvider {
static var previews: some View {
MotherView(viewRouter: ViewRouter())
}
}
I have tried the below singleton but it does not let me update the shared instance? How do I update a singleton instance?
struct LoadingSingleton {
static let shared = LoadingSingleton()
var isLoading = false
private init() { }
}
Make your singleton a ObservableObject with #Published properties:
struct ContentView: View {
#StateObject var loading = LoadingSingleton.shared
var body: some View {
if loading.isLoading {
Text("Loading...")
}
ChildView()
Button(action: { loading.isLoading.toggle() }) {
Text("Toggle loading")
}
}
}
struct ChildView : View {
#StateObject var loading = LoadingSingleton.shared
var body: some View {
if loading.isLoading {
Text("Child is loading")
}
}
}
class LoadingSingleton : ObservableObject {
static let shared = LoadingSingleton()
#Published var isLoading = false
private init() { }
}
I should mention that in SwiftUI, it's common to use .environmentObject to pass a dependency through the view hierarchy rather than using a singleton -- it might be worth looking into.
First, make LoadingSingleton a class that adheres to the ObservableObject protocol. Use the #Published property wrapper on isLoading so that your SwiftUI views update when it's changed.
class LoadingSingleton: ObservableObject {
#Published var isLoading = false
}
Then, put LoadingSingleton in your SceneDelegate and hook it into your SwiftUI views via environmentObject():
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
static let singleton = LoadingSingleton()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(SceneDelegate.singleton))
self.window = window
window.makeKeyAndVisible()
}
}
}
To enable your SwiftUI views to update when changing isLoading, declare a variable in the view's struct, like this:
struct MyView: View {
#EnvironmentObject var singleton: LoadingSingleton
var body: some View {
//Do something with singleton.isLoading
}
}
When you want to change the value of isLoading, just access it via SceneDelegate.singleton.isLoading, or, inside a SwiftUI view, via singleton.isLoading.

Change SwiftUI Text from UIViewController

I'm quite new to Swift and absolute new to SwiftUI.
I'm trying to combine UIViewController Google Maps and modern SwiftUI.
In SwiftUI i have a few Text objects, and I want my GmapsController class to be able to modify these Text values, and redraw SwiftUI struct.
My main struct:
var _swiftUIStruct : swiftUIStruct = swiftUIStruct() // to have access in GmapsController
struct GoogMapView: View {
var body: some View {
let gmap = GmapsControllerRepresentable()
return ZStack {
gmap
_swiftUIStruct
}
}
}
UIViewControllerRepresentable wrapper of GmapsController :
struct GmapsControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<GmapsControllerRepresentable>) -> GmapsController {
return GmapsController()
}
func updateUIViewController(_ uiViewController: GmapsController, context: UIViewControllerRepresentableContext<GmapsControllerRepresentable>) {}
}
The GmapsController itself:
class GmapsController: UIViewController, CLLocationManagerDelegate, GMSMapViewDelegate {
var locationManager = CLLocationManager()
var mapView: GMSMapView!
override func viewDidLoad() {
super.viewDidLoad()
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
let camera = GMSCameraPosition.camera(
withLatitude: (locationManager.location?.coordinate.latitude)!,
longitude: (locationManager.location?.coordinate.longitude)!,
zoom: 15
)
mapView = GMSMapView.map(withFrame: view.bounds, camera: camera)
mapView.delegate = self
self.view = mapView
}
// HERE I WANT TO CHANGE SOME swiftUIStruct Text FIELDS
// calls after map move
func mapView(_ mapView: GMSMapView, idleAt сameraPosition: GMSCameraPosition) {
_swiftUIStruct.someTxt = "I hope this will work" // compilation pass, but value doesn't change
}
}
And the swiftUIStruct struct:
struct swiftUIStruct {
#State var someTxt = ""
var body: some View {
Text(someTxt) // THE TEXT I WISH I COULD CHANGE
}
}
Googling this a whole day just made me feel dumb, any help is appreciated.
I hope my example code helps. Basically, move the model data outside, and pass it along, and change it. If you run this code, you will see the text "I hope this will work", NOT "Initial Content".
import SwiftUI
class ViewModel: ObservableObject {
#Published var someTxt = "Initial Content"
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
ZStack {
GoogleMapsView(viewModel: viewModel)
Text(viewModel.someTxt)
}
}
}
struct GoogleMapsView: UIViewControllerRepresentable {
var viewModel: ViewModel
func makeUIViewController(context: Context) -> GmapsController {
let controller = GmapsController()
controller.viewModel = viewModel
return controller
}
func updateUIViewController(_ uiViewController: GmapsController, context: Context) {}
}
class GmapsController: UIViewController {
var viewModel: ViewModel?
override func viewDidLoad() {
viewModel?.someTxt = "I hope this will work"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
}
}

SwiftUI View stops updating when application goes to background

I'm using the Spotify APK to authorize a connection to the Spotify app. All communication with Spotify is done via the Scene Delegate. My issue is that when I call for authorization and I'm taken to and from the Spotify app, the current view seems to stop updating with #Published variable changes. However, I want the view to change upon successful authorization/connection.
I've tried having the MainView update with different changes to different variables, but it seems that no matter what I do, the view stops updating with changes to published variables once the app leaves and reenters the foreground.
SceneDelegate:
class SceneDelegate: UIResponder, UIWindowSceneDelegate, SPTAppRemoteDelegate, SPTAppRemotePlayerStateDelegate {
#ObservedObject var MainVM = MainViewModel()
func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) {
MainVM.viewSwitch = false
}
}
MainViewModel:
class MainViewModel: ObservableObject {
#Published var viewSwitch: Bool = true
var appRemote: SPTAppRemote {
get {
let scene = UIApplication.shared.connectedScenes.first
let sd : SceneDelegate = (scene?.delegate as? SceneDelegate)!
return sd.appRemote
}
}
func connectAppRemote() {
appRemote.authorizeAndPlayURI("")
}
}
MainView:
struct MainView: View {
#ObservedObject var MainVM = MainViewModel()
var body: some View {
if MainVM.viewSwitch {
Text("View 1 Displayed")
} else {
Text("View 2 Displayed")
}
}
.onAppear {
MainVM.connectAppRemote()
}
}
You work with different objects:
A. SceneDelegate has own instance (btw, here you don't need ObservedObject)
class SceneDelegate: UIResponder, UIWindowSceneDelegate,
SPTAppRemoteDelegate, SPTAppRemotePlayerStateDelegate {
#ObservedObject var MainVM = MainViewModel()
and
B. MainView has own
struct MainView: View {
#ObservedObject var MainVM = MainViewModel() // << recreated
You need to pass that one in SceneDelegate as environmentObject in MainView, like
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
let contentView = MainView().environmentObject(MainVM)
and declare it correspondingly
struct MainView: View {
#EnvironmentObject var MainVM: MainViewModel

Resources