A proper way to receive applicationContext and avoid re-transfering? - WatchConnectivity - ios

I have been working on sending an int notificationCount in form of ApplicationsContext from my iOS app to my WatchOS.
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {}
The problem is, the code snippet above only reacts, whenever the notificationCount is changed. Which means when I open my InterfaceController where I need the notificationCount, I don't have any numbers before the value gets updated from the iOS counterpart.
I do suspect that didReceiveApplicationContext only when the sending value is not the same. But is there a proper way to check the value of notificationCount for having the same value as the recent transfer to avoid some re-transfer?

Just save the notificationCount in your Extension Delegate's property (or any other instance you might use for storing data) and use it in your controller's awake method (or/and willActivate if appropriate). In addition, you might want to save it in your UserDefaults or a file so it would survive re-launching your app.
Something like this:
class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate {
var notificationCount: Int?
// ...
func applicationDidFinishLaunching() {
// do your normal init stuff
// read notificationCount from UserDefaults
}
// ...
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
if let count = applicationContext["NotificationCount"] {
notificationCount = count
// save to UserDefaults here
}
}
}

Related

Swift WatchConnectivity app context as Dictionary

I am working on my first Apple Watch app (an extension to my iOS app). I am facing a small problem in sending data from one WKInterfaceController to another.
My First Controller (InterfaceController.swift) has didReceiveMessage where it receives data from my iOS app.
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
let myQrValue = message["qrCode"] as! String
let myQrImage = message["qrCodeImageData"] as! Data
var myData: [AnyHashable: Any] = ["myQrValue": myQrValue, "myQrImage": myQrImage]
if myQrValue.isEmpty == false {
WKInterfaceController.reloadRootControllers(withNames: ["QrScreen"], contexts: [myData])
}
}
Then in my Second Controller (QrInterfaceController.swift), I am having below to fetch the data sent from the first controller -
override func awake(withContext context: Any?) {
super.awake(withContext: context)
print("context \(context)")
if let myData = context {
print("myData \(myData)")
// userQrNumber.setText(myData)
}
if let myQrImage = myQrImage {
userQrImage.setImageData(myQrImage)
}
if let myQrLabel = myQrLabel {
userQrNumber.setText(myQrLabel)
}
self.setTitle("")
}
I am stuck (could be simple/silly question) as how to parse my data from the context in the second controller?
Also, the didReceiveMessage works only the second time when I launch my ViewController where the sendMessage code is placed. Is it normal?
First, you might want to redeclare myData as this:
var myData: [String: Any] = ...
which makes it a bit simpler. Then, in the awake function, you’d go ahead like this:
if let myData = context as? [String: Any] {
if let myQrImage = myData["myQrValue"] as? Date {
...
Does this show you the right direction?

Getting the right dictionary from delegate methods parameters

I am wondering if this approach ensures that I am receiving the appropriate dictionary from the WCSessionDelegate method didReceiveMessage.
I am slightly confused with the uses of if let _, and have only come across the need to use this approach during this occurrence. What use cases is this approach for?
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
var dictionaryKey = "SomeKey"
if let _ = message[dictionaryKey] as? Bool { // receive sent over dictionary with a boolean value
// use dictionary information
}
}

Swift: User default values not being saved/ loaded when app is opened and closed

I have a global settings variable (SettingsVariable.settingOne) that is changed by the user during the apps runtime. I want to make the users change permanent, but at the moment every time the app is then reloaded it reverts to the original values e.g. user changes value to false, but then when the app is rerun the variables value changes back to the original value.
When the value is changed the following code is called (swift 2):
NSUserDefaults.standardUserDefaults().setBool(false, forKey: "settingOne")
and when the app is closed:
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
NSUserDefaults.standardUserDefaults().setBool(SettingsVariables.settingOne, forKey: "settingOne")
}
then once the app is opened again:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
NSUserDefaults.standardUserDefaults().boolForKey("settingOne")
return true
}
But the value of settingOne keeps reverting back to the default value of 'true' set originally within the application.
SettingsVariables.settingOne is contained within a struct:
import UIKit
struct SettingsVariables {
static var settingOne = true
}
Now Apple documentation suggests to use CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication) instead of calling UserDefaults.standard.synchronize() which is marked as a deprecated method.
You need to give synchronize to save NSUserDefaults.
So your code must be
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
NSUserDefaults.standardUserDefaults().setBool(SettingsVariables.settingOne, forKey: "settingOne")
NSUserDefaults.standardUserDefaults().synchronize()
}
You have to synchronize the values NSUserDefaults.standardUserDefaults().synchronize()
this what I use for getting element from NSUserDefaults , UserDataKey is just Enum
func userDataForKey(key: UserDataKey) -> String? {
let userDefaults = NSUserDefaults.standardUserDefaults()
if let value: AnyObject = userDefaults.objectForKey(key.rawValue) {
return value as? String
} else {
return nil
}
}
and this for saving
func setUserData(value: AnyObject?, key: UserDataKey) -> () {
let userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setObject(value, forKey: key.rawValue)
}

