How to implement singleton #ObservedObject in SwiftUI [duplicate] - 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.

Related

How to establish communication between ViewModels of the same screen in SwiftUI MVVM

In my app, I have a Screen with Toolbar and Main View.
VStack {
ToolbarView()
MainView()
}
Think of it like this:
Toolbar has its own View and ToolbarViewModel where we can select “Tools”
struct ToolbarView: View {
#StateObject private var VM = ToolbarViewModel()
var body: some View {
Text("Toolbar View")
}
}
#MainActor final class ToolbarViewModel: ObservableObject {
var selectedTool: Int = 1
func selectTool() {
//We select a new tool
selectedTool = 2
}
}
Main view has its own View and MainViewModel
struct MainView: View {
#StateObject private var VM = MainViewModel()
var body: some View {
Text("Main View")
}
}
#MainActor final class MainViewModel: ObservableObject {
var selectedTool: Int = 1
}
Now, when I tap a button in the ToolbarView and call a function in ToolbarViewModel to select a new tool, the tool must change in the MainViewModel too.
What would be the correct way of implementing this?
In the screen with the MainView and ToolbarView instances, create #StateObject(s) for both
#StateObject private var mainVM = MainViewModel()
#StateObject private var toolbarVM = ToolbarViewModel()
Then, create Observed objects in both your instances:
ToolbarView:
struct ToolbarView: View {
#ObservedObject var VM: ToolbarViewModel
var body: some View {
Text("Toolbar View")
}
}
MainView:
struct MainView: View {
#ObservedObject var VM: MainViewModel
var body: some View {
Text("Main View")
}
}
Then pass your objects in the screen than you created your instances:
VStack {
ToolbarView(VM: toolbarVM)
MainView(VM: mainVM)
}
Finally, whenever you make a change you can just listen to it like:
VStack {
ToolbarView(VM: toolbarVM)
MainView(VM: mainVM)
}
.onChange(of: toolbarVM.isDrawing) { newValue in {
mainVM.isDrawing = newValue
}
.onChange(of: mainVM.isDrawing) { newValue in {
toolbarVM.isDrawing = newValue
}
We don't use view model objects in SwiftUI. The View data struct is already the view model that SwiftUI uses to create/update/remove UIView objects automatically for us. The property wrappers give the struct reference semantics giving us the best of both worlds. You'll have to learn #State and #Binding and put the shared state in a parent View, then pass it down as a let for read access or #Bindng var for write access, e.g.
#State var tools = Tools()
...
VStack {
ToolbarView(tools: $tools)
MainView(tools: tools)
}
struct Tools {
var selectedTool: Int = 1
mutating func selectTool() {
//We select a new tool
selectedTool = 2
}
}
struct MainView: View {
let tools: Tools
var body: some View {
Text("Main View \(tools.selected)")
}
}
struct ToolbarView: View {
#Binding var tools: Tools
var body: some View {
Text("Toolbar View")
Button("Select") {
tools.selectTool()
}
}
}

Create a Publishable property in a view controller, pass it to a SwiftUI View and listen to changes inside view controller?

This is what I am trying to achieve:
class MyVC: UIViewController {
#State var myBoolState: Bool = false
private var subscribers = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
myBoolState.sink { value in .... }.store(in:&subscribers)
}
func createTheView() {
let vc = UIHostingController(rootView: MySwiftUIView(myBoolState: $myBoolState))
self.navigationController!.pushViewController(vc, animated: true)
}
}
struct MySwiftUIView: View {
#Binding var myBoolState: Bool
var body: some View {
Button(action: {
myBoolState = true
}) {
Text("Push Me")
}
}
}
But the above of course does not compile.
So the question is: can I somehow declare a published property inside a view controller, pass it to a SwiftUI View and get notified when the SwiftUI view changes its value?
The #State wrapper works (by design) only inside SwiftUI view, so you cannot use it in view controller. Instead there is ObsevableObject/ObservedObject pattern for such purpose because it is based on reference types.
Here is a demo of possible solution for your scenario:
import Combine
class ViewModel: ObservableObject {
#Published var myBoolState: Bool = false
}
class MyVC: UIViewController {
let vm = ViewModel()
private var subscribers = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
vm.$myBoolState.sink { value in
print(">> here it goes")
}.store(in:&subscribers)
}
func createTheView() {
let vc = UIHostingController(rootView: MySwiftUIView(vm: self.vm))
self.navigationController!.pushViewController(vc, animated: true)
}
}
struct MySwiftUIView: View {
#ObservedObject var vm: ViewModel
var body: some View {
Button(action: {
vm.myBoolState = true
}) {
Text("Push Me")
}
}
}

