Using #EnvironmentObject properties with CADisplayLink - swift

I'm trying to implement CADisplayLink for some animations, but when I try to access my MainData environment object properties from inside class MyAnimations, I get the fatal error No ObservableObject of type MainData found. A View.environmentObject(_:) for MainData may be missing as an ancestor of this view.
In SceneDelegate, I have MainData set as an environment object on ContentView:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var mainData = MainData()
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()
// 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(self.mainData))
self.window = window
window.makeKeyAndVisible()
}
}
...
}
And here's the class with CADisplayLink. createDisplayLink() is called from ContentView:
class MyAnimations: NSObject{
#EnvironmentObject var mainData: MainData
func createDisplayLink() {
let displaylink = CADisplayLink(target: self, selector: #selector(step))
displaylink.add(to: .current, forMode: RunLoop.Mode.default)
}
#objc func step(link: CADisplayLink) {
mainData.displayLinkY += 1.5 //Error here
mainData.displayLinkX += 1.5
}
}
My question is: how can I change environment object properties displayLinkX and displayLinkY from inside step()?

Just remove #EnvironmentObject property wrapper, it is for SwiftUI only
class MyAnimations: NSObject{
var mainData: MainData
init(mainData: MainData) {
self.mainData = mainData
super.init()
}
// ... other code
}

Related

Setup UserDefaults property as Published property in View Model [duplicate]

