How to update state from Phone view on WatchOS view? - ios

I'm using a companion app to authorize a user with a 3rd party service. Once authorized, I update a UserDefaults variable to true. On the companion app side, the view updates correctly and shows that the user has been authenticated. However, on the watch OS side the view does not update. Would I need to use the Watch Connectivity API and send a message to the watch to update the state? Or is there a simple way?
Phone App
struct AuthenticationView: View {
#State private var startingWebAuthenticationSession = false
#AppStorage("authorized") private var authorized = false
var body: some View {
Group {
if !authorized {
VStack {
Button("Connect", action: { self.startingWebAuthenticationSession = true })
.webAuthenticationSession(isPresented: $startingWebAuthenticationSession) {
WebAuthenticationSession(
url: URL(string: "https://service.com/oauth/authorize?scope=email%2Cread_stats&response_type=code&redirect_uri=watch%3A%2F%2Foauth-callback&client_id=\(clientId)")!,
callbackURLScheme: callbackURLScheme
) { callbackURL, error in
guard error == nil, let successURL = callbackURL else {
return
}
let oAuthCode = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == "code"}).first
guard let authorizationCode = oAuthCode?.value else { return }
let url = URL(string: "https://service.com/oauth/token")
var request = URLRequest(url: url!)
request.httpMethod = "POST"
let params = "client_id=\(clientId)&client_secret=\(clientSecret)&grant_type=authorization_code&code=\(authorizationCode)&redirect_uri=\(callbackURLScheme)://oauth-callback";
request.httpBody = params.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print("Error took place \(error)")
return
}
if let data = data, let response = String(data: data, encoding: .utf8) {
let accessTokenResponse: AccessTokenResponse = try! JSONDecoder().decode(AccessTokenResponse.self, from: response.data(using: .utf8)!)
let defaults = UserDefaults.standard
authorized = true
startingWebAuthenticationSession = false
defaults.set(accessTokenResponse.access_token, forKey: DefaultsKeys.accessToken) //TODO: Store securely
ConnectivityService.shared.send(authorized: true)
}
}
task.resume()
}
.prefersEphemeralWebBrowserSession(false)
}
}
}
else {
VStack {
Text("Authenticated!")
}
}
}
}
}
WatchOS
import SwiftUI
struct ConnectView: View {
#ObservedObject var connectivityService: ConnectivityService
var body: some View {
if !$connectivityService.authorized.wrappedValue {
VStack {
Text("Open the app on your primary device to connect.")
}
}
else {
//Some other view
}
}
}
EDIT:
Trying with Watch Connectivity API but the issue I'm experiencing is that when I authenticate from the phone, it'll take some time for the ConnectView to update the authorized variable. I know Watch Connectivity API doesn't update right away but at minimum I'd need some way for the watch to pick up that a secret access token has been retrieved and it can transition to the next view; whether that's through a shared state variable, UserDefaults, or whatever other mechanism.
Here is the ConnectivityService class I'm using:
import Foundation
import Combine
import WatchConnectivity
final class ConnectivityService: NSObject, ObservableObject {
static let shared = ConnectivityService()
#Published var authorized: Bool = false
override private init() {
super.init()
#if !os(watchOS)
guard WCSession.isSupported() else {
return
}
#endif
WCSession.default.delegate = self
WCSession.default.activate()
}
public func send(authorized: Bool, errorHandler: ((Error) -> Void)? = nil) {
guard WCSession.default.activationState == .activated else {
return
}
#if os(watchOS)
guard WCSession.default.isCompanionAppInstalled else {
return
}
#else
guard WCSession.default.isWatchAppInstalled else {
return
}
#endif
let authorizationInfo: [String: Bool] = [
DefaultsKeys.authorized: authorized
]
WCSession.default.sendMessage(authorizationInfo, replyHandler: nil)
WCSession.default.transferUserInfo(authorizationInfo)
}
}
extension ConnectivityService: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { }
func session(
_ session: WCSession,
didReceiveUserInfo userInfo: [String: Any] = [:]
) {
let key = DefaultsKeys.authorized
guard let authorized = userInfo[key] as? Bool else {
return
}
self.authorized = authorized
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
self.authorized = true
}
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) {
}
func sessionDidDeactivate(_ session: WCSession) {
WCSession.default.activate()
}
#endif
}
I tried doing these two lines but they have varying results:
WCSession.default.sendMessage(authorizationInfo, replyHandler: nil)
WCSession.default.transferUserInfo(authorizationInfo)
In the first line, XCode will say that no watch app could be found, even though I'm connected to both physical devices through XCode; launch phone first then watch. I believe the first one is immediate and the second is more of when the queue feels like it. Sometimes if I hard close the watch app, it'll pick up the state change in the authorized variable, sometimes it won't. Very frustrating inter-device communication.
UserDefaults doesn't pick up the access token value on the watch side. Maybe I have to use App Groups?
I do see this error on the Watch side:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
So I thought to try and encapsulate the self.authorized = authorized call into something like:
DispatchQueue.main.async {
self.authorized = authorized
}
But it didn't do anything as far as solving the immediate state change issue.

