Why is this task await leaving the UI context in Xamarin iOS? - ios

The following has somewhat shaken my async/await-based belief system. Under Xamarin/iOS the following fails, saying that UI-related things are being done in a non-UI thread. Adding check points shows that the context does in fact switch after the async file write.
My understanding is that lacking a ConfigureAwait, the following should be completely safe. I'm assuming this is a Xamarin nuance of which I'm unaware but it's difficult to understand what that could be.
This same code works fine on Android and UWP.
private async void ShareButton_OnClicked(object sender, EventArgs e)
{
if (!(BindingContext is PhotoViewModel photoViewModel))
{
return;
}
// in UI context
var name = photoViewModel.Name ?? "temp.jpg";
var file = Path.Combine(FileSystem.CacheDirectory, name);
using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write))
{
await stream.WriteAsync(photoViewModel.Data, 0, photoViewModel.Data.Length);
}
// not in UI context!
// calling this causes SIGABRT: UIKit Consistency error
await Share.RequestAsync(new ShareFileRequest(new ShareFile(file)));
}

calling this causes SIGABRT: UIKit Consistency error
await Share.RequestAsync(new ShareFileRequest(new ShareFile(file)));
Although there is no problem in Android and UWP, it may not be compatible with such writing in iOS. Above line code needs UI thread to invoke , however it is in async method ShareButton_OnClicked. Maybe need to invoke it from Main thread specially, have a try with the follow code to invoke it.
await Device.InvokeOnMainThreadAsync(() =>
{
// inkoke your code .
Share.RequestAsync(new ShareFileRequest(new ShareFile(file)));
});

This turned out to be a bug in mono
https://github.com/mono/mono/issues/16759

Related

iOS requestTrackingAuthorization callback is not on the main thread

The iOS ATT Alert needs to be shown on startup before an app starts, so the remaining app code needs to be run in the completion handler (eg initialising advert code and starting the main game).
However, this crashes because the completion handler callback is not on the main thread (after the alert is actually shown and Allow or Ask App Not to Track is selected).
In iOS (Xamarin C# code):
public override void OnActivated(UIApplication application) // RequestTrackingAuthorization must be called when app is Active
{
if (!this.shownAlert)
{
this.shownAlert=true;
Debug.WriteLine("1) ThreadId={0}", Environment.CurrentManagedThreadId); // ThreadId=1
ATTrackingManager.RequestTrackingAuthorization(delegate (ATTrackingManagerAuthorizationStatus trackingManagerAuthorizationStatus)
{
Debug.WriteLine("2) ThreadId={0}", Environment.CurrentManagedThreadId); // ThreadId=7
});
}
}
Prints:
1) ThreadId=1
2) ThreadId=7
And the similarly in Unity:
Debug.WriteLine("1) ThreadId={0}", Environment.CurrentManagedThreadId); // ThreadId=1
ATTrackingStatusBinding.RequestAuthorizationTracking(delegate (int status)
{
Debug.WriteLine("2) ThreadId={0}", Environment.CurrentManagedThreadId); // ThreadId=4
});
This seems like a bug to me. So how do all the apps and games out there get this to work? Somehow switch to the main thread in the callback? I don’t see any examples of this.

What is the life cycle of a ContentPage in Xamarin Forms iOS and why does it crash when I switch to another app?

My Xamarin iOS app is crashing on a ContentPage when I switch away from it to another app.
The first occurrence was in a label renderer, so I wrapped it in a try catch block:
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
//Add a try because of this:
//https://stackoverflow.com/questions/46881393/ios-crash-report-unexpected-start-state-exception
try
{
base.OnElementChanged(e);
if (label != null)
{
sv = label.Parent as ScrollViewEx;
}
}
catch
{
}
}
Now it is crashing in another part of the app trying to access a disposed editor object when I switch away.
Is there a life cycle thing going on here that I need to be aware of in terms of why the OS is trying to re-render UI objects in such circumstances?
And how can I generically protect my app from these sorts of crashes without having to wrap every single UI object in a try catch just because I'm switching to another app?
I am not familiar with the issue, however, what I can recommend is at the end of the catch keyword, put (Exception e) as a parameter
try
{...}
catch(Exception e)
{
Debug.WriteLine(e.Message);
}
This way you will be one step closer to figuring out this painful bug.