I have an #ObservedObject in my View:
struct HomeView: View {
#ObservedObject var station = Station()
var body: some View {
Text(self.station.status)
}
which updates text based on a String from Station.status:
class Station: ObservableObject {
#Published var status: String = UserDefaults.standard.string(forKey: "status") ?? "OFFLINE" {
didSet {
UserDefaults.standard.set(status, forKey: "status")
}
}
However, I need to change the value of status in my AppDelegate, because that is where I receive my Firebase Cloud Messages:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
// Print full message.
let rawType = userInfo["type"]
// CHANGE VALUE OF status HERE
}
But if I change the status UserDefaults value in AppDelegate - it won't update in my view.
How can my #ObservedObjectin my view be notified when status changes?
EDIT: Forgot to mention that the 2.0 beta version of SwiftUI is used in the said example.
Here is possible solution
import Combine
// define key for observing
extension UserDefaults {
#objc dynamic var status: String {
get { string(forKey: "status") ?? "OFFLINE" }
set { setValue(newValue, forKey: "status") }
}
}
class Station: ObservableObject {
#Published var status: String = UserDefaults.standard.status {
didSet {
UserDefaults.standard.status = status
}
}
private var cancelable: AnyCancellable?
init() {
cancelable = UserDefaults.standard.publisher(for: \.status)
.sink(receiveValue: { [weak self] newValue in
guard let self = self else { return }
if newValue != self.status { // avoid cycling !!
self.status = newValue
}
})
}
}
Note: SwiftUI 2.0 allows you to use/observe UserDefaults in view directly via AppStorage, so if you need that status only in view, you can just use
struct SomeView: View {
#AppStorage("status") var status: String = "OFFLINE"
...
I would suggest you to use environment object instead or a combination of both of them if required. Environment objects are basically a global state objects. Thus if you change a published property of your environment object it will reflect your view. To set it up you need to pass the object to your initial view through SceneDelegate and you can work with the state in your whole view hierarchy. This is also the way to pass data across very distant sibling views (or if you have more complex scenario).
Simple Example
In your SceneDelegate.swift:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView().environmentObject(GlobalState())
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
The global state should conform ObservableObject. You should put your global variables in there as #Published.
class GlobalState: ObservableObject {
#Published var isLoggedIn: Bool
init(isLoggedIn : Bool) {
self.isLoggedIn = isLoggedIn
}
}
Example of how you publish a variable, not relevant to the already shown example in SceneDelegate
This is then how you can work with your global state inside your view. You need to inject it with the #EnvironmentObject wrapper like this:
struct ContentView: View {
#EnvironmentObject var globalState: GlobalState
var body: some View {
Text("Hello World")
}
}
Now in your case you want to also work with the state in AppDelegate. In order to do this I would suggest you safe the global state variable in your AppDelegate and access it from there in your SceneDelegate before passing to the initial view. To achieve this you should add the following in your AppDelegate:
var globalState : GlobalState!
static func shared() -> AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
Now you can go back to your SceneDelegate and do the following instead of initialising GlobalState directly:
let contentView = ContentView().environmentObject(AppDelegate.shared().globalState)

Compiler warning when setting up SceneDelegate for #EnvironmentObject

I'm trying to implement #EnvironmentObject to pass an array from a View in one tab to another view in another tab.
I get the yellow compiler warning:
Initialization of immutable value 'reportView' was never used;
consider replacing with assignment to '_' or removing it
in SceneDelegate.swift
Here is my SceneDelegate.swift:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var questionsAsked = QuestionsAsked()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
ProductsStore.shared.initializeProducts()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: MotherView().environmentObject(ViewRouter()))
self.window = window
window.makeKeyAndVisible()
}
let reportView = ReportView().environmentObject(questionsAsked)
}
}
Here is my ObservabelObject:
import Foundation
class QuestionsAsked: ObservableObject {
#Published var sectionThings = [SectionThing]()
#Published var memoryPalaceThings = [MemoryPalaceThing]()
}
I use:
#EnvironmentObject var questionsAsked: QuestionsAsked
in my view that generates the data to be passed around.
I pass in the data like so:
questionsAsked.sectionThings = testSectionThings ?? []
In the view where the data is to be passed to I have:
#EnvironmentObject var questionsAsked: QuestionsAsked
I then access it as follows:
totalThings += questionsAsked.sectionThings.count
totalThings += questionsAsked.memoryPalaceThings.count
The issue here is that the line in question is useless as it will not be presented in the Scene. The only view that is presented is MotherView. In order to pass questionsAsked down to that view you just append it like the other .environmentObject. So this should read:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
// this really need to be #StateObject´s
#StateObject var viewRouter = ViewRouter()
#StateObject var questionsAsked = QuestionsAsked()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
ProductsStore.shared.initializeProducts()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
//Inject them here into the MotherView
window.rootViewController = UIHostingController(rootView: MotherView().environmentObject(viewRouter).environmentObject(questionsAsked))
self.window = window
window.makeKeyAndVisible()
}
}
}
In the MotherView and descending Views you can now access them by the #EnvironmentObject wrapper.

What is equivalent to 'window.rootViewController' in WindowGroup - SwiftUI

I am new to SwiftUI and facing a problem where I want to change the root view when a certain action occurs inside the app.
How I handle it when using SceneDelegate was as follows
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
... // the code of initializing the window
observeOnChangeWindow()
}
func observeOnChangeWindow(){
NotificationCenter.default.addObserver(self, selector: #selector(self.performChangeWindow), name: Notification.Name(K.changeWindowNotificationName), object: nil)
}
#objc func performChangeWindow() {
self.window?.rootViewController = UIHostingController(rootView: SplashScreenView())
}
However, I am not currently using SceneDelegate as I am initializing the app using WindowGroup
struct MyApp: App {
var body: some Scene {
WindowGroup {
SplashScreenView()
}
}
}
My question is :
How can I perform the same thing I am doing using SceneDelegate now ?
With the help of comments and some tutorials, I reached out to the solution (Tested on iOS 15, Xcode 13.2.1):
Add the following code to the Main App Launcher.
struct MyApp: App {
#StateObject var appState = AppState.shared
var body: some Scene {
WindowGroup {
SplashScreenView().id(appState.environment)
}
}
}
And then I created the AppState class, which is the class that when changes I will change the window.
class AppState: ObservableObject {
static let shared = AppState()
#Published var environment = "Production"
}
And whenever I wanted to change the environment and do the same functionality of changing window in UIKit , do the following :
AppState.shared.environment = "Pre-Production"