Related

How can I retry failed requests using alamofire?

I append failed requests to queue manager (contains array) in case of no connection
I'm presenting a custom pop-up with a retry button. When the retry button is pressed, I want to retry the requests that cannot be sent in the no connection state. There may be more than one request.
When I try the retryRequest method from Alamofire Session class, the task state of the request remains in the initilazed or finished state, but it must be resumed in order to send the request successfully, how can I solve this situation?
InterceptorInterface.swift
public func didGetNoInternetConnection() {
let viewModel = AppPopupViewModel(title: L10n.AppPopUp.areYouOffline, description: L10n.AppPopUp.checkInternetConnection, image: Images.noInternetConnection.image, firstButtonTitle: L10n.General.tryAgain, secondButtonTitle: nil, firstButtonAction: { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async {
self.retry()
}
}, secondButtonAction: nil, dismissCompletion: nil, dimColor: Colors.appGray.color.withAlphaComponent(0.8), showCloseButton: true, customView: nil)
DispatchQueue.main.async {
AppPopupManager.show(with: viewModel, completion: nil)
}
}
private func retry() {
guard let head = NetworkRequestStorage.shared.head else {
return
}
let request = head.request
let session = head.session
session.retryRequest(request, withDelay: nil)
}
APIInterceptor.swift
final class APIInterceptor: Alamofire.RequestInterceptor {
private let configure: NetworkConfigurable
private var lock = NSLock()
// MARK: - Initilize
internal init(configure: NetworkConfigurable) {
self.configure = configure
}
// MARK: - Request Interceptor Method
internal func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
lock.lock()
defer {
lock.unlock()
}
var urlRequest = urlRequest
if let token = self.configure.accessToken {
/// Set the Authorization header value using the access token.
urlRequest.setValue("Bearer " + token, forHTTPHeaderField: "Authorization")
}
// Set Accept-Language header value using language code
urlRequest.setValue(configure.languageCode, forHTTPHeaderField: "Accept-Language")
// Arrange Request logs for develope and staging environment
if let reachability = NetworkReachabilityManager(), !reachability.isReachable {
configure.didGetNoInternetConnection()
completion(.failure(APIClientError.networkError))
}
completion(.success(urlRequest))
}
// MARK: - Error Retry Method
internal func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
// Arrange Error logs for develope and staging environment
if let aError = error as? APIClientError, aError.statusCode == 400 { // no connection state
NetworkRequestStorage.shared.enqueue(request: request, session: session)
completion(.doNotRetryWithError(error))
} else {
request.retryCount <= configure.retryCount ? completion(.retry) : completion(.doNotRetryWithError(error))
}
}
}
If the request is successful or there is no connection error, I remove it from the NetworkRequestStoroge class.

Forwarding user's location to server when application is in the background (Swift/iOS)

