Code working in AppDelegate but not in any other class - ios

I am using socket.io client for swift. When I declare and connect the socket and use its commands in AppDelegate, things work fine. But as soon as I move this stuff from AppDelegate to another class (making socket a global variable) and call the function of that class from AppDelegate, socket errors start to appear (the description of errors is really irrelevant to this topic. I had a discussion regarding that in Socket.io issues)
The question I wanna ask is this happening? I have used
deinit {
NSLog("CCNOTIFICATIONS RESOURCES GOT DE INITIALIZED")
}
in that file but this never gets called.
According to my knowledge, AppDelegate is a delegate which gets called when there is some change in app's state i-e its launches or goes to background/foreground. And also that it is not a good practice to mess huge amount of code in AppDelegate.
Apparently there is no difference in scope of both files then why is this happening?
Please correct me if I am wrong in my understanding.
How this error problem can be solved? Or least what other approach I can try to get to root of this problem?
Any help would be appreciated.
UPDATE:
Here is the code:
When I write it in AppDelegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
public static var socket = SocketIOClient(socketURL: URL(string: “URL”)!, config: [.log(true), .compress])
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
AppDelegate.socket.connect()
AppDelegate.socket.on("connect") {data, ack in
print("Connected successfully")
}
AppDelegate.socket.on("book-adventure-provider") {data, ack in
print("BOOKING CAME")
print(data)
}
return true
}
When I write it in other file:
CCNotifications:
public class CCNotifications : INotifications
{
public var socket = SocketIOClient(socketURL: URL(string: “URL”)!, config: [.log(true), .compress, .forcePolling(true), .forceNew(true)])
public func sendBookingRequest(adventureId: String, success: #escaping () -> Void, error: #escaping CErrorCallback) {
if (ConnectionStatus.isConnected() == false)
{
let errorObj = CError()
errorObj.message = "No internet connection"
errorObj.code = ClientConstants.connectionErrorCode
error(errorObj)
}
else
{
let jwtToken = KeychainWrapper.standard.string(forKey: "jwtToken")
let data = ["adventure_id": adventureId, "jwt": jwtToken]
socket.emit("book-adventure", data)
socket.on("book-adventure-seeker") { data, ack in
print(data)
}
}
}
}
CuufyClient:
public class CuufyClient : IClient {
public var notifications: INotifications? = nil
public static var Notifications: INotifications? { get { return CuufyClient.client?.notifications}}
//region Actions
public static func Initialize(completion: ((_ error: CError?) -> Void)?) {
CuufyClient.client?.notifications = CCNotifications ()
completion? (nil)
}
//endregion
}
Finally calling it as:
CuufyClient.Notifications?.funcWhichIWannaCall.
Note:
Both ways work. But when I write code in CCNotifications and run it, at sometimes it starts giving socket error: Session id unknown and then connects itself again and again.
UPDATE 2:
A line before the socket error in the logs I have observed this error:
2017-12-15 15:45:34.133480+0500 TestTarget[5332:230404] TIC Read Status [1:0x60000017f440]: 1:57
Upon searching this error I got to know that in Xcode 9, this error shows that TCP connection has closed. When I opened this issue on socket.io branch the specialist said:
This library operates on a fairly high level above the underlying
networking. It relies on either URLSession or Starscream to tell it
when the connection is lost.
Can anyone help me regarding URLSession that why TCP is getting closed as I am fairly new to iOS.

First to say, your approach of keeping the AppDelegate thin is a good idea.
Regarding the deinit: This function will be called when the object ceases to exist. Since global variables will life "forever" - as long as they are assigned to a different object - they will be cleaned up only at program termination (not: background mode etc.) Therefore, you will never see the deinit call.
Your thoughts about the life cycle are correct, there shouldn't be a difference if you have done everything correctly - which I cannot judge here because you didn't show any code of how and when that global socket is created/initialized etc.
Regarding the updated question: Unfortunatly, I don't know the socket.io library. But having just a short look, your code differs slightly between the AppDelegate and the other version. It might be a timeing issue - just check (and maybe fix) a few things:
When do you call connect()?
You might want to first set up all the ...on()-Handlers before connecting or calling emit; otherwise the server might respond before everything has set up correctly
Check if you might set up the ...on()-Handlers only one, centrally, and not with every sendBookingRequest-call
What is jwtToken? Is it an API token for you application, or some kind of seesion token? When do you store the value, and why is it stored in the keychain? Might its value change during requests/responses?

