Swift WatchConnectivity app context as Dictionary - ios

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?

Related

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 URLSession completion Handler

I am trying to make an API call in my Swift project. I just started implementing it and i am trying to return a Swift Dictionary from the call.
But I think i am doing something wrong with the completion handler!
I am not able to get the returning values out of my API call.
import UIKit
import WebKit
import SafariServices
import Foundation
var backendURLs = [String : String]()
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
#IBOutlet var containerView : UIView! = nil
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
func getBackendURLs(completion: #escaping (NSArray) -> ()) {
let backend = URL(string: "http://example.com")
var json: NSArray!
let task = URLSession.shared.dataTask(with: backend! as URL) { data, response, error in
guard let data = data, error == nil else { return }
do {
json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSArray
completion(json)
} catch {
#if DEBUG
print("Backend API call failed")
#endif
}
}
task.resume()
}
func extractJSON(JSON : NSArray) -> [String : String] {
var URLs = [String : String]()
for i in (0...JSON.count-1) {
if let item = JSON[i] as? [String: String] {
URLs[item["Name"]! ] = item["URL"]!
}
}
return URLs
}
}
The first print() statements gives me the correct value, but the second is "nil".
Does anyone have a suggestion on what i am doing wrong?
Technically #lubilis has answered but I couldn't fit this inside a comment so please bear with me.
Here's your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
What will happen is the following:
viewDidLoad is called, backendURLs is nil
you call getBackendURLs, which starts on another thread in the background somewhere.
immediately after that your code continues to the outer print(backendURLs), which prints nil as backendURLs is still nil because your callback has not been called yet as getBackendURLs is still working on another thread.
At some later point your getBackendURLs finishes retrieving data and parsing and executes this line completion(json)
now your callback is executed with the array and your inner print(backendURLs) is called...and backendURLs now has a value.
To solve your problem you need to refresh your data inside your callback method.
If it is a UITableView you could do a reloadData() call, or maybe write a method that handles updating the UI for you. The important part is that you update the UI inside your callback, because you don't have valid values until then.
Update
In your comments to this answer you say:
i need to access the variable backendURLs right after the completionHandler
To do that you could make a new method:
func performWhateverYouNeedToDoAfterCallbackHasCompleted() {
//Now you know that backendURLs has been updated and can work with them
print(backendURLs)
//do what you must
}
In the callback you then send to your self.getBackendURLs, you invoke that method, and if you want to be sure that it happens on the main thread you do as you have figured out already:
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
DispatchQueue.main.async {
self.performWhateverYouNeedToDoAfterCallbackHasCompleted()
}
}
Now your method is called after the callback has completed.
As your getBackendURLs is an asynchronous method you can not know when it has completed and therefore you cannot expect values you get from getBackedURLs to be ready straight after calling getBackendURLs, they are not ready until getBackendURLs has actually finished and is ready to call its callback method.
Hope that makes sense.

No items from ExtensionDelegate array

I'm calling on my ExtensionDelegate from ComplicationController to give an array of evnts.
Seems to work fine calling ExtensionDelegate from InterfaceController, both of which are in my watch app.
But for some reason, I get 0 items in the evnts array when calling on my ExtensionDelegate from my ComplicationController.
Any ideas? Thanks!
ExtensionDelegate:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
static var evnts = [Evnt]()
ComplicationController:
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
// extEvnts = 0 somehow here
let extEvnts = ExtensionDelegate.evnts
This all works fine when I do it from my InterfaceController though:
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
if let tColorValue = userInfo["TeamColor"] as? String, let matchValue = userInfo["Matchup"] as? String {
receivedData.append(["TeamColor" : tColorValue , "Matchup" : matchValue])
ExtensionDelegate.evnts.append(Evnt(dataDictionary: ["TeamColor" : tColorValue , "Matchup" : matchValue]))
doTable()
} else {
print("matchValue are not same as dictionary value")
}
}
func doTable() {
let extEvnts = ExtensionDelegate.evnts
self.rowTable.setNumberOfRows(extEvnts.count, withRowType: "rows")
for (index, evt) in extEvnts.enumerate() {
if let row = rowTable.rowControllerAtIndex(index) as? TableRowController {
row.mLabel.setText(evt.eventMatch)
} else {
print("nope")
}
}
}
When you declare evnts, you've initialised it to an empty array ([Evnt]()).
When you access it from getCurrentTimelineEntryForComplication(complication: withHandler:), if nothing has modified the array, it will still be empty.
Inside session(session:didReceiveUserInfo:), you add items to the array, then immediately call doTable(), at which point ExtensionDelegate.evnts is not empty, as it contains the items you added just moments previously.
Given that you have no items when getCurrentTimelineEntryForComplication(complication: withHandler:) is being called, it would appear that this is happening before session(session:didReceiveUserInfo:) occurs.
If you want to make sure that you have data when getCurrentTimelineEntryForComplication(complication: withHandler:) is called, you should load some data before or at that point in the WatchKit application lifecycle.

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.

