How to pass an EnvironmentObject to an ObservedObject within that EnvironmentObject? - ios

I have an EnvironmentObject called GameManager() that is basically the root of my app:
class GameManager: ObservableObject {
#ObservedObject var timeManager = TimeManager()
Because there is a lot of code in it, I want to delegate certain tasks into seperate classes/files.
For example, I want to create a timer running every second. This could easily run inside GameManager(), but I want to delegate this to a seperate class called TimeManager():
class TimeManager: ObservableObject {
#EnvironmentObject var gameManager: GameManager
var activeGameTimer: Timer?
#Published var activeGameSeconds: Int = 0
func start(){
self.activeGameTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ _ in
self.activeGameSeconds += 1
}
}
}
Only problem is, TimeManager needs to have a reference to GameManager() - in order to react to certain events in my game.
However, there doesn't seem to be a way to pass GameManager() into TimeManager().
Is there a smooth way to achieve this? If not, is there another way I should arrange what I'm trying to do?
If the solution is hacky, I would rather not do it.

First of all #ObservedObject and #EnvironmentObject are property wrappers designed for SwiftUI View not for other else, so using them in classes might be harmful or at least useless - they do not functioning as you'd expected.
Now the solution for your scenario is dependency injection (both types are reference-types so instances are injected as a references):
class GameManager: ObservableObject {
var timeManager: TimeManager!
init() {
timeManager = TimeManager(gameManager: self)
}
// ... other code
}
class TimeManager: ObservableObject {
var gameManager: GameManager
var activeGameTimer: Timer?
#Published var activeGameSeconds: Int = 0
init(gameManager: GameManager) {
self.gameManager = gameManager
}
// ... other code
}

Related

iOS: MVVM-C ViewController super class

I have MVVM-C arch. Each UIViewController has a ViewModel and CoordinatorDelegate to notify the Coordinator when navigation needs to be performed. The code that create the VC repeat itself, and I though it would be great to create a super class to unify all static funcs that create the VC. Like this:
import UIKit
class MVVMCViewController: UIViewController {
weak var coordinatorDelegate: CoordinatorDelegate?
var viewModel: Modelling?
static func initVC(storyboard: Storyboard,
coordinatorDelegate: CoordinatorDelegate?,
viewModel: Modelling?) -> Self {
let viewController = Self.instantiate(in: storyboard)
viewController.coordinatorDelegate = coordinatorDelegate
viewController.viewModel = viewModel
return viewController
}
}
All CoordinatorDelegateProtocols will inherit from CoordinatorDelegate and all ViewModels will be inheriting from Modelling
But the subclassing does not work smoothly.
Any ideas?
Hi this model wouldn't work fine.
MVVMCViewController has hardcoded protocols as variable type, so You should have the same in your childVC.
To make it work as u want MVVMCViewController show be generic (but can be a lot of issues with it), like
class MVVMCViewController<T: Modelling, U: CoordinatorDelegate>: UIViewController {
weak var coordinatorDelegate: U?
var viewModel: T?
}
or add just casted properties to ConnectViewController
class ConnectViewController: MVVMCViewController {
weak var coordinatorDelegate: CoordinatorDelegate?
var viewModel: Modelling?
var currentDelegate: ConnectViewControllerCoordinatorDelegate? {
coordinatorDelegate as? ConnectViewControllerCoordinatorDelegate
}
var currentVM: ConnectViewModel? {
viewModel as? ConnectViewModel
}
}
Your superclass MVVMCViewController defines two properties coordinatorDelegate and viewModel. If you just need to access them in your child class ConnectViewController, just access it normally. You don't need to define it again.
Also, in your parent class, you have weak var coordinatorDelegate: CoordinatorDelegate?. But in your child class (ConnectViewController), you redeclare the property with a different type (ConnectViewControllerCoordinatorDelegate?). That is illegal, even if it is a subclass of CoordinatorDelegate.
Hence, either
Rename the property in child class to avoid the conflict
Keep the name and the type, but add an override keyword for the property if you plan to add additional functionality in your child class
Do not declare the property again at all in your child class if you don't need to add additional functionality to it. Just access it directly.
Refer to how inheritance works in Swift over here: https://docs.swift.org/swift-book/LanguageGuide/Inheritance.html