Using `InterfaceController` logic for `ComplicationController'

I'm doing an Apple Watch App, with a Complication.
I've got the WatchKit App part working great with this Ev class...
class Ev {
var evTColor:String
var evMatch:String
init(dataDictionary:Dictionary<String,String>) {
evTColor = dataDictionary["TColor"]!
evMatch = dataDictionary["Match"]!
}
class func newEv(dataDictionary:Dictionary<String,String>) -> Ev {
return Ev(dataDictionary: dataDictionary)
}
}
... and this InterfaceController
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
if let tColorValue = userInfo["TColor"] as? String, let matchValue = userInfo["Match"] as? String {
receivedData.append(["TColor" : tColorValue , "Match" : matchValue])
evs.append(Ev(dataDictionary: ["TColor" : tColorValue , "Match" : matchValue]))
doTable()
} else {
print("tColorValue and matchValue are not same as dictionary value")
}
}
func doTable() {
self.rowTable.setNumberOfRows(self.evs.count, withRowType: "rows")
for (index, evt) in evs.enumerate() {
if let row = rowTable.rowControllerAtIndex(index) as? TableRowController {
row.mLabel.setText(evt.evMatch)
row.cGroup.setBackgroundColor(colorWithHexString(evt.evTColor))
} else {
print("nope")
}
}
}
I'm having a hard time getting the same sort of thing to work in my Complication, any ideas?
I'm not sure if I can just use the same Ev code for my ExtensionDelegate, and then what exactly to put in my ComplicationController.
If I use the same Ev code in my ExtensionDelegate I'm getting a fatal error: use of unimplemented initializer init().
And in my ComplicationController I'm not sure how to go about best using the data I already have from InterfaceController to fill out the getCurrentTimelineEntryForComplication &getTimelineEntriesForComplication methods in ComplicationController.
Will post any extra code as needed, thanks!
EDIT:
Per a question, my data comes from CloudKit to the iPhone App (which I then pass to the Watch App via WCSession, so my problem is accessing that data in my Watch App for my Complication)
Instead of having your InterfaceController implement and receive the WCSession messages, I would set up a singleton class that receives those messages instead. That class can parse and organize your user info data from the WCSession. That singleton class can/will be accessible in your ComplicationController and your InterfaceController
Singletons are fairly easy to setup in swift:
class DataManager : WCSessionDelegate {
// This is how you create a singleton
static let sharedInstance = DataManager()
override init() {
super.init()
if WCSession.isSupported() {
self.watchConnectivitySession?.delegate = self
self.watchConnectivitySession?.activateSession()
}
}
// This is where you would store your `Ev`s once fetched
var dataObjects = [Ev]()
// This is the method that would fetch them for you
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
//parse your userInfoDictionary
self.dataObjects = evs
}
}
Then in your InterfaceController you can reference it using DataManager.sharedInstance.dataObjects to build your InterfaceController or ComplicationsController
The idea with a singleton is that you have a one global reference. DataManager only gets instantiated once and only once.

Where best to call updateApplicationContext using Watch Connectivity?

Several of the good blog posts detailing Watch Connectivity (http://www.kristinathai.com/watchos-2-tutorial-using-application-context-to-transfer-data-watch-connectivity-2/ and http://natashatherobot.com/watchconnectivity-application-context/) use simple app examples that send data to the watch when you tap on UI on the iPhone.
My app simply lists the data from my iPhone app, so I don't need to send data immediately, I just wanted to send it when the app loads or enters background...to this end I've made the updateApplicationContext in didFinishLaunching and didEnterBackground...however my dataSource delegate in my watch interface controllers are very spotting at getting triggered...particularly the glance only loads on the simulator and never on device. Is there a better time and place to push the info?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
WatchSessionManager.sharedManager.startSession()
do {
try WatchSessionManager.sharedManager.updateApplicationContext(["peopleDict" : peopleDict])
} catch {
print(error)
}
return true
}
func applicationDidEnterBackground(application: UIApplication) {
do {
try WatchSessionManager.sharedManager.updateApplicationContext(["peopleDict" : peopleDict])
} catch {
print(error)
}
}
below is my WatchSessionManager I used to call activiateSession in my extensionDelegate's appliciationDidFinishLaunching
import WatchConnectivity
protocol DataSourceChangedDelegate {
func dataSourceDidUpdate(dataSource: DataSource)
}
class WatchSessionManager: NSObject, WCSessionDelegate {
static let sharedManager = WatchSessionManager()
private override init() {
super.init()
}
private var dataSourceChangedDelegates = [DataSourceChangedDelegate]()
private let session: WCSession = WCSession.defaultSession()
func startSession() {
session.delegate = self
session.activateSession()
}
func addDataSourceChangedDelegate<T where T: DataSourceChangedDelegate, T: Equatable>(delegate: T) {
dataSourceChangedDelegates.append(delegate)
}
func removeDataSourceChangedDelegate<T where T: DataSourceChangedDelegate, T: Equatable>(delegate: T) {
for (index, indexDelegate) in dataSourceChangedDelegates.enumerate() {
if let indexDelegate = indexDelegate as? T where indexDelegate == delegate {
dataSourceChangedDelegates.removeAtIndex(index)
break
}
}
}
}
// MARK: Application Context
// use when your app needs only the latest information
// if the data was not sent, it will be replaced
extension WatchSessionManager {
// Receiver
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
dispatch_async(dispatch_get_main_queue()) { [weak self] in
self?.dataSourceChangedDelegates.forEach { $0.dataSourceDidUpdate(DataSource(data: applicationContext))}
}
}
}
As updateApplicationContext only stores the newest application context you can update it whenever you like. The watch will only get the newest data. There is no queue with old contexts.
On the watch side the most secure location to activate the session and set the WCSessionDelegate is in the ExtensionDelegate init method:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
override init() {
super.init()
WatchSessionManager.sharedManager.startSession()
}
...
}
Your Glance does not update because when the Glance is shown, applicationDidFinishLaunching is not being called (because the watch app is not launched when only the Glance is launched)

Resources