How to implement handleUserActivity in WatchOS in SwiftUI? - swift

I've passed to my complicationDescriptors a userInfo dictionary that includes the names of my complications so I can be notified of which complication the user tapped on and launch them to that View. I'm using the new #App and #WKExtensionDelegateAdaptor for other reasons, so I have access to handleUserActivity in my extensionDelegate and can unpack the source of the complication tap, but how do I launch them to a specific view in SwiftUI? handleUserActivity seems to be set up to work with WKInterfaceControllers?
class ExtensionDelegate: NSObject, WKExtensionDelegate {
let defaults = UserDefaults.standard
func applicationDidFinishLaunching() {
//print("ApplicationDidFinishLanching called")
scheduleNextReload()
}
func applicationWillResignActive() {
//print("applicationWillResignActive called")
}
func handleUserActivity(_ userInfo: [AnyHashable : Any]?) {
if let complication = userInfo?[TrackerConstants.complicationUserTappedKey] as? String {
if complication == TrackerConstants.recoveryDescriptorKey {
//What now?
} else if complication == TrackerConstants.exertionDescriptorKey {
//What now?
}
}
}

I managed to update my view according to the tapped complication userInfo by using notifications, inspired by this answer.
First declare a notification name:
extension Notification.Name {
static let complicationTapped = Notification.Name("complicationTapped")
}
In your ExtensionDelegate:
func handleUserActivity(_ userInfo: [AnyHashable : Any]?) {
if let complication = userInfo?[TrackerConstants.complicationUserTappedKey] as? String {
NotificationCenter.default.post(
name: Notification.Name.complicationTapped,
object: complication
)
}
}
Finally in your view:
struct ContentView: View {
#State private var activeComplication: String? = nil
var body: some View {
NavigationView { // or TabView, etc
// Your content with NavigationLinks
}
.onReceive(NotificationCenter.default.publisher(
for: Notification.Name.complicationTapped
)) { output in
self.activeComplication = output.object as? String
}
}
}
For more information on how to activate a view from here, see Programmatic navigation in SwiftUI

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)

SwiftUI published variable not updating view

Context: I am trying to use firebase authentication and firestore to get the user's data. The problem I am running into is that the views are presented before the data is completely fetched and that obviously causes the app to crash. That being said, I am utilizing the firebase authentication listener in my app delegate to ensure the user is authenticated before fetching the users' data (which is also done in the app delegate as shown below)
App delegate snippet
class AppDelegate: NSObject, UIApplicationDelegate {
var handle: AuthStateDidChangeListenerHandle?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
self.handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if (user != nil){
print("UserAuthentication User authenticated in delegate")
DatabaseDelegate().getUserInfo(UID: user!.uid, withCompletionHandler: {
print("got user data")
})
} else {
print(" UserAuthentication User not authenticated in delegate")
try! Auth.auth().signOut()
}
}
return true
}
This is the database code I am querying and want to listen for when the data is finished loading:
class DatabaseDelegate: ObservableObject {
#Published var userDataLoaded = Bool()
func getUserInfo(UID: String, withCompletionHandler completionHandler: #escaping () -> Void) {
database.collection("Users").document(UID).getDocument { (document, error) in
if let document = document, document.exists {
let data = document.data()!
guard let UID = data["UUID"] as? String else { return }
guard let Name = data["Name"] as? String else { return }
guard let PhoneNumber = data["PhoneNumber"] as? String else { return }
guard let StripeID = data["StripeID"] as? String else { return }
self.userDataLoaded = true
UserData.append(User(UID: UID, Name: Name, PhoneNumber: PhoneNumber, StripeID: StripeID, PurchasedContent: ["TEMP": true]))
completionHandler()
}
}
}
}
And this is the SwiftUI view I want to update based on the userDataLoaded above:
struct MainViewDelegate: View {
//MARK: VARIABLES
#State var showAnimation = true
#State var locationHandler = LocationHandler()
#ObservedObject var databaseDelegate = DatabaseDelegate()
init(){
//Check and enable user location
locationHandler.requestAuthorisation()
}
var body: some View {
VStack {
//Check if data has finished loading, if not, show loading. Listen for changes when the data is finished loading and then present the tab view when it is.
switch databaseDelegate.userDataLoaded {
case true:
TabView {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
CheckoutView()
.tabItem {
Label("Services", systemImage: "bag")
}
SettingsView()
.tabItem {
Label("Settings", systemImage: "gearshape")
}
}
case false:
Text("Loading data")
}
}
}
}
Thank you in advanced. I am new to swiftui (transitioning from uikit) and I've spent too much time trying to solve this silly issue
You're using two different instances of DatabaseDelegate, one in the AppDelegate and one in the MainViewDelegate. The boolean is only updated in app delegate's instance.
Move your auth listener into your DatabaseDelegate.
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
class DatabaseDelegate: ObservableObject {
#Published var userDataLoaded = false
private var handle: AuthStateDidChangeListenerHandle?
init() {
self.handle = Auth.auth().addStateDidChangeListener { (auth, user) in
// .. etc
self.getUserInfo(...)
}
}
private func getUserInfo(UID: String, withCompletionHandler completionHandler: #escaping () -> Void) {
database.collection("Users").document(UID).getDocument { (document, error) in
// .. etc
self.userDataLoaded = true
}
}
}
You need to use StateObject instead of ObservedObject since you are initializing it internally on the view, instead of injecting it from an external source.
struct MainViewDelegate: View {
#StateObject private var databaseDelegate = DatabaseDelegate()
}
If you want to use ObservedObject, you can create it externally and inject into the view like so:
var databaseDelegate = DatabaseDelegate()
MainViewDelegate(databaseDelegate: databaseDelegate)
struct MainViewDelegate: View {
#ObservedObject var databaseDelegate: DatabaseDelegate
}