Converting SceneDelegate Enviromental Object to AppMain

Feel free to edit my title for better clarity.
I am starting a new iOS project and am no longer using SceneDelegate/AppDelegate. My problem is I want my ObservableObject to be an Environmental Object for my entire project but am having trouble converting and finding recent examples.
This is how I defined it in my previous iOS 13 project.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions){
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
//Environmental View
let observer = GlobalObserver()
let baseView = SplashScreenView().environment(\.managedObjectContext, context)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: baseView.environmentObject(observer))
self.window = window
window.makeKeyAndVisible()
}
}
Here is my main simplified
#main
struct DefaultApp: App {
//Environmental View
let observer: GlobalObserver
init(){
observer = GlobalObserver()
}
var body: some Scene {
WindowGroup {
LoginView()
//.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
The project generated a PersistenceController which I presume had to do with local storage. Do I need to somehow pass my observer into the .environment for loginView?
Yes, it's exactly as you have it in your example code, except that for some reason you commented it all out. So for instance:
class Thing : ObservableObject {
#Published var name = "Matt"
}
#main
struct SwiftUIApp: App {
#StateObject var thing = Thing()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(thing)
}
}
}
Now in any View struct you just say
#EnvironmentObject var thing : Thing
and presto, you're observing from the global instance.

How do I catch a value change that was changed via a function in SwiftUI?

My View don't check that's the value has changed. Can you tell me why or can you help me?
You can see below the important code.
My Class
import SwiftUI
import SwiftyStoreKit
class Foo: ObservableObject {
#Published var Trigger : Bool = false
func VerifyPurchase() {
let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
switch result {
case .success(let receipt):
let productId = "com.musevisions.SwiftyStoreKit.Purchase1"
// Verify the purchase of Consumable or NonConsumable
let purchaseResult = SwiftyStoreKit.verifyPurchase(
productId: productId,
inReceipt: receipt)
switch purchaseResult {
case .purchased(let receiptItem):
print("\(productId) is purchased: \(receiptItem)")
self.Trigger.toggle()
case .notPurchased:
print("The user has never purchased \(productId)")
self.Trigger.toggle()
}
case .error(let error):
print("Receipt verification failed: \(error)")
self.Trigger.toggle()
}
}
}
}
In SceneDelegate my important code at sceneDidbecom to trigger the value to true and then if the function completed, then I want that's trigger back to false
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let foo = Foo()
let contentView = ContenView(foo: Foo).environment(\.managedObjectContext, context)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidBecomeActive(_ scene: UIScene) {
let foo = Foo()
foo.Trigger = true
foo.VerifyPurchase()
}
My View that's doesnt update self when the Value has changed.
struct ContentView: View {
#ObservedObject var foo: Foo
var body: some View {
Text(self.foo.Trigger ? "true" : "false")
}
}
SwiftyStoreKit.verifyReceipt is an asynchronous function and #Published variables must be updated on the main thread.
Try adding DispatchQueue.main.async when you change the Trigger variable in the background:
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
...
DispatchQueue.main.async {
self.Trigger = false
}
}
Also note that variables in Swift are usually lowercased - ie. trigger instead of Trigger.
You're also using two different Foo instances in your project.
I recommend you remove the code from the func sceneDidBecomeActive(_ scene: UIScene) and move it to the .onAppear in the ContentView:
struct ContentView: View {
#ObservedObject var foo: Foo
var body: some View {
Text(self.foo.Trigger ? "true" : "false")
.onAppear {
foo.Trigger = true
foo.VerifyPurchase()
}
}
}
Also instead of
Text(self.foo.Trigger ? "true" : "false")
you can do
Text(String(describing: self.foo.Trigger))