Issue with Google Analytics in Swift 2 or 3

I have a problem with Swift 2 (Swift 3) and Google Analytics.
This is the line with the problem:
tracker.send(GAIDictionaryBuilder.createScreenView().build())
Xcode tell's me:
Cannot invoke 'send' with an argument list of type '(NSMutableDictionary!)'
Update for Swift 3 (2016.10.19)
let tracker = GAI.sharedInstance().defaultTracker
let build = (GAIDictionaryBuilder.createScreenView().build() as NSDictionary) as! [AnyHashable: Any]
tracker?.send(build)
Still an ugly approach, let me know if there's an cleaner conversion.
Original
Same here, struggling to resolve tons of errors.
What I did (deprecated):
var build = GAIDictionaryBuilder.createAppView().build() as [NSObject : AnyObject]
tracker.send(build)
Edit (2015)
Thanks to #George Poulos. . Recently they updated the options, now createAppView is deprecated, should use createScreenView instead.
var build = GAIDictionaryBuilder.createScreenView().build() as [NSObject : AnyObject]
tracker.send(build)
In addition to the accepted answer:
Changed this:
tracker.send(GAIDictionaryBuilder.createEventWithCategory("UX", action: "User sign in", label: nil, value: nil).build())
To this:
tracker.send(GAIDictionaryBuilder.createEventWithCategory("UX", action: "User sign in", label: nil, value: nil).build() as [NSObject : AnyObject])
This might be a bit of an overkill, but I prefer creating a short extension and not need to type the castings every time:
In any swift file, paste the following code:
extension GAIDictionaryBuilder
{
func buildSwiftCompatible() -> [NSObject:AnyObject]
{
return self.build() as [NSObject:AnyObject]
}
}
Then you can call buildSwiftCompatible() instead of the usual build():
tracker.send(GAIDictionaryBuilder.createScreenView().buildSwiftCompatible())
Have fun.
This is a solution I came up with.. Maybe it could help some of you. It's a struct you need to instantiate in every UIViewController, but it helps with the boilerplate.
import UIKit
struct Analytics {
fileprivate let viewController: UIViewController
fileprivate let tracker = GAI.sharedInstance().defaultTracker
init (forScreen viewController: UIViewController) {
self.viewController = viewController
}
func startTracking () {
let screenView = GAIDictionaryBuilder.createScreenView().build() as NSDictionary
guard
let tracker = tracker,
let build = screenView as? [AnyHashable: Any]
else { return }
tracker.set(kGAIScreenName, value: String(describing: viewController))
tracker.send(build)
}
}
class HomeViewController: UIViewController {
lazy var analytics: Analytics = {
return Analytics(forScreen: self)
}()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear() {
super.viewWillAppear()
analytics.startTracking()
}
}
For swift 3:
let build:NSObject = GAIDictionaryBuilder.createScreenView().build()
tracker?.send(build as! [AnyHashable: Any])
let build = GAIDictionaryBuilder.createScreenView().build() as [NSObject : AnyObject]
tracker?.send(build)

Resources