Flutter async executing on main thread

I have a CameraPreview that fills the whole screen, with a FloatingActionButton at the bottom to take a picture.
On the onPress method of the button, I'm making a network call for which I do not care (yet) about the result. So I would like everything made inside that method to be done asynchronously, so it does not block my main thread.
That means (if I get it right) that I sould not use the await keyword.
This is the code in my onPressed
// Attempt to take a picture and log where it's been saved
await controller.takePicture(path);
print("Done taking picture");
sendBase64ToAPI(path);
This is my sendBase64ToApi method
Future<String> sendBase64ToAPI(String path) async {
File(path).readAsBytes().then(thenMethod);
return null;
}
void thenMethod(List bytes){
print("Start reading file");
Image image = decodeImage(bytes);
int x = ((screenWidth/2) + (overlayWidth/2)).toInt();
int y = ((screenHeight/2) + (overlayHeight/2)).toInt();
print("Start cropping image");
image = copyCrop(image, x, y, overlayWidth, overlayHeight);
var base64Str = base64.encode(image.getBytes());
print("Done");
print(base64Str.substring(0,30));
print(base64Str.substring(base64Str.length-30,base64Str.length-1));
}
My UI is completely frozen between 'Start reading file' and 'Start cropping image' although, those are async methods, called without await so that shouldn't happen.
Why are those methods not executing asynchronously ?
Okay, this seems to be a known issue from the library I'm using.
The documentation recommends to use the decodeImage function in an Isolate.
Will keep the question open for a few days, if someone spots what is synchronous in my code.

MoveToRegion in xamarin forms maps behaves strangely

I am using a Map control in my app, and i need to set the visible region in such a way that it should cover all the pins.
Irony is same code doesn't work on both the platform, iOS works awkwardly , below code yield almost the same visible region in both platform.
if(Device.OS == TargetPlatform.iOS)
customMap.MoveToRegion (MapSpan.FromCenterAndRadius (customMap.CustomPins [0].Pin.Position, Distance.FromMiles (0.20)));
if(Device.OS == TargetPlatform.Android)
customMap.MoveToRegion (MapSpan.FromCenterAndRadius (customMap.CustomPins [0].Pin.Position, Distance.FromMiles (55.0)));
Can anyone explains it? why I need to code like it?
i have found a workaround , i am waiting for some explanation before accepting my own answer for it
Device.StartTimer(TimeSpan.FromMilliseconds(500), () =>
{
customMap.MoveToRegion(MapSpan.FromCenterAndRadius(customMap.CustomPins [0].Pin.Position, Distance.FromMiles(55.0)));
return false;
});
I was running into a problem where the MovetoRegion was being delayed (15-30 seconds) when trying to center on the users current location using the Xamarin Geolocator Plugin, on both IOS and Android. Things work alot better with Saket Kumar's approach with the 500ms delay. Here is my code snippet, hope this helps someone.
private void CenterOnMe_Clicked(object sender, EventArgs e)
{
var locator = CrossGeolocator.Current;
var t = Task.Run(async () =>
{
var position = await locator.GetPositionAsync(TimeSpan.FromSeconds(10));
Device.StartTimer(TimeSpan.FromMilliseconds(500), () =>
{
AroundMeMap.MoveToRegion(
MapSpan.FromCenterAndRadius(
new Position(position.Latitude, position.Longitude), Distance.FromMiles(1)));
return false;
});
});
}

Refresh Screen to Update ListField with new Data