You should not place socket event listener in sendBookingRequest method.
You just need to bind a socket event listener once.
socket.on("book-adventure-seeker") { data, ack in
print(data)
}
so, you can place it in on connect event.
AppDelegate.socket.on("connect") {data, ack in
print("Connected successfully")
}

Related

iOS 14 How to trigger Local Network dialog and check user answer?

I've seen this Q/A What triggers, but it's not what I want. I also read this Network privacy permission check, but there is no answer. I also search for any methods or classes which can help me here: Network, but no luck again.
There is a new dialog for the Local Network authorization, where user can Allow/Don't Allow "to find and connect to devices on your local network".
But I'm struggling to find any API for how to trigger this popup and how to check is access granted or not(for example in AVCapture, I can check the authorization status for AVMediaType).
Thank you!
I found a way to trigger the prompt, receive a callback of the user's selection, and detect if the user has previously allowed or denied the prompt if it already appeared.
To trigger the permission we use a service discovery API. When the user declines or previously declined we receive an error.
It doesn't indicate if the permission was granted, so we also published a network service that returns success if the permission has been granted.
By combining the 2 into a single component, we can trigger the prompt and get an indication of approval or decline: Until we receive success from the network service or error from the service discovery we assume that the permission is still pending.
import Foundation
import Network
#available(iOS 14.0, *)
public class LocalNetworkAuthorization: NSObject {
private var browser: NWBrowser?
private var netService: NetService?
private var completion: ((Bool) -> Void)?
public func requestAuthorization(completion: #escaping (Bool) -> Void) {
self.completion = completion
// Create parameters, and allow browsing over peer-to-peer link.
let parameters = NWParameters()
parameters.includePeerToPeer = true
// Browse for a custom service type.
let browser = NWBrowser(for: .bonjour(type: "_bonjour._tcp", domain: nil), using: parameters)
self.browser = browser
browser.stateUpdateHandler = { newState in
switch newState {
case .failed(let error):
print(error.localizedDescription)
case .ready, .cancelled:
break
case let .waiting(error):
print("Local network permission has been denied: \(error)")
self.reset()
self.completion?(false)
default:
break
}
}
self.netService = NetService(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100)
self.netService?.delegate = self
self.browser?.start(queue: .main)
self.netService?.publish()
}
private func reset() {
self.browser?.cancel()
self.browser = nil
self.netService?.stop()
self.netService = nil
}
}
#available(iOS 14.0, *)
extension LocalNetworkAuthorization : NetServiceDelegate {
public func netServiceDidPublish(_ sender: NetService) {
self.reset()
print("Local network permission has been granted")
completion?(true)
}
}
How to use:
Add LocalNetworkAuthorization class to your project
Open .plist file and add "_bonjour._tcp", "_lnp._tcp.", as a values under "Bonjour services"
Call requestAuthorization() to trigger the prompt or get the authorization status if it already been approved/denied
I did open DTS request and had conversion with Apple support team. Here is some important parts which I included below.
How to check is access granted or not
From support team:
For know, there is no such an API to check user permission.
From support team:
If the user declines, the connection fails. Exactly how it fails
depends on the network API you’re using and how you use that API.
By default the connection will fail with NSURLErrorNotConnectedToInternet.
If you set waitsForConnectivity on the session configuration, the request will wait for things to improve. In that case you’ll receive
the -URLSession:taskIsWaitingForConnectivity: delegate callback to
tell you about this. If the user changes their mind and enables local
network access, the connection will then go through.
Unfortunately there’s no direct way to determine if this behaviour is
the result of a local network privacy restriction or some other
networking failure.
How to trigger this popup
From support team:
the problem here is that the local network permission alert is
triggered by outgoing traffic and you do not generate any outgoing
traffic. The only way around this is to generate some dummy outgoing
traffic in order to trigger this alert.
I’ve seen other developers in this situation and the absence of a
direct API to trigger the local network permission alert is quite
annoying. I encourage you to file a bug about this.
I’ve been discussing this issue with the local network privacy team
and our current advice for apps in your situation — that is, apps that
want to receive broadcasts but don’t send any local network traffic —
is as follows:
The system should do a better job of handling this. We’re tracking that as a bug rdar://problem/67975514. This isn’t fixed in the
current iOS 14.2b1 release but you should continue to test with iOS
beta seeds as they are released.
In the meantime you can force the local network privacy alert to show by sending a message. We specifically recommend that you send a
message that’s roughly equivalent to the message you’re trying to
receive, so in your case that means sending an IPv4 UDP broadcast.
UPDATE
For iOS 14.2 - prompt is received for inbound traffic FIXED. Because of this you don't need below example for simulating traffic to triggering prompt.
Here is class for dummy outgoing traffic simulation:
example
That traffic will never leave the iOS device and thus, even if the
interface is asleep, it won’t wake it up. And even if it did wake up
the interface, the cost of that is trivial because you’re not doing it
over and over again, just once in order to trigger the local network
privacy alert.
In my case it was accessing this variable for some internal device statistics:
ProcessInfo.processInfo.hostName
Accessing this variable caused the alert to appear. If it doesn't cover your case perhaps you can search source code for some references around the local network/host.
Because there is no API that directly returns your local network access state you can use next approach with publishing your Bonjour service and it returns the right result if access to local network was already set for your app (on app start e.g.). The approach causes the alert to appear as well but returns false before you select any button so to get the right result you should put this check to applicationDidBecomeActive and it will give the correct state after local network alert is disappeared and you return to your app.
class getLocalNetworkAccessState : NSObject {
var service: NetService
var denied: DispatchWorkItem?
var completion: ((Bool) -> Void)
#discardableResult
init(completion: #escaping (Bool) -> Void) {
self.completion = completion
service = NetService(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100)
super.init()
denied = DispatchWorkItem {
self.completion(false)
self.service.stop()
self.denied = nil
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: denied!)
service.delegate = self
self.service.publish()
}
}
extension getLocalNetworkAccessState : NetServiceDelegate {
func netServiceDidPublish(_ sender: NetService) {
denied?.cancel()
denied = nil
completion(true)
}
func netService(_ sender: NetService, didNotPublish errorDict: [String : NSNumber]) {
print("Error: \(errorDict)")
}
}
How to use:
getLocalNetworkAccessState { granted in
print(granted ? "granted" : "denied")
}
NOTE: Don't forget to set NSLocalNetworkUsageDescription and add "_lnp._tcp." to NSBonjourServices in your Info.plist.
UPDATE
There is the second approach that works similar the code from above but can wait for an user's answer by checking an application state and then returns a valid access state for Local Network Privacy:
class LocalNetworkPrivacy : NSObject {
let service: NetService
var completion: ((Bool) -> Void)?
var timer: Timer?
var publishing = false
override init() {
service = .init(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100)
super.init()
}
#objc
func checkAccessState(completion: #escaping (Bool) -> Void) {
self.completion = completion
timer = .scheduledTimer(withTimeInterval: 2, repeats: true, block: { timer in
guard UIApplication.shared.applicationState == .active else {
return
}
if self.publishing {
self.timer?.invalidate()
self.completion?(false)
}
else {
self.publishing = true
self.service.delegate = self
self.service.publish()
}
})
}
deinit {
service.stop()
}
}
extension LocalNetworkPrivacy : NetServiceDelegate {
func netServiceDidPublish(_ sender: NetService) {
timer?.invalidate()
completion?(true)
}
}
// How to use
LocalNetworkPrivacy().checkAccessState { granted in
print(granted)
}
ObjC
You can use swift code without rewriting to ObjC and to do that just add swift file to your project and call checkAccessState directly (the function must be marked with #objc):
#import "YourProjectName-Swift.h" // import swift classes to objc
...
LocalNetworkPrivacy *local = [LocalNetworkPrivacy new];
[local checkAccessStateWithCompletion:^(BOOL granted) {
NSLog(#"Granted: %#", granted ? #"yes" : #"no");
}];
Apple has (late September 2020) published a Local Network Privacy FAQ which answers this, although it does seem that further changes to make this easier are likely.
There are Swift and Objective-C code examples for how to trigger the prompt by a workaround:
Currently there is no way to explicitly trigger the local network
privacy alert (r. 69157424). However, you can bring it up implicitly
by sending dummy traffic to a local network address. The code below
shows one way to do this. It finds all IPv4 and IPv6 addresses
associated with broadcast-capable network interfaces and sends a UDP
datagram to each one. This should trigger the local network privacy
alert, assuming the alert hasn’t already been displayed for your app.
And as for how to check result, keep your eye on this FAQ answer which says:
If your goal is to connect to a local network address using
NWConnection then, starting with iOS 14.2 beta, you can use the
unsatisfied reason property.
It can be triggered by sending dummy request with TCP IP socket. This code works perfectly for Flutter iOS app using Socket and IP address of the device itself:
import 'package:network_info_plus/network_info_plus.dart';
import 'dart:io';
try{
var deviceIp = await NetworkInfo().getWifiIP();
Duration? timeOutDuration = Duration(milliseconds: 100);
await Socket.connect(deviceIp, 80, timeout: timeOutDuration);
} catch (e) {
print(
'Exception..');
}
Another workaround to consider if you're making a local network request with URLSession and would like the request to wait for the user to consent to the dialog is to set the waitsForConnectivity flag of URLSessionConfiguration to true:
Find:
URLSession.shared.dataTask(...)
Replace with:
// Default config
let config = URLSessionConfiguration.default
// Wait for user to consent to local network access
if #available(iOS 11.0, *) {
config.waitsForConnectivity = true
}
// Execute network request
let task = URLSession(configuration: config).dataTask(...)
This will cause the request to hang until the dialog is either accepted or declined.
This works on (at least) iOS 16.
First create a MCNearbyServiceAdvertiser and MCNearbyServiceBrowser. Then, the popup appears when you start these 'services'; see start() in the code below.
Perhaps starting one of the too is sufficient too; I simply did both together, because that's what I needed.
class Connector : NSObject, ObservableObject
{
#Published var peers = [MCPeerID]()
#Published var event: String?
private let serviceType = "app"
private let peerId = MCPeerID(displayName: UIDevice.current.name)
private let serviceAdvertiser: MCNearbyServiceAdvertiser
private let serviceBrowser: MCNearbyServiceBrowser
private let session: MCSession
private let log = Logger()
override init()
{
session = MCSession(peer: peerId, securityIdentity: nil, encryptionPreference: .none)
serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerId,
discoveryInfo: ["event" : "hello"],
serviceType: serviceType)
serviceBrowser = MCNearbyServiceBrowser(peer: peerId, serviceType: serviceType)
super.init()
session.delegate = self
serviceAdvertiser.delegate = self
serviceBrowser.delegate = self
}
deinit
{
serviceAdvertiser.stopAdvertisingPeer()
serviceBrowser.stopBrowsingForPeers()
}
func start()
{
serviceAdvertiser.startAdvertisingPeer()
serviceBrowser.startBrowsingForPeers()
}
}
See a bit more code here
See other answers here for how to check if the user granted this permission.
For reference, my solution to that problem is here. It is optimized for the use with SwiftUI where it can be used as an ObservableObject.
It bases on #TalSahar's approach, but also retries publishing the net service when it fails. This covers the case of the user granting access while the app is running:
browser.stateUpdateHandler = { [weak self] state in
os_log("NWBrowser status update: %#", log: OSLog.default, type: .debug, "\(state)")
guard let self else { return }
switch state {
case .failed(_):
self.service?.publish()
case .waiting(_):
self.status = .denied
default:
break
}
}
I wrote up this class that can be used if you're not on iOS 14.2.
This class will prompt user for permission to access local network (first time).
Verify existing permission state if already denied/granted.
Just remember this instance has to be kept alive so if you are using this in a function call within another class you need to keep the instance alive outside of the scope of the calling function. You will also need the network multicasting entitlement under certain circumstances.
import UIKit
import Network
class LocalNetworkPermissionChecker {
private var host: String
private var port: UInt16
private var checkPermissionStatus: DispatchWorkItem?
private lazy var detectDeclineTimer: Timer? = Timer.scheduledTimer(
withTimeInterval: .zero,
repeats: false,
block: { [weak self] _ in
guard let checkPermissionStatus = self?.checkPermissionStatus else { return }
DispatchQueue.main.asyncAfter(deadline: .now(), execute: checkPermissionStatus)
})
init(host: String, port: UInt16, granted: #escaping () -> Void, failure: #escaping (Error?) -> Void) {
self.host = host
self.port = port
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationIsInBackground),
name: UIApplication.willResignActiveNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationIsInForeground),
name: UIApplication.didBecomeActiveNotification,
object: nil)
actionRequestNetworkPermissions(granted: granted, failure: failure)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
/// Creating a network connection prompts the user for permission to access the local network. We do not have the need to actually send anything over the connection.
/// - Note: The user will only be prompted once for permission to access the local network. The first time they do this the app will be placed in the background while
/// the user is being prompted. We check for this to occur. If it does we invalidate our timer and allow the user to make a selection. When the app returns to the foreground
/// verify what they selected. If this is not the first time they are on this screen, the timer will not be invalidated and we will check the dispatchWorkItem block to see what
/// their selection was previously.
/// - Parameters:
/// - granted: Informs application that user has provided us with local network permission.
/// - failure: Something went awry.
private func actionRequestNetworkPermissions(granted: #escaping () -> Void, failure: #escaping (Error?) -> Void) {
guard let port = NWEndpoint.Port(rawValue: port) else { return }
let connection = NWConnection(host: NWEndpoint.Host(host), port: port, using: .udp)
connection.start(queue: .main)
checkPermissionStatus = DispatchWorkItem(block: { [weak self] in
if connection.state == .ready {
self?.detectDeclineTimer?.invalidate()
granted()
} else {
failure(nil)
}
})
detectDeclineTimer?.fireDate = Date() + 1
}
/// Permission prompt will throw the application in to the background and invalidate the timer.
#objc private func applicationIsInBackground() {
detectDeclineTimer?.invalidate()
}
/// - Important: DispatchWorkItem must be called after 1sec otherwise we are calling before the user state is updated.
#objc private func applicationIsInForeground() {
guard let checkPermissionStatus = checkPermissionStatus else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: checkPermissionStatus)
}
}
Declare outside the scope of the function in order to keep alive. Just remember to set to nil once you're done if the whole calling class isn't being deallocated later in order to unsubscribe to notifications.
Can be used like this:
class RandomClass {
var networkPermissionChecker: LocalNetworkPermissionChecker?
func checkPermissions() {
networkPermissionChecker = LocalNetworkPermissionChecker(host: "255.255.255.255", port: 4567,
granted: {
//Perform some action here...
},
failure: { error in
if let error = error {
print("Failed with error: \(error.localizedDescription)")
}
})
}
}