SwiftUI - Using #main to set the rootViewController

When using the SceneDelegate in SwiftUI, it was possible to create a function like the one below that could be used to set the view as shown here. However, in the latest version we now use a WindowsGroup. Is it possible to write a function that changes the view in the WindowsGroup?
func toContentView() {
let contentView = ContentView()
window?.rootViewController = UIHostingController(rootView: contentView)
}
Here is possible alternate approach that do actually the same as your old toContentView
helper class
class Resetter: ObservableObject {
static let shared = Resetter()
#Published private(set) var contentID = UUID()
func toContentView() {
contentID = UUID()
}
}
content of #main
#StateObject var resetter = Resetter.shared
var body: some Scene {
WindowGroup {
ContentView()
.id(resetter.contentID)
}
}
now from anywhere in code to reset to ContentView you can just call
Resetter.shared.toContentView()

Swift detect changes to a variable from another view within a view

I have the following Class
class GettingData: ObservableObject {
var doneGettingData = false
{
didSet {
if doneGettingData {
print("The instance property doneGettingData is now true.")
} else {
print("The instance property doneGettingData is now false.")
}
}
}
}
And I'm updating the variable doneGettingData within a mapView structure that then I'm using in my main view (contentView).
This variable its changing from false / true while the map gets loaded and I can detecte it from the print that I have used in the class so I know it's changing.
I want to use it to trigger a spinner within ContentView where I have the following code:
import SwiftUI
import Combine
struct ContentView: View {
var done = GettingData().doneGettingData
var body: some View {
VStack {
MapView().edgesIgnoringSafeArea(.top)
Spacer()
Spinner(isAnimating: done, style: .large, color: .red)
}
}
struct Spinner: UIViewRepresentable {
let isAnimating: Bool
let style: UIActivityIndicatorView.Style
let color: UIColor
func makeUIView(context: UIViewRepresentableContext<Spinner>) -> UIActivityIndicatorView {
let spinner = UIActivityIndicatorView(style: style)
spinner.hidesWhenStopped = true
spinner.color = color
return spinner
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<Spinner>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What should I do in order to the variable to change as it is changing with the print but inside the view ? I have tried many options but nothing works !
Thank you
Make your property published
class GettingData: ObservableObject {
#Published var doneGettingData = false
}
then make data observed and pass that instance into MapView for use, so modify that property internally
struct ContentView: View {
#ObservedObject var model = GettingData()
var body: some View {
VStack {
MapView(dataGetter: model).edgesIgnoringSafeArea(.top)
Spacer()
Spinner(isAnimating: model.doneGettingData, style: .large, color: .red)
}
}

EnvironmentVariables not working when passing variable from one view to another in SwiftUI

I have found a few similar examples of how to pass variables among multiple views in SwiftUI:
Hacking with Swift - How to use #EnvironmentObject to share data between views
How to pass variable from one view to another in SwiftUI
I am trying to follow the examples and use EnvironmentVariables and modify the ContentView where it's first defined in the SceneDelegate. However, when trying both examples, I get the error "Compiling failed: 'ContentView_Previews' is not a member type of 'Environment'". I am using Xcode Version 11.3.1.
Following the example given in How to pass variable from one view to another in SwiftUI, here is code contained in ContentView:
class SourceOfTruth: ObservableObject{
#Published var count = 0
}
struct ContentView: View {
#EnvironmentObject var truth: SourceOfTruth
var body: some View {
VStack {
FirstView()
SecondView()
}
}
}
struct FirstView: View {
#EnvironmentObject var truth: SourceOfTruth
var body: some View {
VStack{
Text("\(self.truth.count)")
Button(action:
{self.truth.count = self.truth.count-10})
{
Text("-")
}
}
}
}
struct SecondView: View {
#EnvironmentObject var truth: SourceOfTruth
var body: some View {
Button(action: {self.truth.count = 0}) {
Text("Reset")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(SourceOfTruth())
}
}
... and here is the contents of SceneDelegate:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var truth = SourceOfTruth() // <- Added
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(SourceOfTruth())) // <- Modified
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
}
func sceneDidBecomeActive(_ scene: UIScene) {
}
func sceneWillResignActive(_ scene: UIScene) {
}
func sceneWillEnterForeground(_ scene: UIScene) {
}
func sceneDidEnterBackground(_ scene: UIScene) {
}
}
I does not depend on Xcode version and it is not an issue. You have to set up ContentView in ContentView_Previews in the same way as you did in SceneDelegate, provide .environmentObject, as in below example
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(_Your_object_here())
}
}