I need to keep my server updated with user's location even when the app is in the background or terminated.
The location updating is working just fine and seems to wake the application as wanted.
My problem is regarding the forwarding of the user's location via a PUT request to the server.
I was able to go through the code with breakpoints and it goes well except that when I check with Charles if requests are going though, nothing appears.
Here is what I have so far:
API Client
final class BackgroundNetwork: NSObject, BackgroundNetworkInterface, URLSessionDelegate {
private let keychainStorage: Storage
private var backgroundURLSession: URLSession?
init(keychainStorage: Storage) {
self.keychainStorage = keychainStorage
super.init()
defer {
let sessionConfiguration = URLSessionConfiguration.background(withIdentifier: "backgroundURLSession")
sessionConfiguration.sessionSendsLaunchEvents = true
sessionConfiguration.allowsCellularAccess = true
backgroundURLSession = URLSession(configuration: sessionConfiguration,
delegate: self,
delegateQueue: nil)
}
}
func put<T: Encodable>(url: URL, headers: Headers, body: T) {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "PUT"
let authenticationToken: String? = try? keychainStorage.get(forKey: StorageKeys.authenticationToken)
if let authenticationToken = authenticationToken {
urlRequest.setValue(String(format: "Bearer %#", authenticationToken), forHTTPHeaderField: "Authorization")
}
headers.forEach { (key, value) in
if let value = value as? String {
urlRequest.setValue(value, forHTTPHeaderField: key)
}
}
do {
let jsonData = try JSONEncoder().encode(body)
urlRequest.httpBody = jsonData
} catch {
#if DEBUG
print("\(error.localizedDescription)")
#endif
}
backgroundURLSession?.dataTask(with: urlRequest)
}
}
AppDelegate
// ...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if launchOptions?[UIApplication.LaunchOptionsKey.location] != nil {
environment.locationInteractor.backgroundDelegate = self
_ = environment.locationInteractor.start()
}
return true
}
// ...
extension AppDelegate: LocationInteractorBackgroundDelegate {
func locationDidUpdate(location: CLLocation) {
taskId = UIApplication.shared.beginBackgroundTask {
UIApplication.shared.endBackgroundTask(self.taskId)
self.taskId = .invalid
}
environment.tourInteractor.updateLocationFromBackground(latitude: Float(location.coordinate.latitude),
longitude: Float(location.coordinate.longitude))
UIApplication.shared.endBackgroundTask(taskId)
taskId = .invalid
}
}
SceneDelegate (yes, the application is using SwiftUI and Combine and I target iOS 13 or later)
func sceneWillEnterForeground(_ scene: UIScene) {
if let environment = (UIApplication.shared.delegate as? AppDelegate)?.environment {
environment.locationInteractor.backgroundDelegate = nil
}
}
func sceneDidEnterBackground(_ scene: UIScene) {
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
appDelegate.environment.locationInteractor.backgroundDelegate = appDelegate
_ = appDelegate.environment.locationInteractor.start()
}
}
So basically, whenever my app goes in background, I set my delegate, restart the location updates and whenever an update comes, my interactor is called and a request is triggered.
According to breakpoints, eveything just works fine up to backgroundURLSession?.dataTask(with: urlRequest). But for some reason the request never gets fired.
I obviously checked Background Modes capabilities Location updates and Background fetch.
Any idea why ?
That’s correct, the line
backgroundURLSession?.dataTask(with: urlRequest)
does nothing. The way to do networking with a session task is to say resume, and you never say that. Your task is created and just thrown away. (I’m surprised the compiler doesn’t warn about this.)

WatchConnectivity - transerUserInfo is sending, but not receiving data

I've been making an app that uses WatchConnectivity to transfer a simple struct from the Apple Watch to iPhone, and have been running into some troubles. Sending is perfectly fine, and both devices are reachable and activated on the same session, but the iPhone never seems to receive the struct I send it.
Here's my current code. I decided to use transferUserInfo to allow background transfer of data.
The struct:
struct myDataList {
var xAcc: [Int]
var timestamps: [Int]
}
Watch (Sending):
func sendTestData(data:myDataList) {
print("sending file to iphone")
if WCSession.default.activationState == WCSessionActivationState.activated && WCSession.isSupported() && WCSession.default.isReachable {
WCSession.default.transferUserInfo(["Data" : data])
}
else {
print("Could not send")
}
}
iPhone (Receiving):
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
print("received something")
DispatchQueue.main.async {
if let data = userInfo["Data"] as? myDataList {
for (acc,time) in zip(data.xAcc,data.timestamps){
let dataLine: String = "\(acc),\(time)\n"
self.appendToFile(file: "data", data: dataLine)
}
}
}
}
On both devices I've started a session like so:
if WCSession.isSupported() {
WCSession.default.delegate = self
WCSession.default.activate()
}
I've tested the other functions to write to file/etc and they are working individually. I'd appreciate feedback and advice on how to resolve this. Cheers!

