Apple Watch complications are not reliably updated - watchkit

I have an iPhone app that sends data from the iPhone app directly to the watch face to be displayed as a complication.
I use the WatchConnectivity framework to create a WCSession to send the data to the watch from the phone.
My data is stored in a dictionary, and sent to the watch using WCSession's transferCurrentComplicationUserInfo method. (This method can be used something like 50 times a day, and I am aware of this - that is not the issue.)
The transferCurrentComplicationUserInfo method seems to work the first time that I attempt to send data.
My problem is that my iPhone app is meant to call this function several times in a session, and it only reliably works the first time.
When I send a second set of data, the first set remains on the complication. Often, when I send the third set, the second set appears. Sometimes the second set appears permanently, and sometimes it only appears for a brief second before displaying the third set.
It is inconsistent, and that is the issue I am having.
Is there anything that I have set up incorrectly?
Code:
//iPhone code to send data to Apple Watch:
func sendComplication(complication: Complication) {
guard let session = session else {
delegate?.failedToSendComplication(reason: "Could not connect to your Apple Watch.")
return
}
guard let context = convertComplicationToDictionary(complication: complication) else {
delegate?.failedToSendComplication(reason: "Couldn't cast complication to a dictionary.")
return
}
if session.remainingComplicationUserInfoTransfers > 0 {
session.transferCurrentComplicationUserInfo(context)
delegate?.didSendComplication()
} else {
delegate?.failedToSendComplication(reason: "Due to hardware limitations, you can only send a certain amount of complications in a day. You have exceeded that limit for today. You can still set complications from the Apple Watch app.")
}
}
// WatchKit Extension Delegate to receive and handle data sent from iPhone app
import WatchKit
import WatchConnectivity
class ExtensionDelegate: NSObject, WKExtensionDelegate {
var session: WCSession?
override init() {
super.init()
self.session = newWatchConnectivitySession()
}
func newWatchConnectivitySession() -> WCSession? {
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
return session
}
return nil
}
func reloadComplicationTimeline() {
let server = CLKComplicationServer.sharedInstance()
guard let activeComplicationFamilies = server.activeComplications else { return }
for comp in activeComplicationFamilies {
server.reloadTimeline(for: comp)
}
}
}
extension ExtensionDelegate: WCSessionDelegate {
func sessionReachabilityDidChange(_ session: WCSession) {
if session.activationState != .activated {
self.session = newWatchConnectivitySession()
}
}
// Receive info from iPhone app
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
// Parse dictionary and update data source
reloadComplicationTimeline()
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
guard let error = error else { return }
print(error.localizedDescription)
}
}
// UPDATE //
Upon further inspection, I now see that steps are happening out of order.
This is the sequence of events:
sendComplication is called from the iPhone app
ExtensionDelegate is initialized on the Watch app, setting up the WCSession
The complication is updated (too early - this is before the WCSession receives the new data)
The WCSession didReceiveUserInfo delegate method is called, data is parsed, and the data source is updated (too late)
The complication is told to reload, but nothing happens (possible budgeting issue?)