I am using a ListField Control to display data returned from xml webservice. I want to refresh the ListField or the screen every minute to update the ListField with new records or data.
I tried using the code below but it is not working properly (It is hanging).
public MyApp() {
// Push a screen onto the UI stack for rendering.
UiApplication.getUiApplication().invokeLater(new Runnable() {
public void run() {
UiApplication.getUiApplication().pushScreen(new MyScreen());
}
},5000,true);
}
ResponseHandler handler = new ResponseHandler();
ListField listUsers = new ListField(handler.getItem().size());
public MyScreen() {
setTitle("yQAforum");
//Fetch the xml from the web service
String wsReturnString = GlobalV.Fetch_Webservice("myDs");
//Parse returned xml
SAXParserImpl saxparser = new SAXParserImpl();
ByteArrayInputStream stream = new ByteArrayInputStream(wsReturnString.getBytes());
try {
saxparser.parse( stream, handler );
}
catch ( Exception e ) {
response.setText( "Unable to parse response.");
}
//Return vector sze from the handler class
listUsers.setSize(handler.getItem().size());
listUsers.setCallback(this);
listUsers.setEmptyString("No Users found", 0);
add(listUsers);
}
You are attempting to fetch data from your webservice on the UI thread. That's almost always the wrong thing to do.
The UI thread (also known as the main thread) is responsible for drawing the UI, and tracking user actions, like touches, or navigation via a trackpad/trackball. If the UI thread is blocked waiting for a remote web server to respond, it cannot service the UI.
There's a couple changes you should make:
public MyApp() {
// Push a screen onto the UI stack for rendering.
UiApplication.getUiApplication().invokeLater(new Runnable() {
public void run() {
UiApplication.getUiApplication().pushScreen(new MyScreen());
}
},5000,true);
}
should be changed to
public MyApp() {
// Push a screen onto the UI stack for rendering.
pushScreen(new MyScreen());
}
The MyApp() constructor will already be called on the UI thread, so there is no need to use invokeLater() to perform the pushScreen() call on the UI thread. It already will be called on the UI thread, if run from within the MyApp constructor. Also, the 5000 msec delay isn't really helpful. This will just delay the startup of your app by 5 seconds, which users will hate.
If you are trying to implement a splash screen, or something similar, when the app starts up, please search stack overflow for "BlackBerry splash screen", and I'm sure you'll find results.
Now, once your MyScreen class is created, you should take care not to fetch web service results from the UI thread. The MyScreen constructor will be run on the UI thread. If you want, you can initiate a web service request on a background thread, once the screen is shown. One way to do that is to use onUiEngineAttached():
protected void onUiEngineAttached(boolean attached) {
if (attached) {
// TODO: you might want to show some sort of animated
// progress UI here, so the user knows you are fetching data
Timer timer = new Timer();
// schedule the web service task to run every minute
timer.schedule(new WebServiceTask(), 0, 60*1000);
}
}
public MyScreen() {
setTitle("yQAforum");
listUsers.setEmptyString("No Users found", 0);
listUsers.setCallback(this);
add(listUsers);
}
private class WebServiceTask extends TimerTask {
public void run() {
//Fetch the xml from the web service
String wsReturnString = GlobalV.Fetch_Webservice("myDs");
//Parse returned xml
SAXParserImpl saxparser = new SAXParserImpl();
ByteArrayInputStream stream = new ByteArrayInputStream(wsReturnString.getBytes());
try {
saxparser.parse( stream, handler );
}
catch ( Exception e ) {
response.setText( "Unable to parse response.");
}
// now, update the UI back on the UI thread:
UiApplication.getUiApplication().invokeLater(new Runnable() {
public void run() {
// TODO: record the currently selected, or focused, row
//Return vector sze from the handler class
listUsers.setSize(handler.getItem().size());
// Note: if you don't see the list content update, you might need to call
// listUsers.invalidate();
// here to force a refresh. I can't remember if calling setSize() is enough.
// TODO: set the previously selected, or focused, row
}
});
}
}
You'll need to add some error handling, in case the web service doesn't respond, or takes longer than a minute (you wouldn't want to be making a new request, if the last one hadn't finished).
But, this should get you started.
Note: once you fix the problem with running network code on the UI thread, you may still find that your code doesn't work. There could be problems fetching the web service data. You'll have to debug that. I am only showing you one problem with the code posted. If you still have problems with the web service fetch, post another question (with the UI thread problem fixed). Thanks.

Resources