Trouble adding `UNUserNotificationCenterDelegate` in a SwiftUI app - ios

My problem is that I don't understand how to resolve this warning: ⚠️ Warning: Instance will be immediately deallocated because property 'delegate' is 'weak'
Swift docs state:
You must assign your delegate object to the UNUserNotificationCenter object before your app finishes launching.
In a SwiftUI app that doesn't use AppDelegate, that means I should assign it in App.init().
import SwiftUI
import UserNotifications
class UNCDelegate: NSObject, UNUserNotificationCenterDelegate {
// this is where I think I can respond to the user's tapping on a notification
}
#main
struct MyApp: App {
#StateObject private var dataController: DataController()
init() {
let UNC = UNUserNotificationCenter.current()
UNC.delegate = UNCDelegate() // ⚠️ Warning: Instance will be immediately deallocated because property 'delegate' is 'weak'
// Here I would want to share the UNC across the app by putting it into the SwiftUI environment somehow, so I can schedule notifications whenever I want and still use the handlers in the delegate
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
I wouldn't be surprised if my approach is all wrong. There aren't many examples online that match my use case. My ultimate goal is to open a specific route when user taps on a notification. But with the intention of keeping the question specific, I want to understand how to assign the delegate.

The warning is telling you that the delegate property is weak, so after that line, nothing will be holding a strong reference to the newly created UNCDelegate object, and it will be deallocated.
So just put it in MyApp instead, so that it doesn't get deallocated.
#main
struct MyApp: App {
#StateObject var uncDelegate = UNCDelegate()
init() {
let UNC = UNUserNotificationCenter.current()
UNC.delegate = uncDelegate
}
...
}

Related

How to access data in AppDelegate from ObservableObject class in SwiftUI

With the new SwiftUI Lifecycle there normally is no AppDelegate anymore. However, in order to implement Firebase Messaging, it is recommended on the Firebase docs to implement an AppDelegate and attach it using:
#main
struct ExampleApp: SwiftUI.App {
// register app delegate for Firebase setup
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate //<--- HERE
#StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState)
}
}
}
Inside this AppDelegate functions one obtains an FCM Token, with which the device is identified to subsequently send it notifications remotely. This token has to get sent to the server. This could be done inside the respective function inside the AppDelegate . However, there is a class AppState (ObservableObject protocol) that handles the user data and writing to the server, so it would make a lot more sense to write pass it to this class (or retrieve in the class from the AppDelegate?) and then push it from there.
How can this be accomplished?
edit: I guess this could be achieved by using a static property in the AppDelegate as described in this answer. Is using statics to access variables globally not bad practice? Is there a other (better) way?
You can do it with the "old way" of accessing the delegate
#MainActor //Required by the app delegate
class AppState: ObservableObject{
lazy var appDelegate: AppDelegate? = {
UIApplication.shared.delegate as? AppDelegate
}()
}
Then you can just use
appDelegate?.yourToken
yourToken referencing a property in the delegate

Alternate of ViewController in SwiftUI project