Try the following:
func reloadComplicationTimeline() {
#if os(watchOS)
let server = CLKComplicationServer.sharedInstance()
if let activeComplicationFamilies = server.activeComplications {
for comp in activeComplicationFamilies {
server.reloadTimeline(for: comp)
}
#endif
}
func sendComplication(complication: Complication) {
guard WCSession.default.activationState == .activated else {
delegate?.failedToSendComplication(reason: "Could not connect to your Apple Watch.")
return
}
guard let context = convertComplicationToDictionary(complication: complication) else {
delegate?.failedToSendComplication(reason: "Couldn't cast complication to a dictionary.")
return
}
#if os(iOS)
if WCSession.default.isComplicationEnabled {
let userInfoTranser = WCSession.default.transferCurrentComplicationUserInfo(context)
delegate?.didSendComplication()
} else {
delegate?.failedToSendComplication(reason: "Due to hardware limitations, you can only send a certain amount of complications in a day. You have exceeded that limit for today. You can still set complications from the Apple Watch app.")
}
#endif
}
Here is a good example from Apple that could help you more: source

As you describe it in your update, your iPhone app calls session.transferCurrentComplicationUserInfo(context) before the watch sets up its WCSession. But the docs say:
[transferCurrentComplicationUserInfo(_:)] can only be called while the
session is active - that is, the activationState property is set to
WCSessionActivationState.activated. Calling this method for an
inactive or deactivated session is a programmer error.
Thus I suggest that you implement (if you haven’t done so already) the WCSessionDelegate function session(_:activationDidCompleteWith:error:) (see here), and transfer complication data only after the session has been activated.

Resetting the iPhone, Apple Watch, and Mac fixed the problem.

Related

SwiftUI - Sign in with Apple crash on watchOS

I am implementing Sign in with Apple on watchOS using SwiftUI.
Since the WKInterfaceAuthorizationAppleIDButton doesn't conform to the View protocol I am wrapping it using WKInterfaceObjectRepresentable to use it in a SwiftUI view.
I have implemented a Coordinator to handle taps on the button and it also acts as a delegate for the Sign up process callbacks.
Below is my code:
struct AppleLoginButton: WKInterfaceObjectRepresentable {
typealias WKInterfaceObjectRepresentable = WKInterfaceObjectRepresentableContext<AppleLoginButton>
func updateWKInterfaceObject(_ wkInterfaceObject: WKInterfaceAuthorizationAppleIDButton, context: WKInterfaceObjectRepresentableContext<AppleLoginButton>) {
// No code required
}
class Coordinator: NSObject, ASAuthorizationControllerDelegate {
#objc func buttonPressed(_ sender: WKInterfaceAuthorizationAppleIDButton) {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.performRequests()
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
// Verify the user
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
// Handle error.
}
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeWKInterfaceObject(context: WKInterfaceObjectRepresentableContext<AppleLoginButton>) -> WKInterfaceAuthorizationAppleIDButton {
return WKInterfaceAuthorizationAppleIDButton(target: context.coordinator, action: #selector(Coordinator.buttonPressed(_:)))
}
}
Whenever I tap the Sign in button the App crashes with the following error every time,
Thread 1: EXC_BAD_ACCESS (code=1, address=0x74747572)
can anybody help me if something is wrong with my code or the way I have implemented the Coordinator ?
However the above code works on iOS in SwiftUI with ASAuthorizationAppleIDButton button
And further if I implement Sign in with Apple in WatchKit it works without any issues.
So it seems that I am missing some implementation detail, would love some help on this.
Update:
I googled for EXC errors and most answers point to illegal memory access as the culprit for these crashes.
Further googling for target-action crashes revealed that if the target is released from the memory pool it leads to crashes.
So can anyone help how can I keep the Coordinator in memory without it getting autoreleased ?
I filed a Feedback with Apple regarding this issue, and they replied with the following fix.
WKInterfaceAuthorizationAppleIDButton does not trigger the action with a sender parameter. The fix here is to stop specifying a sender parameter on buttonPressed. So:
#objc func buttonPressed(_ sender: WKInterfaceAuthorizationAppleIDButton)
becomes
#objc func buttonPressed()
Then line:
WKInterfaceAuthorizationAppleIDButton(target: context.coordinator, action: #selector(Coordinator.buttonPressed(_:)))
becomes
WKInterfaceAuthorizationAppleIDButton(target: context.coordinator, action: #selector(Coordinator.buttonPressed))
The above changes fixes my issue and I have tested the same on a real device.
Hope someone finds this useful.

IgniteMesssaging works in sync mode

It seems that Ignite(2.0) Messaging's send function works in sync mode, it will be blocked be the listener. And below is my testing code.
ignite.message().localListen("TEST", (nodeId, Msg) -> {
try {
Thread.sleep(500);
} catch (Exception ex) {
}
return true;
});
for (int i = 0; i < 100; i++) {
ignite.message().send("TEST", "Hello World");
}
It cost about 50 seconds to send 100 messages, and it is almost equals the sleep time 500 ms * 100. seems the send function in sync mode not in async mode.
Does anybody know how to change the send function in async mode?
Thanks in advance.
Seems async listener invocation was missed while adding new API, but you still have two options:
Use deprecated withAsync(), unless sendAsync() will be added.
Pass your own Executor in predicate, if you always return true, for example.
I've just opened a ticket for that IGNITE-5570
It seems that you're testing within one node. In this case there is no message sent and listener is invoked synchronously. Network communication is asynchronous in Ignite, so if you do the testing on two nodes, you should not see such behavior.

Does Firebase RemoteConfig keep fetching?

I have the following code that loads in the initial ViewController viewDidLoad. It works fine initially. But shouldn't it look for changes every 10 seconds?
When I make an update to a config value in Firebase and publish, I don't see this come through in the app. I am running in debug mode so throttling isn't an issue.
If I restart the app, I see the new value. Since the interval is set to 10 seconds, shouldn't I see the update while the app is running?
let rc = FIRRemoteConfig.remoteConfig()
let interval: TimeInterval = 10
FIRRemoteConfig.remoteConfig().fetch(withExpirationDuration: interval) {
(status, error) in
guard error == nil else {
//handle error here
return
}
FIRRemoteConfig.remoteConfig().activateFetched()
let test = rc["key1"].stringValue //this runs only once
}
Any ideas why this isn't update?
You should use scheduledTimer instead.
/// Fetches Remote Config data and sets a duration that specifies how long config data lasts.
/// Call activateFetched to make fetched data available to your app.
/// #param expirationDuration Duration that defines how long fetched config data is available, in
/// seconds. When the config data expires, a new fetch is required.
/// #param completionHandler Fetch operation callback.
open func fetch(withExpirationDuration expirationDuration: TimeInterval, completionHandler: FirebaseRemoteConfig.FIRRemoteConfigFetchCompletion? = nil)
fetch(withExpirationDuration: interval) is to fetch data with a timeout, that is your interval.
let interval: TimeInterval = 10
Timer.scheduledTimer(timeInterval: interval,
target: self,
selector: #selector(updateConfig),
userInfo: nil,
repeats: true)
func updateConfig() {
let rc = FIRRemoteConfig.remoteConfig()
FIRRemoteConfig.remoteConfig().fetch { (status, error) in
guard error == nil else {
//handle error here
return
}
FIRRemoteConfig.remoteConfig().activateFetched()
let test = rc["key1"].stringValue //this runs only once
}
}

How to handle timeout in queries with Firebase

I noticed that if I execute a query in Firebase and the database server is not reachable, the callback waits just forever (or until the server is reachable again).
Where this behavior is quite natural for the asynchronous approach used, it would nevertheless be useful to have an easy way to specify a timeout so you could inform the user about the status.
Is there such an option and I just missed it - or it really missing?
Or how would you solve this problem?
you can manage yourself a timer controller that after x seconds remove the listener to you firebase reference. It's very simple, just one line of code in android for example.
You can see the code for the web (Detaching Callbacks section):
https://www.firebase.com/docs/web/guide/retrieving-data.html
or for android (Detaching Callbacks section):
https://www.firebase.com/docs/android/guide/retrieving-data.html#section-detaching
same section for IOS ;)
As per today there is no timeout concept on those listeners. One option is to manage the timeout yourself.
This is how I do it when I also want to display a progress dialog while loading the content.
private void showProgressDialog(boolean show, long time) {
try {
if (progressDialog != null) {
if (show) {
progressDialog.setMessage("Cargando...");
progressDialog.show();
new Handler().postDelayed(new Runnable() {
public void run() {
if(progressDialog!=null && progressDialog.isShowing()) {
progressDialog.dismiss();
Toast.makeText(ActPreguntas.this, "Couldn't connect, please try again later.", Toast.LENGTH_LONG).show();
}
}
}, time);
} else {
progressDialog.dismiss();
}
}
}catch(IllegalArgumentException e){
}catch(Exception e){
}
}
So when you make a request to Firebase you call showProgressDialog(true,5000) and after 5 seconds if the dialog stills there is because it could not connect and you then do what you have to as per the timeout.
On the callback of the Firebase listener you do this showProgressDialog(false,0)
Hope it helps.
Here's my solution for the Firebase iOS SDK, this may be helpful for others:
extension DatabaseReference {
func observe(_ eventType: DataEventType, timeout: TimeInterval, with block: #escaping (DataSnapshot?) -> Void) -> UInt {
var handle: UInt!
let timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { (_) in
self.removeObserver(withHandle: handle)
block(nil)
}
handle = observe(eventType) { (snapshot) in
timer.invalidate()
block(snapshot)
}
return handle
}
}
Usage:
database.child("users").observe(.value, timeout: 30) { (snapshot) in
guard let snapshot = snapshot else {
// Timeout!
return
}
// We got data within the timeout, so do something with snapshot.value
}
I would suggest simply using a thread?
Allow yourself to assign your call to Firebase from within a thread instance, then in the rare event that the write to Firebase takes too long you can just cancel the thread?
let thread = NSThread(target:self, selector:#selector(uploadToFirebase), object:nil)
. . .
func uploadToFirebase(data: Dictionary) {
// Do what you need to here. Just an example
db.collection("posts").document("some unique post id").setData([
"name": "John",
"likes": 0
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
Then just create a timer that cancels the thread if the timer fires. If not, just cancel the timer.
If you're using the Firebase SDK v6.5.0 and above, you can use FirebaseOptions's setConnectTimeout (https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/FirebaseOptions.Builder.html#setConnectTimeout(int)).
Sample:
Integer connectTimeoutinMillis = 6000; //6 seconds
FirebaseOptions firebaseOptions = FirebaseOptions.builder()
.setCredentials(credentials)
.setDatabaseUrl(Application.firebaseSDKDatabaseUrl)
.setConnectTimeout(connectTimeoutinMillis)
.build();
FirebaseApp.initializeApp(firebaseOptions);

Detect if Firebase connection is lost/regained

Is there a strategy that would work within the current Firebase offering to detect if the server connection is lost and/or regained?
I'm considering some offline contingencies for mobile devices and I would like a reliable means to determine when the Firebase data layer is available.
This is a commonly requested feature, and we just released an API update to let you do this!
var firebaseRef = new Firebase('http://INSTANCE.firebaseio.com');
firebaseRef.child('.info/connected').on('value', function(connectedSnap) {
if (connectedSnap.val() === true) {
/* we're connected! */
} else {
/* we're disconnected! */
}
});
Full docs are available at https://firebase.google.com/docs/database/web/offline-capabilities.
Updated:
For many presence-related features, it is useful for a client to know when it is online or offline. Firebase Realtime Database clients provide a special location at /.info/connected which is updated every time the client's connection state changes. Here is an example:
DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
boolean connected = snapshot.getValue(Boolean.class);
if (connected) {
System.out.println("connected");
} else {
System.out.println("not connected");
}
}
#Override
public void onCancelled(DatabaseError error) {
System.err.println("Listener was cancelled");
}
});
I guess this changed in the last couple of months. Currently the instructions are here:
https://firebase.google.com/docs/database/web/offline-capabilities
In summation:
var presenceRef = firebase.database().ref("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().set("I disconnected!");
and:
var connectedRef = firebase.database().ref(".info/connected");
connectedRef.on("value", function(snap) {
if (snap.val() === true) {
alert("connected");
} else {
alert("not connected");
}
});
I'll admit I don't know a lot about how references are set, or what that means (are you making them out of thin air or do you have to have already created them beforehand?) or which one of those would trigger something on the server as opposed to something on the front end, but if the link is still current when you read this, a little more reading might help.
For android you can make user offline by just a single function called onDisconnect()
I did this in one of my chat app where user needs to get offline automatically if network connection lost or user disconnected from Internet
DatabaseReference presenceRef = FirebaseDatabase.getInstance().getReference("USERS/24/online_status");
presenceRef.onDisconnect().setValue(0);
On disconnecting from network Here I am making online_status 0 of user whose Id is 24 in firebase.
getReference("USERS/24/online_status") is the path to the value you need to update on offline/online.
You can read about it in offline capabilities
Note that firebase takes time around 2-10 minutes to execute onDisconnect() function.
firebase for web
firebase.database().ref(".info/connected").on("value",(snap)=> {});
The suggested solution didn't work for me, so I decided to check the connection by writing and reading 'health/check' value. This is the code:
const config = {databaseURL: `https://${projectName.trim()}.firebaseio.com/`};
//if app was already initialised delete it
if (firebase.apps.length) {
await firebase.app().delete();
}
// initialise app
let cloud = firebase.initializeApp(config).database();
// checking connection with the app/database
let connectionRef = cloud.ref('health');
connectionRef.set('check')
.then(() => {
return connectionRef.once("value");
})
.then(async (snap) => {
if (snap.val() === 'check') {
// clear the check input
await connectionRef.remove();
// do smth here becasue it works
}
});

Resources