SwiftUI - Sign in with Apple crash on watchOS - watchkit

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.

Related

Xamarin Forms iOS - Saving a user tag in Azure Notification Hubs works in AppDelegate but not in a service

I'm currently trying to get push notifications working for my mobile app using Azure Notification Hubs. Android is working fine and the initial iOS set up in AppDelegate works ok with a sample tag.
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
if (deviceToken == null)
{
return;
}
SBNotificationHub hub = new SBNotificationHub(CommonConstants.LISTEN_CONNECTION_STRING, CommonConstants.NOTIFICATION_HUB_NAME);
// update registration with Azure Notification Hub
hub.UnregisterAll(deviceToken, async (error) =>
{
if (error != null)
{
System.Diagnostics.Debug.WriteLine($"Unable to call unregister {error}");
return;
}
string[] tags = new[] { "iostestpush" };
NSSet userTags = new NSSet(tags);
hub.RegisterNative(deviceToken, userTags, (error) =>
{
if (error != null)
{
System.Diagnostics.Debug.WriteLine($"Unable to call register {error}");
return;
}
});
var templateExpiration = DateTime.Now.AddDays(120).ToString(System.Globalization.CultureInfo.CreateSpecificCulture("en-US"));
hub.RegisterTemplate(deviceToken, "defaultTemplate", CommonConstants.APN_TEMPLATE_BODY, templateExpiration, userTags, (errorCallback) =>
{
if (errorCallback != null)
{
System.Diagnostics.Debug.WriteLine($"RegisterTemplateAsync error: {errorCallback}");
}
});
});
}
The issue I'm having is I need to register the UserId after a successful login. So I set up a service with the above code, saved the token to the device as string so it can be retrieved in the service and turned back into an NSData token
NSData deviceToken = new NSData(token, NSDataBase64DecodingOptions.None);
After a successful login I send the token string and the tag array to my service.
string[] userTag = new[] { loginResponse.UserId.ToString() };
await this._azureReg.SendRegistrationToServer(deviceToken, userTag);
Which, other than turning the token back into NSData and the user tag into an NSSet, is the same as above other than the name change. But Azure is claiming there is no registration even though my output shows
Registered for push notifications with token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
I thought it was the string conversion back and forth, so tested that in the AppDelegate and it worked fine.
So, I'm at a loss at how to register the UserId after a successful login and why it works in one place but not the other.
I hope that's clear and thanks for any advice in advance.
You probably ran into the same bug as me and several others.
Basically SBNotificationHub method overloads like UnregisterAll and RegisterTemplate with the callback signature do not work when you use them off the main thread, using the libraries to date. I was also using a Service for the same purpose (to handle push across platforms with different tags, especially for user id) but my implementation involved switching off the main thread for this.
The bug we logged and is now being addressed is here: https://github.com/Azure/azure-notificationhubs-ios/issues/95
The solution, for now, is to ditch SBNotificationHub completely. The Xamarin / Azure documentation is out of date, and SBNOtificationHub is legacy code. The recommended library is MSNotificationHub. https://github.com/azure/azure-notificationhubs-xamarin
As workarounds you can use the SBNotificationHub method overloads that do not involve callbacks (they return an error message instead) or the workaround in the 95 issue above.

How to deal with lifetime when implementing async timer?

Background
I'm developing a database manager via http connection in Rust. The database has a authenticaition-expiration strategy, and I need to persist the connection in my database manager by re-login after the expiration time.
Pseudocode
My database manager is Database, and there are some methods of it:
struct Database {
access_token: Option<String>
}
impl Database {
async fn login(&mut self, auth: Option<String>) {
if let Some(auth) = auth {
// If user provide auth, login with auth
let response = do_log_in(auth).await;
// get access_token from response
self.access_token = Some(response.access_token);
} else {
// If user doesn't provide auth, login with access_token
do_log_in(access_token).await;
}
restart_timer();
}
// restart the login timer
fn restart_timer(&mut self) {
self.cancel_timer();
self.do_start_timer();
}
}
My implementation
I use tokio to implement the restart_timer thing:
First, I add a field timer: Option<tokio::task::JoinHandle> to Database
Then, when do_start_timer() is called, use tokio::spawn to spawn a new task and assign the returning join handle to self.timer.
Inside the tokio::spawn closure, I await the tokio::time::delay_for, then await the login.
To cancel the timer, I just assign self.timer with None, making the task detached.
Problem
In the detail code of the above implementation, there is a touch thing:
self.timer = Some(tokio::spawn(async move {
tokio::time::delay_for(/* ... */).await;
self.login().await;
}));
The above snippet is in the do_start_timer thing, but this can't compile:
expected `&mut Database`
found `&mut Database`
but, the lifetime must be valid for the static
I don't know what to do.

Apple Watch complications are not reliably updated

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.

How to handle socket exception when net is not there?