Going crazy with UserDefaults in Swift[UI]

Launching myself into Swift and SwiftUI, I find the process of migrating from UIKit quite hard.
Presently stomped by UserDefaults, even after trying to make sense of the many tutorials I found on the web.
Please tell me what I'm doing wrong here :
VERY simple code to :
register a bool value to a UserDefault,
display that bool in a text !
Doesn't get any simpler than that.
But I can't get it to work, as the call to UserDefaults throws this error message :
Instance method 'appendInterpolation' requires that 'Bool' conform to '_FormatSpecifiable'
My "app" is the default single view app with the 2 following changes :
1- In AppDelegate, I register my bool :
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UserDefaults.standard.register(defaults: [
"MyBool 1": true
])
return true
}
2- in ContentView, I try to display it (inside struct ContentView: View) :
let defaults = UserDefaults.standard
var body: some View {
Text("The BOOL 1 value is : Bool 1 = \(defaults.bool(forKey: "MyBool 1"))")
}
Any ideas ?
Thanks
Your issue is that the Text(...) initializer takes a LocalizedStringKey rather than a String which supports different types in its string interpolation than plain strings do (which does not include Bool apparently).
There's a couple ways you can work around this.
You could use the Text initializer that takes a String and just displays it verbatim without attempting to do any localization:
var body: some View {
Text(verbatim: "The BOOL 1 value is : Bool 1 = \(defaults.bool(forKey: "MyBool 1"))")
}
Alternatively, you could extend LocalizedStringKey.StringInterpolation to support bools and then your original code should work:
extension LocalizedStringKey.StringInterpolation {
mutating func appendInterpolation(_ value: Bool) {
appendInterpolation(String(value))
}
}
To solve your problem, just add description variable, like:
var body: some View {
Text("The BOOL 1 value is : Bool 1 = \(defaults.bool(forKey: "MyBool 1").description)")
}
To answer your questions:
1- register a bool value to a UserDefault,
2- display that bool in a text !
I tested the following code and confirm that it works on ios 13.4 and macos using catalyst. Note the String(...) wrapping.
in class AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// UserDefaults.standard.register(defaults: ["MyBool 1": true])
UserDefaults.standard.set(true, forKey: "MyBool 1")
return true
}
in ContentView
import SwiftUI
struct ContentView: View {
#State var defaultValue = false // for testing
let defaults = UserDefaults.standard
var body: some View {
VStack {
Text("bull = \(String(UserDefaults.standard.bool(forKey: "MyBool 1")))")
Text(" A The BOOL 1 value is Bool 1 = \(String(defaultValue))")
Text(" B The BOOL 1 value is : Bool 1 = \(String(defaults.bool(forKey: "MyBool 1")))")
}
.onAppear(perform: loadData)
}
func loadData() {
defaultValue = defaults.bool(forKey: "MyBool 1")
print("----> defaultValue: \(defaultValue) ")
}
}
Not sure why you use register, but you can just set the bool value like this:
UserDefaults.standard.set(true, forKey: "MyBool1")
in SwiftUI I use these:
UserDefaults.standard.set(true, forKey: "MyBool 1")
let bull = UserDefaults.standard.bool(forKey: "MyBool 1")
I figured that the best way to use UserDefaults is inside a class. It helps us subscribe to that class from any model using #ObservedObject property wrapper.
Boolean method can be used for rest of the types
//
// ContentView.swift
//
import SwiftUI
struct ContentView: View {
#ObservedObject var data = UserData()
var body: some View {
VStack {
Toggle(isOn: $data.isLocked){ Text("Locked") }
List(data.users) { user in
Text(user.name)
if data.isLocked {
Text("User is Locked")
} else {
Text("User is Unlocked")
}
}
}
}
}
//
// Model.swift
//
import SwiftUI
import Combine
let defaults = UserDefaults.standard
let usersData: [User] = loadJSON("Users.json")
// This is custom JSON loader and User struct is to be defined
final class UserData: ObservableObject {
// Saving a Boolean
#Published var isLocked = defaults.bool(forKey: "Locked") {
didSet {
defaults.set(self.isLocked, forKey: "Locked")
}
}
// Saving Object after encoding
#Published var users: [User] {
// didSet will only work if used as Binding variable. Else need to create a save method, which same as the following didSet code.
didSet {
// Encoding Data to UserDefault if value of user data change
if let encoded = try? JSONEncoder().encode(users) {
defaults.set(encoded, forKey: "Users")
}
}
}
init() {
// Decoding Data from UserDefault
if let users = defaults.data(forKey: "Users") {
if let decoded = try? JSONDecoder().decode([User].self, from: users) {
self.users = decoded
return
}
}
// Fallback value if key "Users" is not found
self.users = usersData
}
// resetting UserDefaults to initial values
func resetData() {
defaults.removeObject(forKey: "Users")
self.isLocked = false
self.users = usersData
}
}
Note: This code is not tested. It is directly typed here.
Try this:
struct MyView {
private let userDefaults: UserDefaults
// Allow for dependency injection, should probably be some protocol instead of `UserDefaults` right away
public init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
}
}
// MARK: - View
extension MyView: View {
var body: some View {
Text("The BOOL 1 value is: \(self.descriptionOfMyBool1)")
}
}
private extension MyView {
var descriptionOfMyBool1: String {
let key = "MyBool 1"
return "\(boolFromDefaults(key: key))"
}
// should probably not be here... move to some KeyValue protocol type, that you use instead of `UserDefaults`...
func boolFromDefaults(key: String) -> Bool {
userDefaults.bool(forKey: key)
}
}