SwiftUI Change Observed Object From Another View

I have an observed object in my ContentView which contains tabviews.
class GlobalValue: ObservableObject {
#Published var stringValue = ""
#Published var intValue = 0
}
struct ContentView: View {
#ObservedObject var globalValue = GlobalValue()
}
From one of the tabs I need to change ContentView's observed object value.
ContentView().globalValue.intValue=25
It is not changing the value. How can I change that observed object value? thanks...
It is changing the value. The problem is you are instantiating an instance of GlobalValue locally in your struct. It has no life outside of the struct. I you want to use an Observable Object as a global store like that, you need to create it once and use that instance.
The easiest way to do this is to add
static let shared = GlobalValue() in your class, and in your struct use globalValue = GlobalValue.shared which essentially gives you a singleton. You will have one instance that all the views can read and write to.

Handle Auth state in WindowGroup

I'm new to SwiftUI still and don't really know how to handle best the auth state. If a user is logged in for example i want to redirect him to home screen if not to a certain screen.
I have a service that will tell me if the user is authenticated like: self.authService.isAuthenticated but in my App in WindowGroup i cannot use my service since this is all a struct and i get Cannot use mutating getter on immutable value: 'self' is immutable
I would appreciate a little snippet that can help me solve this here.
My code:
#main
struct MyApp: App, HasDependencies {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// MARK: Services
private lazy var authService: AuthService = dependencies.authService()
var body: some Scene {
WindowGroup {
if !self.authService.isAuthenticated {
WelcomeView()
} else {
MainView()
}
}
}
}
I suppose you want to handle it just for this time, but i'm proposing you look deeper in SwiftUI bindings and state handlings.
So here we just save the value in a variable in the init since this is getting loaded first.
#main
struct MainApp: App, HasDependencies {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// MARK: Services
private lazy var authService: AuthService = dependencies.authService()
var isAuth: Bool = false
init() {
isAuth = self.authService.isAuthenticated
}
var body: some Scene {
WindowGroup {
if isAuth {
MainView()
} else {
WelcomeView()
}
}
}
}
The problem is
private lazy var authService: AuthService = dependencies.authService()
(A) SwiftUI rebuilds views in response to, for example, a #StateObject's ObjectWillChangePublisher. Changes an unwatched variable fall silently in a forest without participating in this UI framework, but would be read if you trigger a state change by some other object. Also, I'd guess that service will be rebuilt every time the struct is first built, but I haven't had a use case for this scenario yet, so I don't know.
(B) You've got a mutating variable holding a reference type stored in a value type. As above, store your service as an #StateObject, which is one way SwiftUI gets around this problem of lifetime management.
To get "lazy" loading, call .onAppear { service.load() }.
That said, you have a services / factory container you probably already want to be an #StateObject and injected into the environment. If you store an ObservableObject inside an ObservableObject, the View will react to the outer object only. That object does not link its ObjectWillChangePublisher to inner objects. You will need to either:
(a) individually inject select services into the environment for children to observe
(b) pass those into observable view models that use Combine to subscribe to specific states
(c) use .onReceive and .onChange APIs on Views to link to specific state changes
(C) Conditionals evaluated in App can cause objects stored in that struct to be rebuilt. Good practice is to keep App super clean, like always. Move any conditional logic to a "Root" View for that Scene.

Using Kotlin mulitplatform classes in SwiftUI