I want to display an error screen if net is not there. I am not using connectivity package because I don't want continuous check. I just want to handle exception while calling backend api and display the screen. I am unable to catch the exception.
I found this issue and this question about socket exceptions but none seem to help me.
This is how I call my backend api -
callBackendApi() async {
try {
http.Response response = await Future.value(/*api call here*/)
.timeout(Duration(seconds: 90), onTimeout: () {
print('TIME OUT HAPPENED');
});
} catch (exception) {
Fluttertoast.showToast(msg: 'Check internet connection.');
print('Error occurred' + exception.toString());
}
}
I use dio like this:
try {
var formData = FormData.from(Map<String, dynamic>.from(data));
var response = await dio.post(
uri,
data: formData,
);
jsonResponse = json.decode(response.data);
} on DioError catch (e) {
if (DioErrorType.RECEIVE_TIMEOUT == e.type ||
DioErrorType.CONNECT_TIMEOUT == e.type) {
throw CommunicationTimeoutException(
"Server is not reachable. Please verify your internet connection and try again");
} else if (DioErrorType.RESPONSE == e.type) {
// 4xx 5xx response
// throw exception...
} else if (DioErrorType.DEFAULT == e.type) {
if (e.message.contains('SocketException')) {
throw CommunicationTimeoutException('blabla');
}
} else {
throw CommunicationException("Problem connecting to the server. Please try again.");
}
}
My solution is to import 'dart.io' in order to catch SocketException from try block:
import 'package:http/http.dart' as http;
import 'dart:io';
try{
//Handle you network call code block in here
}on SocketException catch(_){
//To handle Socket Exception in case network connection is not available during initiating your network call
}
Well i don't know if my answer will be solving your question but days ago i had a problem little bit likely yours but in my case was using firebase realtime database. I was asking to myself how can i protect my app from network fails like no internet connection available? Well i am not using connectivity package too so i solve this problem with an approach that you already has been try using a timeout for network operations. I will share two snipets with differents approaches that i had implemented to handle this kind of problem adding some comments trying explain the differences between them.
Approach 1 - Setting timeout outside from network request method
Well the snipet below is a simple firebase database request where _viewsRef is a DatabaseReference and the once method do the request and returns me a Future with or without data.
// get users visualization from realtime database and returns a future
static Future<DataSnapshot> getUserVisualizations({#required String uid}) async {
return _viewsRef.child(uid).limitToLast(50).once();
}
In my BLoC component class i am calling the method below and setting a timeout to the future that is returned.
myBlocComponentMethod(){
//.. some work and finally the call
FirebaseUserViewsHelper.getUserVisualizations(uid: _currentUid)
.then(
(dataSnapshot){
if (dataSnapshot.value == null) {
// do some things to handle no data
}
else {
/// handle your data here
});
}
} // setting timeout here is an important point
).timeout( Duration(seconds: Constants.NETWORK_TIMEOUT_SECONDS),
onTimeout: (){
// method to handle a timeout exception and tell to view layer that
// network operation fails
// if we do not implement onTimeout callback the framework will throw a TimeoutException
} );
}
Well what is the point here? In this case if the timeout expires and future is not completed yet onTimeout callback is called and there i can tell to the view layer that network operation fails and show to the user some widget about it. But even with timeout expired the request to firebase database stays happening again and again, it's like the async event of request the database stays on dart event queue. I think this behavior is bad for performance aspects but if you're building your UI using a StreamBuilder with a little logic and code your requested data will be available right when you internet connection is back and with BLoC pattern the UI can respond easily to this event and we don't need provide a refresh button by example to user make the request again. I don't know if this is the right approach to implement this behavior but it works.
Approach 2 - Setting timeout inside from network request method
Below another firebase database request method
static Future<DataSnapshot> readUserNode( {#required String uid} ) async
=> USERS_REFERENCE.child(uid).once()
.timeout( Duration(seconds: Constants.NETWORK_TIMEOUT_SECONDS ) );
//note: Without timeout callback this line will throw a TimeoutException if the time expires
The usage in another BLoc component:
myBlocComponentMethod2(){
for( String uid in iterable ){
FirebaseUserHelper.readUserNode(uid: uid)
.then( (userSnapshot){
if (userSnapshot.value == null){
// do your stuffs
}
else {
// more stuffs to do
}
}).catchError( (error){
// when timeout expired we will catch the TimeoutException HERE and handling telling
// the UI what we need
} );
}
}
The big difference here that i get was in the behavior. In this second case since i put the timeout inside the request method when the timeout expires the request event do not run anymore, it's like that request event is removed from dart event queue. This can be good from performance perspective but now we need provide a refresh button in UI for user do the data again to get data from internet again.
I don't know if this workaround will solve your problem because you tell about SocketException what is not the case that i has described and i don't know what api you are using to make your requests. Anyway i hope that the concepts described in this post helps you implement a solution in your problem.

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);

Resources