Keeping a value updated throughout many ViewController's in iOS using Swift

In my ViewController (VC1) I have the following variable:
var orderInfo: Order!
Order is a struct itself like the following:
struct Order {
var orderId: String
var orderReferenceNumber: String
//more variables...
init(
orderId: String,
orderReferenceNumber: String,
){
self.orderId = orderId
self.orderReferenceNumber = orderReferenceNumber
}
init(data: [String: Any]){
orderId = data[DatabaseRef.orderId] as? String ?? ""
orderReferenceNumber = data[DatabaseRef.orderReferenceNumber] as? String ?? ""
}
static func modelToData(order: Order) -> [String: Any] {
let data : [String: Any] = [
DatabaseRef.orderId: order.orderId,
DatabaseRef.orderReferenceNumber: order.orderReferenceNumber,
]
return data
}
}
In VC1, I have a listener that updates its info from Firestore Database (throught addsnapshotslistener). When the variable orderInfo gets updated in VC1 because of a change in the order in the database the listener will update the orderInfo variable in VC1. While the user is in another ViewController (e.g. VC2), I would like to access orderInfo variable with its updated info from VC1. How can I make this happen?
There are numerous of ways of doing that. Lets mention some of them.
You can use NotificationCenter
Example
class VC1: UIViewController {
var orderInfo: Order!
func updateOrder() {
var orderDict = ["orderInfo":orderInfo]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "orderInfoUpdated"), object: nil, userInfo: orderDict)
}
}
// The receiving end
class VC2: UIViewController {
var orderInfo: Order!
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(doSomething(_: )), name: NSNotification.Name(rawValue: "orderInfoUpdated"), object: nil)
}
func doSomething(_ notification: NSNotification) {
if let orderInfo = notification.userInfo?["orderInfo"] as? Order {
// Do something
}
}
}
You can create an "app state" service where you use the singleton
pattern making the user info accessible in all the views
Example:
class AppState {
static let shared = AppState()
var orderInfo: Order!
...
}
On your ViewController
class ViewController: UIViewController {
func doSomeWork() {
print(AppState.shared.orderInfo)
}
}
You can make it global (Not Recommended)