What is the counterpart of ViewController class in a project with interface of type SwiftUI?
I am following this documentation to initialize adLoader from Google AdMob in a SwiftUI project. All examples in the document are within a ViewController : UIViewController class. However, I saw that ViewController is only available when I create a project with storyboard as the interface. But not SwiftUI. Examples in this documentation are initializing the adLoader in the following way by assignment using the keyword self, to parameters rootViewController and adLoader.delegate. My project was created with interface as SwiftUI and does not have a ViewController class. I have a main app class that implements App protocol with #main annotation. I then have a view that is being loaded from the main app class. Please help me understand what I can use in place of the keyword self to initialize the adLoader object.
Code from the google document I am trying to work with:
var adLoader: GADAdLoader!
var nativeAdView: GADNativeAdView!
adLoader = GADAdLoader(adUnitID: YOUR_AD_UNIT_ID, rootViewController: self,
adTypes: [.native],
options: [multipleAdsOptions])
adLoader.delegate = self
Documentation for GADAdLoader
Sample code in project
MyApp.swift
#main
struct MyApp: App {
init() {}
var body: some Scene {
WindowGroup{
HomeView()
}
}
HomeView.swift
struct HomeView: View {
var body: some View {
customMainNavBar
.onAppear(){
------
------
}
}
}
Errors I get when I put this code in the View or the main App.
Cannot convert value of type 'HomeView' to expected argument type 'UIViewController?'
Cannot assign value of type 'HomeView' to type 'GADAdLoaderDelegate?'
Cannot convert value of type 'MyApp' to expected argument type 'UIViewController?'
Cannot assign value of type 'MyApp' to type 'GADAdLoaderDelegate?'

NSKeyValueCoding.setValue(_:forKey:) not working in SwiftUI

I am unable to produce a proper minimal working example, mainly due to my novice level understanding of iOS development, but I do have a simple SwiftUI project that may help.
In my ContentView.swift:
import SwiftUI
struct ContentView: View {
#State var viewText :String
var myClass :MyClass
var body: some View {
VStack{
Text(viewText)
.padding()
Button("Update Text", action: {
myClass.update()
viewText = myClass.txt
})
}
}
}
class MyClass: NSObject {
var txt :String = ""
var useSetVal :Bool = false
func update(){
if(useSetVal){
setValue("used set val", forKey: "txt")
} else {
txt = "used ="
}
useSetVal = !useSetVal
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let mc = MyClass()
ContentView(viewText: "", myClass: mc)
}
}
and in my PracticeApp.swift
import SwiftUI
#main
struct PracticeApp: App {
var body: some Scene {
WindowGroup {
let mc = MyClass()
ContentView(viewText: "", myClass: mc)
}
}
}
In this app, I expect to see the text toggle between "used =" and "used setVal" as I push the button. Instead, I get an exception when I call setValue:
Thread 1: "[<Practice.MyClass 0x60000259dc20> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key txt."
I've been reviewing the answers here but since most answers refer to xib and storyboard files (which I either don't have, or don't know how to find), I don't see how they relate.
By the way, even though the app I'm actually trying to fix doesn't use SwiftUI and the issue with setValue is different, it's still true that I either don't have .xib or .storyboard files or I just don't know where to find them.
I'd appreciate help from any one who could either help me figure out the issue with my example, or who can get me closer to solving the issue with my actual app (including how to produce a proper MWE).
I believe what I've already written is sufficient for the issue (at least for a start), but for those interested, I thought I'd add the full story.
The Full Story
I'm new to iOS development, and I've just taken ownership of an old iOS app. It hasn't really been touched since 2017. I noticed an animation that is not working. Though I cannot verify that it ever did work, I have good reason to assume that it once did, but I can't say when it stopped working.
One issue I noticed is that animated properties are supposed to be updated with the NSKeyValueCoding.setValue(_:forKey:) function, but nothing seems to happen when the function is called.
I was able to work around the issue by overriding the setValue function with my own which basically uses a switch statement to map each key to its corresponding value. However, this did not fix the animation or explain why the setValue function isn't working.
Because both the setValue function and the CABasicAnimation.add(_:forKey:) rely on the same keyPath, I wonder if solving one issue might help me solve the other. I've decided to focus on the setValue issue (at least for now).
When I went to work starting a new project to use as an MWE, I noticed that neither the Storyboard nor the SwiftUI interface options provided by Xcode 13.0 (13A233) started me out with a project structure that matched my existing project. It was clear to me that SwiftUI was new and very different from my existing project, but the Storyboard interface wasn't familiar either and after several minutes a reading tutorials, I failed to build a storyboard app that would respond to button presses at all (all the storyboard app tutorials I found seemed to be set up for older versions of Xcode).
SwiftUI will require that you use #ObservedObject to react to changes in an object. You can make this compliant with both observedobject and key-value manipulation as follows:
struct ContentView: View {
#State var viewText :String
#ObservedObject var myClass :MyClass
var body: some View {
VStack{
Text(viewText)
.padding()
Button("Update Text", action: {
myClass.update()
viewText = myClass.txt
})
}
}
}
class MyClass: NSObject, ObservableObject {
#objc dynamic var txt: String = ""
#Published var useSetVal: Bool = false
func update(){
if(useSetVal){
setValue("used set val", forKey: "txt")
} else {
txt = "used ="
}
useSetVal = !useSetVal
}
}
You need to make the txt property available to Objective-C, in order to make it work with KVO/KVC. This is required as the Key-Value Observing/Coding mechanism is an Objective-C feature.
So, either
class MyClass: NSObject {
#objc var txt: String = ""
, or
#objcMembers class MyClass: NSObject {
var txt: String = ""
This will fix the error, and should make your app behave as expected. However, as others have said, you need to make more changes to the code in order to adhere to the SwiftUI paradigms.

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.

Is there a way to have a custom class act on a UIView without having the ViewController passed (as reference) upon its initialization?

Example: I have a SpeechSynthesizer class that needs to update something in my UIView when it’s done uttering a piece of text. Since the SpeechSynthesizer class conforms to protocol AVSpeechSynthesizerDelegate, it is the one that receives the didFinish signal when the uttering has been completed. The idea here is to keep the ViewController from having too many delegate methods and a long list of protocols to conform to. The workaround I found was to have the ViewController passed in as a SpeechSynthesizer initialization parameter. This way I get to access the ViewController connected to the UIView I want to update from inside the SpeechSynthesizer class. The thing I don’t like about it is that it looks kind of ugly to have the ViewController passed in as a parameter to every single class that needs to use it. So I wonder, which other way I could accomplish this.
I suppose another way to ask the question is: How can I make the function
private func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance)
return something to a ViewController since it's not "called" by it?
I added a reply on Quora. Copying it here:
After doing some research and testing on code of my own here are 2 solutions to this problem.
Solution 1: The Delegate Pattern:
Create a custom delegate protocol in the ViewController
protocol ViewControllerDelegate:class {
func getViewLayer() -> CALayer
}
The ViewController must conform to this newly created protocol and therefore implement all the functions defined by it, so somewhere in the class ViewController you add:
public func getViewLayer() -> CALayer {
return self.view.layer
}
Then on my custom class, ReadTextMachine, I added a variable of the ViewControllerDelegate type
private weak var viewControllerDelegate: ViewControllerDelegate?
The variable must be weak and protocol must be of type class in order to solve a “retain cycle” problem (since both the custom class and the ViewController will point to each other)
You’ll notice now that the function call inside the ViewController is already “callable” from the custom class, so in my ReadTextMachine I added:
let viewLayer = self.viewControllerDelegate?.getViewLayer()
self.cameraPreview = CameraPreview(session: self.camera.getSession(), container: viewLayer!)
self.cameraPreview?.addPreview()
In the above case, my CameraPreview (yes, a 3rd class in this example) simply adds a camera preview layer on the UIView. For that it needed access to the main View’s layer.
The above code still doesn’t work because our original viewController’s instance hasn’t been passed as reference anywhere in our code. For that we add the following function in ReadTextMachine:
public func setViewControllerDelegate(viewController: ViewController) { // call this from the ViewController so that ViewController can be accessed from here.
self.viewControllerDelegate = viewController
}
The above piece of code will have to be called from the ViewController, after we instantiate our custom class (ReadTextMachine), so that the viewControllerDelegate inside it points to the ViewController. So in our ViewController.swift:
operatingMode = ReadTextMachine()
operatingMode.setViewControllerDelegate(viewController: self)
Another example and explanation can be found in this video from LetsBuildThatApp. I derived my solution mostly from it.
My current app in development applying the above solution can be found here: agu3rra/World-Aloud
Solution 2: Notifications and Observers pattern
This one is a bit easier to understand and follow. The general idea is to have your custom class broadcast a message which triggers a function call on your ViewController since it has an observer setup, waiting to hear that message.
So to give an example, in the context I used it, I have a CameraCapture class which uses AVFoundation to capture a photo. The capture photo trigger cannot immediately return an image, since iOS has a set of steps to execute before actually generating an image. I wanted my ReadTextMachine to resume an activity after CameraCapture had a photo available. (To apply this in the context of the CustomClass triggers ViewController event is basically the same, since both are actual classes in an iOS app as well).
So the 1st thing I did was create a broadcast function since I would use it in many places in my app. I simply placed it in a Utilities.swift file in the Xcode project.
public func broadcastNotification(name: String) {
let notification = Notification.Name(rawValue: name)
NotificationCenter.default.post(name: notification, object: nil)
}
The above function takes a string, which must be a unique notification identifier, and broadcasts it thru NotificationCenter.
In my CameraCapture class, I added a static constant to reference the unique identifier of the message:
static let NOTIFY_PHOTO_CAPTURED = "agu3rra.worldAloud.photo.captured"
For those who know AVFoundation, a photo is available when event didFinishProcessingPhoto gets executed, so at the end of that I added:
broadcastNotification(name: CameraCapture.NOTIFY_PHOTO_CAPTURED)
The above is a call to my previously defined utility function.
For my ReadTextMachine class to be able to catch that notification, I added the following on its init() and deinit routines:
override init() {
super.init()
// Setup event observers
let notification1 = Notification.Name(rawValue: CameraCapture.NOTIFY_PHOTO_CAPTURED)
NotificationCenter.default.addObserver(self,
selector: #selector(self.processingDoneTakingPhoto),
name: notification1,
object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self) // cleanup observer once instance no longer exists
}
Removing the observer is important at deinit so that when your object is deallocated from memory, the observer isn’t left lingering around. The above configured observer triggers a function call inside ReadTextMachine:
#IBAction private func processingDoneTakingPhoto() {
// does my stuff
}
That’s it! Again, the entire Xcode project can be downloaded from my project’s Git repository: agu3rra/World-Aloud
Hope this can be of use to others.
Cheers!

Resources