Where are My 421 Memory Leaks Coming from?

I am using Apple's Instruments tool to check out the current progress of my application and manage any leaks early. I seem to have a lot of leaks, but I cannot figure out where they are coming from.
In my application, I have a SignInOperation which is a subclass of Operation. It also conforms to URLSessionDataDelegate so that it can handle my requests without needing to use completion handlers. For example, when adding an instance of SignInOperation to an OperationQueue instance, the operation that performs updates to the UI can just check the error and user properties on the SignInOperation and handle UI updates accordingly since it will have the SignInOperation instance as a dependency.
The class follows:
import UIKit
/// Manages a sign-in operation.
internal final class SignInOperation: Operation, URLSessionDataDelegate {
// MARK: - Properties
/// An internal flag that indicates whether the operation is currently executing.
private var _executing = false
/// An internal flag that indicates wheterh the operation is finished.
private var _finished = false
/// The received data from the operation.
private var receivedData = Data()
/// The data task used for sign-in.
private var sessionTask: URLSessionDataTask?
/// The URL session that is used for coordinating tasks used for sign-in.
private var localURLSession: URLSession { return URLSession(configuration: localConfiguration, delegate: self, delegateQueue: nil) }
/// The configuration used for configuring the URL session used for sign-in.
private var localConfiguration: URLSessionConfiguration { return .ephemeral }
/// The credentials used for user-sign-in.
private var credentials: UserCredentials
/// The current user.
internal var currentUser: User?
/// The error encountered while attempting sign-in.
internal var error: NetworkRequestError?
/// The cookie storage used for persisting an authentication cookie.
internal var cookieStorage: HTTPCookieStorage?
/// A Boolean value indicating whether the operation is currently executing.
override internal(set) var isExecuting: Bool {
get { return _executing }
set {
willChangeValue(forKey: "isExecuting")
_executing = newValue
didChangeValue(forKey: "isExecuting")
}
}
/// A Boolean value indicating whether the operation has finished executing its task.
override internal(set) var isFinished: Bool {
get { return _finished }
set {
willChangeValue(forKey: "isFinished")
_finished = newValue
didChangeValue(forKey: "isFinished")
}
}
/// A Boolean value indicating whether the operation executes its task asynchronously.
override var isAsynchronous: Bool { return true }
// MARK: - Initialization
/// Returns an instane of `SignInOperation`.
/// - parameter credentials: The credentials for user-sign-in.
init(credentials: UserCredentials, cookieStorage: HTTPCookieStorage = CookieStorage.defaultStorage) {
self.credentials = credentials
self.cookieStorage = cookieStorage
super.init()
localURLSession.configuration.httpCookieAcceptPolicy = .never
}
// MARK: - Operation Lifecycle
override func start() {
if isCancelled {
isFinished = true
return
}
isExecuting = true
let request = NetworkingRouter.signIn(credentials: credentials).urlRequest
sessionTask = localURLSession.dataTask(with: request)
guard let task = sessionTask else { fatalError("Failed to get task") }
task.resume()
}
// MARK: - URL Session Delegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
if isCancelled {
isFinished = true
sessionTask?.cancel()
return
}
guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { fatalError("Could not determine status code") }
setError(from: statusCode)
completionHandler(disposition(from: statusCode))
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if isCancelled {
guard let task = sessionTask else { fatalError("Failed to get task") }
task.cancel()
return
}
receivedData.append(data)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
defer { isFinished = true }
if isCancelled {
guard let task = sessionTask else { fatalError("Failed to get task") }
task.cancel()
}
if let statusCode = (task.response as? HTTPURLResponse)?.statusCode { setError(from: statusCode) } else if let taskError = error as? NSError { setError(from: taskError) }
if self.error == nil {
guard let taskResponse = task.response else { fatalError("Invalid response") }
setAuthenticationCookie(from: taskResponse)
processData()
}
}
// MARK: - Helpers
/// Handles the processing of the data received from the data task.
private func processData() {
currentUser = UserModelCreator.user(from: receivedData)
}
/// Handles the persistence of the returned cookie from the request's response.
private func setAuthenticationCookie(from response: URLResponse) {
guard let storage = cookieStorage else { fatalError() }
let cookiePersistenceManager = ResponseCookiePersistenceManger(storage: storage)
cookiePersistenceManager.removePreviousCookies()
guard let httpURLResponse = response as? HTTPURLResponse else { fatalError("Invalid response type") }
if let cookie = ResponseCookieParser.cookie(from: httpURLResponse) {cookiePersistenceManager.persistCookie(cookie: cookie) }
}
/// Handles the return of a specified HTTP status code.
/// - parameter statusCode: The status code.
private func setError(from statusCode: Int) {
switch statusCode {
case 200: error = nil
case 401: error = .invalidCredentials
default: error = .generic
}
}
/// Returns a `URLResponse.ResponseDisposition` for the specified HTTP status code.
/// - parameter code: The status code.
/// - Returns: A disposition.
private func disposition(from code: Int) -> URLSession.ResponseDisposition {
switch code {
case 200: return .allow
default: return .cancel
}
}
/// Handles the return of an error from a network request.
/// - parameter error: The error.
private func setError(from error: NSError) {
switch error.code {
case Int(CFNetworkErrors.cfurlErrorTimedOut.rawValue): self.error = .requestTimedOut
case Int(CFNetworkErrors.cfurlErrorNotConnectedToInternet.rawValue): self.error = .noInternetConnection
default: self.error = .generic
}
}
}
Then, to see if everything works, I call the operation in viewDidAppear:, which results in all of the expected data being printed:
import UIKit
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let credentials = UserCredentials(emailAddress: "xxxxxxx#xxxx.xx", password: "xxxxxxxxxxxxxxxxxx")
let signInOp = SignInOperation(credentials: credentials)
let printOperation = Operation()
printOperation.addDependency(signInOp)
printOperation.completionBlock = {
if let error = signInOp.error { return print("\n====> Sign-in Error: \(error.message)\n") }
if let user = signInOp.currentUser { print("\n====> User: \(user)\n") }
}
let queue = OperationQueue()
queue.addOperations([signInOp, printOperation], waitUntilFinished: false)
}
}
However, when using the Leaks profiler in Instruments, I get some alarming data.
I don't really know where to start here. When I click on any of the detected leaks, I am not taken to my code that the leak originates from. I have watched a few tutorials and read Apple's documentation, but I am stuck trying to figure out where the leaks are coming from. It seems like a ridiculous amount/
I don't see anywhere in my code where I have strong reference cycles, so I am asking for some help with trying to figure out how to resolve the 421 detected leaks.
It turns out that I do have two strong reference cycles, which are the two following properties in my SignInOperation subclass: sessionTask & localURLSession.
After making these properties weak, I no longer have leaks detected:
/// The URL session that is used for coordinating tasks used for sign-in.
private weak var localURLSession: URLSession { return URLSession(configuration: localConfiguration, delegate: self, delegateQueue: nil) }
/// The configuration used for configuring the URL session used for sign-in.
private weak var localConfiguration: URLSessionConfiguration { return .ephemeral }