Send data created while the user offline

I want to allow users to create some items while they are offline. then send created items to backend when the user reconnect to internet.I am confused, What's the proper way to achieve that?
Should I use waitsforconnectivity of URLSession and it will send the request even when the user close the app
Or should I schedule a background task? if so then how to trigger this task when user connect to the internet?
Notes: I am using Alamofire for networking
I think you're overcomplicating this.
If you're using Alamofire for networking then I wouldn't suggest the first approach as that would be mixing the usage of URLSession and Alamofire for networking and that's not a great idea.
In terms of your second approach. Why does it need to be a background task? Why can't you just check if the user is first connected to the internet, and if they are you can proceed normally. If not, then just create items and cache them somehow. Then when you reconnect to the internet you can send the cached items first as you would send normal items.
Alamofire has a built in NetworkReachabilityManager which will help you determine your network status. There's a nice example in this answer for using it.
You can use Alomafire itself do that:
NetworkManager
class NetworkManager {
//shared instance
static let shared = NetworkManager()
let reachabilityManager = Alamofire.NetworkReachabilityManager(host: "www.google.com")
func startNetworkReachabilityObserver() {
reachabilityManager?.listener = { status in
switch status {
case .notReachable:
print("The network is not reachable")
case .unknown :
print("It is unknown whether the network is reachable")
case .reachable(.ethernetOrWiFi):
print("The network is reachable over the WiFi connection")
case .reachable(.wwan):
print("The network is reachable over the WWAN connection")
}
}
// start listening
reachabilityManager?.startListening()
}
}
Reachability Observer
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// add network reachability observer on app start
NetworkManager.shared.startNetworkReachabilityObserver()
return true
}
}