I am building a small project using the Kotlin Mulitplatform Mobile (KMM) framework and am using SwiftUI for the iOS application part of the project (both of which I have never used before).
In the boilerplate for the KMM application there is a Greeting class which has a greeting method that returns a string:
package com.example.myfirstapp.shared
class Greeting {
fun greeting(): String {
return "Hello World!"
}
}
If the shared package is imported into the iOS project using SwiftUI, then the greeting method can be invoked and the string that's returned can put into the View (e.g. Text(Greeting().greeting()))
I have implemented a different shared Kotlin class whose properties are modified/fetched using getters and setters, e.g. for simplicity:
package com.example.myfirstapp.shared
class Counter {
private var count: Int = 0
getCount() {
return count
}
increment() {
count++
}
}
In my Android app I can just instantiate the class and call the setters to mutate the properties of the class and use it as application state. However, I have tried a number of different ways but cannot seem to find the correct way to do this within SwiftUI.
I am able to create the class by either creating a piece of state within the View that I want to use the Counter class in:
#State counter: Counter = shared.Counter()
If I do this then using the getCount() method I can see the initial count property of the class (0), but I am not able to use the setter increment() to modify the property the same way that I can in the Android Activity.
Any advice on the correct/best way to do this would be greatly appreciated!
Here's an example of what I'd like to be able to do just in case that helps:
import shared
import SwiftUI
struct CounterView: View {
#State var counter: shared.Counter = shared.Counter() // Maybe should be #StateObject?
var body: some View {
VStack {
Text("Count: \(counter.getCount())")
Button("Increment") { // Pressing this button updates the
counter.increment() // UI state on the previous line
}
}
}
}
I believe the fundamental issue is that there isn't anything that's notifying SwiftUI layer when the count property is changed in the shared code (when increment is called). You can at least verify that value is being incremented by doing something like following (where we manually retrieve updated count after incrementing it)
struct ContentView: View {
#ObservedObject var viewModel = ViewModel(counter: Counter())
var body: some View {
VStack {
Text("Count: \(viewModel.count)")
Button("Increment") {
viewModel.increment()
}
}
}
}
class ViewModel: ObservableObject {
#Published var count: Int32 = 0
func increment() {
counter.increment()
count = counter.getCount()
}
private let counter: Counter
init(counter: Counter) {
self.counter = counter
}
}

Global variable in Struct using mutating function/map doesn't changing value when updating

In View Controller
viewModel.frequentDate = Date() // This changes frequently
In ViewModel
struct ViewModel {
var frequentDate = Date()
mutating func validateSomeTime(startDatePicker: Observable<Date>) {
self.validdate = startDatePicker.map(someValid)
}
func someValid(_ date: Date) -> String {
if date = self.frequentDate {
return "Done"
}
}
}
frequentDate doesn't change frequently it always give first initial date. When I call observable validate the date value is initial one not updates the latest.
this self.frequentDate will never change of the VC lift cycle.
But if I changed ViewModel struct to class. It works fine
#Paulw11 mentioned a good point. You should probably make your viewModel a class and control its lifecycle in ViewController. The simplest way to do it is to instantiate your ViewModel directly inside ViewController
class ChurchesViewController {
let viewModel = ChurchesViewModel()
}
But what to do if your ViewModel uses some repositories, managers etc.? Well, you should inject ViewModel into ViewController somewhere else. In a simplest architecture such as MVVM-C you will probably use Coordinator to construct your flow:
class ChurchesCoordinator: Coordinator {
init(navigationController: UINavigationController, firstManager: FirstManager, secondManager: SecondManager) { ... }
func start() {
let vc = ChurchesViewController() // any instantiation method
let vm = ChurchesViewModel(firstManager: self.firstManager, secondManager: self.secondManager)
vc.viewModel = vm
navigationController.pushViewController(vc, animated: true, completion: nil)
}
}
This is a simple example, but I personally use a library for DI called Dip or Swinject, it simplifies things a lot. If you to choose other architectures, probably you will have another modules to make a DI.

Resources