Passing data to Apple Watch app

I am trying to pass data from my app into my Apple Watch app. Basically, I am using the same method as I used for creating the today widget and so I am passing data through NSUserDefaults.
The problem is, that when I run my app, the data does not update the labels in the Watch app as I would expect it to.
Here is what I have...
override init(context: AnyObject?) {
// Initialize variables here.
super.init(context: context)
// Configure interface objects here.
NSLog("%# init", self)
var defaults = NSUserDefaults(suiteName: "group.AffordIt")
var totalBudgetCalculation = ""
if (defaults!.stringForKey("totalBudgetWidget") != nil) {
println("Worked")
totalBudgetCalculation = defaults!.stringForKey("totalBudgetWidget")!
initialBudgetLabel.setText("Initial: \(totalBudgetCalculation)")
}
var currentBudgetCalculation = ""
if (defaults!.stringForKey("currentBudgetWidget") != nil) {
currentBudgetCalculation = defaults!.stringForKey("currentBudgetWidget")!
currentBudgetLabel.setText("Current: \(currentBudgetCalculation)")
}
}
I tried putting this code in willActivate(), however that doesn't seem to make a difference.
Anyone know where I am going wrong?
This applies to OS 1 only. See below for better answers.
I got it working using your method. I guess there's a couple of things you can check:
1) Are you synchronising the defaults after you set the value:
defaults?.synchronize();
NSLog("%# ", defaults?.dictionaryRepresentation())
2) Have you enabled the App Group in both your app and your extension?
3) Are you using the correctly named app group when constructing the NSDefaults? For example, I use:
NSUserDefaults(suiteName: "group.com.brindysoft.MyWatch");
Once all that's set up I run the app, set the value in the defaults, then run the glance target which reads the value from the default and that seems to work!
Still stuck? check your app groups in your apple account
The accepted answer applies to apple watch os 1. See NSUserDefaults not working on Xcode beta with Watch OS2
For OS2 - you will need to use the WatchConnectivity frameworks and implement the WCSessionDelegate.
import WatchConnectivity
import WatchKit
#available(iOS 9.0, *)
var alertDelegate:HomeIC? = nil
public class WatchData: NSObject,WCSessionDelegate {
var session = WCSession.defaultSession()
//
class var shared: WatchData {
struct Static {
static var onceToken: dispatch_once_t = 0
static var instance: WatchData? = nil
}
dispatch_once(&Static.onceToken) {
Static.instance = WatchData()
}
return Static.instance!
}
public func session(session: WCSession, didReceiveFile file: WCSessionFile){
print(__FUNCTION__)
print(session)
}
public func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
print(__FUNCTION__)
print(session)
alertDelegate?.showMessage("didReceiveApplicationContext")
}
public func sessionReachabilityDidChange(session: WCSession){
print(__FUNCTION__)
print(session)
print("reachability changed:\(session.reachable)")
let text = session.reachable ? "reachable" : "unreachable"
alertDelegate?.showMessage(text)
}
public func sessionWatchStateDidChange(session: WCSession) {
print(__FUNCTION__)
print(session)
print("reachable:\(session.reachable)")
// alertDelegate?.showMessage("sessionWatchStateDidChange")
if !session.receivedApplicationContext.keys.isEmpty {
alertDelegate?.showMessage(session.receivedApplicationContext.description)
}
}
public func session(session: WCSession, didReceiveMessageData messageData: NSData){
if !session.receivedApplicationContext.keys.isEmpty {
alertDelegate?.showMessage(session.receivedApplicationContext.description)
}
}
public func session(session: WCSession, didReceiveMessage message: [String : AnyObject]){
print(__FUNCTION__)
if let data = message["data"] {
alertDelegate?.showMessage(data as! String)
return
}
}
public func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
print(__FUNCTION__)
if let data = message["data"] {
alertDelegate?.showMessage(data as! String)
return
}
guard message["request"] as? String == "showAlert" else {return}
}
public func activate(){
if WCSession.isSupported() { // it is supported
session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
print("watch activating WCSession")
} else {
print("watch does not support WCSession")
}
if(!session.reachable){
print("not reachable")
return
}else{
print("watch is reachable")
}
}
}
Sample Usage
class HomeIC: WKInterfaceController {
// MARK: Properties
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
// Initialize the `WCSession`.
WatchData.shared.activate()
alertDelegate = self
}
internal func showMessage(msg:String){
let defaultAction = WKAlertAction(title: msg, style: WKAlertActionStyle.Default) { () -> Void in }
let actions = [defaultAction]
self.presentAlertControllerWithTitle( "Info", message: "", preferredStyle: WKAlertControllerStyle.Alert, actions: actions)
}
}
in my iphone code / I can invoke sharing data here
if #available(iOS 9.0, *) {
WatchData.shared.sendInbox()
} else {
// Fallback on earlier versions
}
And somewhere else I have another discrete singleton for watch data session.
#available(iOS 9.0, *)
public class WatchData: NSObject,WCSessionDelegate {
var session = WCSession.defaultSession()
var payload:String = ""
class var shared: WatchData {
struct Static {
static var onceToken: dispatch_once_t = 0
static var instance: WatchData? = nil
}
dispatch_once(&Static.onceToken) {
Static.instance = WatchData()
}
return Static.instance!
}
public func sessionReachabilityDidChange(session: WCSession){
print(__FUNCTION__)
print(session)
print("reachability changed:\(session.reachable)")
if (session.reachable){
}
}
public func sessionWatchStateDidChange(session: WCSession) {
print(__FUNCTION__)
print(session)
print("reachable:\(session.reachable)")
}
public func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
print(__FUNCTION__)
guard message["request"] as? String == "showAlert" else {return}
guard let m = message["m"] as? String else { return }
print("msg:",m)
}
public func sendInbox(){
if (!session.reachable){
if WCSession.isSupported() { // it is supported
session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
print("iphone activating WCSession")
} else {
print("iphone does not support WCSession")
}
session.activateSession()
}
if(session.paired){
if(session.watchAppInstalled){
print("paired | watchAppInstalled")
}
}else{
print("not paired | or no watchAppInstalled")
}
if(!session.reachable){
print("not reachable")
return
}else{
/*let transfer:WCSessionUserInfoTransfer = (session.transferUserInfo(["data" : "Test2"]) as WCSessionUserInfoTransfer?)!
if(transfer.transferring){
print("-> iphone")
}else{
print("!-> iphone")
}*/
session.sendMessage(["data" :"test"],
replyHandler: { reply in
},
errorHandler: { error in
print(error)
})
}
}
}
Refer to sample watch os2 app
https://github.com/shu223/watchOS-2-Sampler/tree/20eeebeed66764d0814603e97d3aca5933236299
As #johndpope said, shared NSUserDefaults no longer work on WatchOS2.
I'm posting a simplified solution that's not as full featured as john's but will get the job done in most cases.
In your iPhone App, follow these steps:
Pick find the view controller that you want to push data to the Apple Watch from and add the framework at the top.
import WatchConnectivity
Now, establish a WatchConnectivity session with the watch and send some data.
if WCSession.isSupported() { //makes sure it's not an iPad or iPod
let watchSession = WCSession.defaultSession()
watchSession.delegate = self
watchSession.activateSession()
if watchSession.paired && watchSession.watchAppInstalled {
do {
try watchSession.updateApplicationContext(["foo": "bar"])
} catch let error as NSError {
print(error.description)
}
}
}
Please note, this will NOT work if you skip setting the delegate, so even if you never use it you must set it and add this extension:
extension MyViewController: WCSessionDelegate {
}
Now, in your watch app (this exact code works for Glances and other watch kit app types as well) you add the framework:
import WatchConnectivity
Then you set up the connectivity session:
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
let watchSession = WCSession.defaultSession()
watchSession.delegate = self
watchSession.activateSession()
}
and you simply listen and handle the messages from the iOS app:
extension InterfaceController: WCSessionDelegate {
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
print("\(applicationContext)")
dispatch_async(dispatch_get_main_queue(), {
//update UI here
})
}
}
That's all there is to it.
Items of note:
You can send a new applicationContext as often as you like and it
doesn't matter if the watch is nearby and connected or if the watch
app is running. This delivers the data in the background in an
intelligent way and that data is sitting there waiting when the
watch app is launched.
If your watch app is actually active and running, it should receive
the message immediately in most cases.
You can reverse this code to have the watch send messages to the
iPhone app the same way.
applicationContext that your watch app receives when it is viewed will ONLY be the last message you sent. If you sent 20 messages before the watch app is viewed, it will ignore the first 19 and handle the 20th one.
For doing a direct/hard connection between the 2 apps or for background file transfers or queued messaging, check out the WWDC video.
Another way to communicate between the app and the watch is via wormhole:
https://github.com/mutualmobile/MMWormhole
Send:
[self.wormhole passMessageObject:#{#"titleString" : title}
identifier:#"messageIdentifier"];
id messageObject = [self.wormhole messageWithIdentifier:#"messageIdentifier"];
Recieve:
[self.wormhole listenForMessageWithIdentifier:#"messageIdentifier"
listener:^(id messageObject) {
// Do Something
}];
Just use watch connectivity for communicate between these two platform you can read more about this in apple document
https://developer.apple.com/documentation/watchconnectivity

Resources