Inconsistent behaviour with WatchKit app - Swift

I'm trying to make iOS app to communicate with watch, but i get inconsistent behaviour all the time - either the communication is too slow, or none of the data gets transferred at all.
Besides, i don't see any "Phone disabled" screen when the watchKit runs (which causes a crash, because i need to get data from the phone first).
This is what i have in regards to establishing the WCSession in the iPhone app
App Delegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if NSClassFromString("WCSession") != nil {
if #available(iOS 9.0, *) {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
if session.paired {
print("Watch connected")
} else {
print("No watch")
}
}
} else {
}}
if NSClassFromString("WCSession") != nil {
if(WCSession.isSupported()){
session.sendMessage(["b":"delegateSaysHi"], replyHandler: nil, errorHandler: nil)
}}
}
MainViewController
(viewDidLoad)
if NSClassFromString("WCSession") != nil {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
if session.paired {
print("Watch connected")
} else {
print("No watch")
}
}}
MainViewController (Method for transferring bunch of data from iOS app to watchKit app)
func transferData(){
do {
let dataArray = ["somedata": array2d1]
try WCSession.defaultSession().updateApplicationContext(dataArray)
let dataArray1 = ["somedata1": array2d2]
try WCSession.defaultSession().updateApplicationContext(dataArray1)
let dataArray2 = ["somedata2": array2d3]
try WCSession.defaultSession().updateApplicationContext(dataArray2)
let dataArray3 = ["somedata3": array2d4]
try WCSession.defaultSession().updateApplicationContext(dataArray3)
// and up to 12
}
catch {
print("Something wrong happened")
}
}
And this is for watchKit app
App Delegate
func applicationDidFinishLaunching() {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
}
}
func applicationDidBecomeActive() {
if(WCSession.isSupported()){
self.session.sendMessage(["b":"peek"], replyHandler: nil, errorHandler: nil)
}
InterfaceController (awakeWithContext)
if(WCSession.defaultSession().reachable){
self.session.sendMessage(["b":"peek"], replyHandler: nil, errorHandler: nil)
}
Method for receiving ApplicationContext data
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
if let retrievedArray1 = applicationContext["somedata"] as? [[String]] {
self.watchAppArray = retrievedArray1
}
if let retrievedArray2 = applicationContext["somedata2"] as? [[String]] {
self.watchAppArray = retrievedArray1
// and so on for 12 arrays sent from phone
}
}
}}
Any advices on clearing out the situation are very welcome!
Thank you.
Multiple delegates/activations:
You're repeatedly setting up, delegating, and activating sessions in different parts of your app. You keep changing your delegate, so code in one part of the app will no longer be used after you delegated handling to a different part of your app.
You should use a single session/delegate throughout your app. One solution is to setup a WCSession singleton which would be available app-wide. Here's a guide which walks you through that process.
Only the most recent application context would get sent:
By trying to queue up multiple application context requests, the earlier ones would no longer be in the queue when the system gets around to transmitting it, as the system would have already replaced the preceding context with the later one. So only the last (dataArray3) would ever get transmitted.
Use the updateApplicationContext:error: method to communicate recent state information to the counterpart. When the counterpart wakes, it can use this information to update its own state. ... This method overwrites the previous data dictionary, so use this method when your app needs only the most recent data values.
If all of the arrays represent the recent state of your application, you want to transmit them together in a single dictionary.
var dataArray = [String: AnyObject]()
dataArray["somedata"] = array2d1
dataArray["somedata1"] = array2d2
dataArray["somedata2"] = array2d3
dataArray["somedata3"] = array2d4
do {
try session.updateApplicationContext(dataArray)
}
catch {
print(error)
}
It may also help to add some error handling to your sendMessage code, as the paired device may not always be reachable.
Slow communication:
As for the communication being too slow, there are two issues at hand.
Transfers may not happen immediately.
When only one session is active, the active session may still send updates and transfer files, but those transfers happen opportunistically in the background.
Remember that background transfers are not be delivered immediately. The system sends data as quickly as possible but transfers are not instantaneous, and the system may delay transfers slightly to improve power usage. Also, sending a large data file requires a commensurate amount of time to transmit the data to the other device and process it on the receiving side.
The more data you send, the longer it takes to transmit/receive it all.
When sending messages, send only the data that your app needs. All transfers involve sending data wireless to the counterpart app, which consumes power. Rather than sending all of your data every time, send only the items that have changed.
You can control how much data you send, as well as whether the data is sent interactively or in the background. If the watch is reachable, you could use sendMessage for immediate communication. If it's not reachable, you could fall back on a background method.