Swift: Change the tabBar icon from appDelegate

I would like to change an icon of my tabbar from the appDelegate.
I will explain why I want to do that.
From my website I send data by push to my application, so in the appdelegate I recieve the data in the function didReceiveRemoteNotification, with these data I create a local notification manually. I would also like to be able to modify the icon of my TabBar to show that there is a new notification.
So how to change the icon of the tabbar from the app delegate?
Here is a photo of my app, the green circle is the part that means "New notification"
My App images
Here my code in appdelegate:
func application(_ application: UIApplication, didReceiveRemoteNotification data: [AnyHashable: Any]) {
if let data = data as? NSDictionary {
print("Title: \(data)")
LocalNotification.createLocalNotificationWithIntervals(identifier: "Push",
title: data["title"] as! String,
body: data["body"] as! String,
intervals: 1) { error in
guard error == nil else {
print("Error: \(error!.localizedDescription)")
return
}
print("Successfully execute notification")
}
}
}
I user a tabor controller:
class FittoTabBarController: UITabBarController {
let kImageNoLabelInset: CGFloat = 6.0
var selectedTab: FittoTabBar.Tabs = .pods {
didSet {
selectedIndex = selectedTab.rawValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
selectedTab = .pods
removeItemsTitles()
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
guard let selectedItemIndex = tabBar.items?.index(of: item),
let selectedTab = FittoTabBar.Tabs(rawValue: selectedItemIndex) else {
return
}
self.selectedTab = selectedTab
}
private func removeItemsTitles() {
if let items = self.tabBar.items {
for item in items {
item.title = ""
item.imageInsets = UIEdgeInsets(top: kImageNoLabelInset, left: 0.0, bottom: -kImageNoLabelInset, right: 0.0)
}
}
}
The input of my application is on the tabbar controller
With the code provided above, You need to follow these steps to make it working.
In your FittoTabBarController add these methods.
func setBadge(_ value: String?) {
_ = self.viewControllers![2].tabBarItem.badgeValue = value
}
func getBadge() -> String? {
return self.viewControllers![2].tabBarItem.badgeValue
}
func resetBadge() {
self.viewControllers![2].tabBarItem.badgeValue = nil
}
In your appDelegate, get the rootViewController of your window, on receiving notification
func application(_ application: UIApplication, didReceiveRemoteNotification data: [AnyHashable: Any]) {
if let data = data as? NSDictionary {
print("Title: \(data)")
let myTabBarController = self.window?.rootViewController as! FittoTabBarController
var newBadgeCount = "1"
if let currentBadgeCount = myTabBarController.getBadge() {
// Convert to int
var intValOfCurrentBadge = Int(currentBadgeCount)!
// Increaset it by one.
intValOfCurrentBadge = intValOfCurrentBadge + 1
// Convert back to string.
newBadgeCount = "\(intValOfCurrentBadge)"
}
// Set your badge value here.
myTabBarController.setBadge(newBadgeCount)
// ADD YOUR EXISTING CODE HERE
}
}
when user clicks on the 3rd tab, just call the resetBadge() method to remove the badge count.
Hope it helps.