'The UIApplicationDelegate in the iPhone App never called reply()' error all of a sudden

I recently reverted back to a previous version of my app through my Time Capsule backup, and everything is working as it should be with the exception of one thing. When I try to use the watch extension for my app, it never receives a reply from the parent application. I haven't changed any code, but it doesn't work no matter what. Even if I just send an empty request and a simple string back, I get the same error:
The UIApplicationDelegate in the iPhone App never called reply()
This is (the simplified version of) my code:
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
WKInterfaceController.openParentApplication(["test": "test"]) { userInfo, error in
println("User Info: \(userInfo)")
println("Error: \(error)")
}
}
Delegate:
func application(application: UIApplication, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]?, reply: (([NSObject : AnyObject]!) -> Void)!) {
let favouritesArrayDefaults = NSUserDefaults.standardUserDefaults().arrayForKey("favourites")!
if let pfqueryRequest: AnyObject = (userInfo as? [String: AnyObject])?["parkName"] {
} else {
let taskID = beginBackgroundUpdateTask()
reply(["Success": "Success"])
endBackgroundUpdateTask(taskID)
}
}
Anyone have any ideas?
You are supposed to call the watch's reply function immediately. You can't wait until an async function completes. By then the watch has given up and decided the phone isn't going to reply. I can't find the statement to that effect in Apple's docs any more, but I do remember reading it.
Your code does not call the replyBlock in the first if block (before the else). Might that be the code path that you see the error log for?
Figured it out! The line:
let favouritesArrayDefaults = NSUserDefaults.standardUserDefaults().arrayForKey("favourites")!
Would crash if the array was empty. This was the first time I had tried the watch app with an empty array in the main app so that was why it had never occurred before.

How do I exactly use MMWormhole with Swift?

I have an iPhone application and added a WatchKitExtension. From the iPhone App I want to pass a String to the WatchApp which is supposed to change an image on the Watch.
What I already did was to download the source files and import the MMWormhole.m & .h. They are written in Obj-C and so Xcode automatically bridged them for me.
I also added an app group and activated it for my WatchExtension & my iPhone target
In the tutorial on GitHub it says I have to initialize the wormhole with:
self.wormhole = [[MMWormhole alloc] initWithApplicationGroupIdentifier:#"group.com.mutualmobile.wormhole"
optionalDirectory:#"wormhole"];
...and send a message using:
[self.wormhole passMessageObject:#{#"titleString" : title}
identifier:#"messageIdentifier"];
But I have actually no idea where to put that, I am using Swift in my iPhone application and the WatchExtension.
Can anyone please help me there?
I suppose it depends on different applications, but for one application I put the listeners in the didFinishLaunchingWithOptions method of my app delegate in the main iOS app. This was because the user would be using the watch, and they would be relaying information off to the phone
There were multiple listeners...
var wormhole = MMWormhole(applicationGroupIdentifier: "group", optionalDirectory: nil)
wormhole.listenForMessageWithIdentifier("identifier", listener: { (message) -> Void in
//do stuff
})
wormhole.listenForMessageWithIdentifier("identifier2", listener: { (message) -> Void in
//do stuff
})
wormhole.listenForMessageWithIdentifier("identifier3", listener: { (message) -> Void in
//do stuff
})
And then in a WKInterfaceController, I sent a message. Sometimes in an action, sometimes in the willActivate method. It really depends on the flow of your app
var wormhole = MMWormhole(applicationGroupIdentifier: "group", optionalDirectory: nil)
#IBAction func buttonPushed(){
wormhole.passMessageObject("object", identifier: "identifier1")
}
This can work both ways though, I could have very easily put a listener in my watch which would wait for messages initiated by some Interface Controller on the phone.
Here are my instructions. Hopefully they help you with the simplest use case and then you can expand from there. (Remember to structure your code so that it actually makes sense!)
Get MMWormhole (the .h and the .m) added to your project. If you know how to use Cocoapods, do that, but otherwise, just use git submodules. (I use git submmodules)
Because you need the .h to be visible from Swift, you need to use a bridging header.
Set up an App Group, which requires using the Developer Portal. Link is here
In your iPhone build target -> Capabilities -> App Groups and add your group. If all three checkboxes do not go perfectly, go back to the Developer Portal and make sure everything is right or start again.
MMWormhole, iPhone Side
Set up the wormhole somewhere you can reach it. NOTE: your group ID has to be the one from above!
let wormhole = MMWormhole(applicationGroupIdentifier: "group.testMe.now", optionalDirectory: nil)
wormhole.listenForMessageWithIdentifier("wormholeMessageFromWatch", listener: { (message ) -> Void in
if let messageFromWatch = message as? String {
// do something with messageFromWatch
}
})
iPhone App Sends String
wormhole.passMessageObject("message from phone to watch", identifier: "wormholeMessageFromPhone")
iPhone app registers to receive and sends again in the callback via MMWormhole (asynchronous but cool)
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
universe.initWormhole(.phone, messageHandler: { (message) -> () in
universe.wormhole.passMessageObject("the phone got \(message)", identifier: "wormholeMessageFromPhone")
})
return true
}
MMWormhole, Apple Watch Side
Set up the wormhole somewhere you can reach it. NOTE: your group ID has to be the one from above!
let wormhole = MMWormhole(applicationGroupIdentifier: "group.testMe.now", optionalDirectory: nil)
wormhole.listenForMessageWithIdentifier("wormholeMessageFromPhone", listener: { (message ) -> Void in
if let messageFromPhone = message as? String {
// do something with messageFromPhone
}
})
MMWormhole, watch app registers to receive
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
universe.initWormhole(.watch, messageHandler: { (message) -> () in
println("MMWormhole Message Came to Watch: \(message)")
})
}
MMWormhole, watch app sends
// force open the parent application because otherwise the message goes nowhere until the app is opened
WKInterfaceController.openParentApplication(["":""], reply: nil)
universe.wormhole.passMessageObject("[from watch to phone]", identifier: "wormholeMessageFromWatch")

Resources