Monthly Archives: June 2020

Code snippets for Xamarin Forms XAML Platform-specifics

This post is part of Luis Matos Code Snippets’ Xamarin Month

In the early days of Xamarin Forms, if you wanted to use a platform-specific feature of a page or a view, you’d have to write a custom renderer.

Later came effects, which were a little less painful.

But the game-changer was when Microsoft decided to build-in common platform-specific features, right into the XAML, so that you could turn on a platforms-specific feature for one platform only, with the directive being ignored for others.

The canonical example in my mind is telling iOS to use the Safe Area, depending on the specific phone, so parts of your UI are not cut off on certain phones. But here is the thing: I can never remember the magical incantation you use to do that. I have to google Xamarin ios safe area every time. There has to be a better way.

             xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
             ios:Page.UseSafeArea="true"

Of course there is – a Code Snippet. Now all I have to remember is psios to include the namespace and then pssa to include the safe-area platform specific … I type the snippet, and hit tab, and bingo, the iOS Platform-specific XAML is entered into the editor (ios:Page.UseSafeArea="true" in case you were wondering).

No more googling. In that spirit I’ve defined a whole slew of snippets for these platform-specifics, including one to import the ios namespace used in the pssa snippet.

The snippet file is here. To use it go to Tools->Code Snippets Manager and hit Import and import this file. Here are some of the Snippets I’ve defined

Shortcut Explanation
psios Snippet to bring in the iOS namespace for platform specific code
pssa Snippet for a iOS safe area
psnpbs Hide NavigationPage Bar Separator on iOS
psmdps Apply MasterDetailPage Shadow on iOS
pslpt Show Large Page Titles on iOS
pshpi Show Home Indicator Visibility on iOS
pspsv Set Page Status Bar Visibility on iOS

There are a lot more platform-specifics than these, and of course they are available for Android too. If I find myself typing them more than once, I’ll create a snippet.

But really, if you find yourself typing the same thing over and over, do yourself a favor and define some snippets. It’s why you became a programmer in the first place, remember? Make the computer do the easy stuff for you, and you can focus on the hard stuff.

Building and releasing a Xamarin WatchOS app to TestFlight

I’m working on my third Apple Watch app built using Xamarin. In this post I’m going to describe how I develop and what some of the issues were that I encountered when testing and uploading to the App Store for beta testing.

The edit/build/run development loop

I build and run and test on the WatchOS simulator when doing general development, since this is the fastest, and I can debug apps on the WatchOS simulator

At the time of writing (early June 2020), the released version of Visual Studio for Mac has an error where I can’t run my apps on real devices, so I have switched to the preview channel which contains a fix for the issue.

When I’m ready to commit a new feature, I’ll test on a physical watch. To do that I build the iOS host app in release mode, and deploy that to my iPhone (I don’t deploy to the Apple Watch). On the iPhone I open Apple’s Apple Watch app, scroll down to my app:

I then force-quit this iOS Apple Watch app by swiping it away, and go back to my app as above, and it shows as updating (assuming I already installed it). 30-120 seconds later, and the app is installed on my watch and I’m ready to test it:

Uploading to TestFlight

To release to TestFlight I do a full build of the iOS app and then right-click and select Archive for Publishing:

Then I fire up Xcode, go to Window->Organizer and tap Distribute App to kick off the upload:

A word about certificates: I use fastlane match to manage my certificates and it is a lifesaver. When I want to set up my certificates on a new dev machine I just type

fastlane match developer

and that is it.

Blank complications (not a Xamarin issue)

While testing my Apple Watch complications (little icons on the watch face), I saw that some of them were displayed as tinted squares:

The images I’d created for an Apple Watch Complication were in the wrong format. The Apple documentation says “Complication images use only the alpha channel of the image.” … I don’t know about you, but this didn’t mean much to me.

I fixed this by downloading ImageMagick and using this command to turn the icons into black and white:

bash
magick convert Icon.png -threshold 50% Icon.png

I did this for all the non-color ones (Circular, Extra Large, Modular and Utilitarian).

Crash on launch #1

Even though my app ran fine on my Apple Watch when built and run locally, when I installed it and ran it from TestFlight it crashed immediately on launch.

I installed a sysdiagnose for watchOS profile to let me see the logs from the Apple Watch in Apple’s Console app, and got a clue. This error:

WatchOS app with Intent Exension: DYLD, dependent dylib '...libmonosgen-2.0.1.dylib' not found for TestFlight beta

I googled it, and found that I’d reported the same issue for another app I’d created, and there was a workaround.

Normally an Apple Watch app has two projects: The main project (which contains a storyboard and not much else), and an extension project where all the code resides. Because I want my app to be able to respond to Siri to create notes in my app, I have a third project highlighted in blue below, an Intents App Extension to handle Siri request on the Apple Watch:

Xamarin tries to share code between the assemblies, and it doesn’t work in this situation.

I needed to specify an mtouch flag in the build settings for both the extension, and the intents extension projects: --nodevcodeshare

This fixed the issue, but had unintended consequences.

Error uploading to TestFlight

I was pretty confident after fixing the issue with the shared code by enabling the nodevshare flag. Indeed the app uploaded to TestFlight, and I waited for it to be processed. I received this email:

From: App Store Connect <no_reply@email.apple.com>
Subject: App Store Connect: Your app "WatchGit" (Apple ID: 1234 Version: 2020.1 Build: 4) has one or more issues

Dear Developer, We identified one or more issues with a recent delivery for your app, "WatchGit" 2020.1 (4). Please correct the following issues, then upload again.  

ITMS-90389: Size Limit Exceeded - The size of watch application /Payload/WatchGit.iOS.app/Watch/WatchGitWatchOSWatchOSApp.app' (92MB) has exceeded the 75MB size limit. 

Best regards,
 The App Store Team

It seems as though by removing the sharing of code between my watch app’s extension, and the intents extension, I’d exceeded the max app size.

All was not lost however, I knew how to handle this one: I could tell the linker to link everything which would remove all code that wasn’t needed. You’ll see I changed the Linker behavior from Link Framework SDKs Only to Link All.

I also told the linker to skip my “Core” project, where I put all non platform-specific code that can be shared between iOS, WatchOS, MacOS, Android, UWP, Wear OS, Tizen and xBox (which is the whole point of using Xamarin in the first place, perhaps second only to being able to use C#):

Crash on launch #2

Once again, brimming with confidence, I uploaded my new build to TestFlight in the App Store. I was so confident I didn’t even test locally on a real phone, just in the Simulator. That was a mistake.

Once again it crashed on launch, but I was able to reproduce this crash on a local Apple Watch – I sprinkled Exception handlers in all the places I’d missed, and discovered the cause of a crash.

I use a Shared Project to share code between my iOS app and my WatchOS app:

specifically a class I use to to transfer information between the devices as serialized JSON:

  public class TokenInfoMessage : Message {
    public string AccessToken { get; set; }
  }

This was being linked away because I wasn’t explicitly instantiating it anywhere, instead I let the JSON deserializer do that:

var credentials = SimpleJson.DeserializeObject<TokenInfoMessage>(json));

The solution was once again straight-forward and well documented. If you want to tell the linker not to throw code away you can define and use a Preserve attribute:

using System;
namespace WatchGit.iOS.Shared {
  [Preserve(AllMembers = true)]
  public class Message {
    public string MessageKind { get; set; }
    public string MessageVersion { get; set; }
  }

  [Preserve(AllMembers = true)]
  public class TokenInfoMessage : Message {
    public string AccessToken { get; set; }
  }
}

Menus not working

On the real watch, when testing, I found that I got a crash when hard-pressing the screen to display my app’s menu:

ObjCRuntime.RuntimeException: Can't register the class xyz when the dynamic registrar has been linked away.

Fortunately I remembered getting this error in my first Apple Watch app, and remembered the magic formula to stop the dynamic linker from being thrown away in my main Interface Controller:

void DummyMethod() {
  Log.D("This is it");
}

public override void Awake(NSObject context) {
  base.Awake(context);

  // Forces dynamic linking not to be thrown away
  Action action = DummyMethod;
  try {
    ObjCRuntime.Runtime.ConnectMethod(action.Method, new ObjCRuntime.Selector("xyzzy"));
  } catch(Exception) { }

I don’t remember the details, but I guess the menu system must use dynamic registrar, and this code keeps it from being linked away.

My menus worked:

Finally, bliss, I could download my app from TestFlight and test it myself, and perhaps even one day make it available as public TestFlight beta…

Bonus – Beta review rejection

After a lot of testing I decided to make my app available as a public TestFlight beta. This involves adding a tester that isn’t one of the registered developers. Apple do some manual checks, which, while not as exhaustive as when you publish the app to the store, invariably they throw up an issue or two.

So I was not surprised when I received my rejection email. I was surprised at the reason.

App Store Connect

Dear Atadore SARL,

Your app, WatchGit 2020.1 (12), has been reviewed, but cannot be made available for TestFlight beta testing.

For details, or to contact App Store Review, visit Resolution Center in App Store Connect.

Best regards,
App Store Review

When I checked out the resolution center, expecting some deeply technical reason as to why it was not accepted, I saw that all they wanted was a video of the app in action:

Jun 2, 2020 at 7:56 AM
From Apple
Guideline 2.1 - Information Needed

We have started the review of your app, but we are not able to continue because we need access to a video that demonstrates the current version of your app in use on a physical watchOS device.

Please ensure the video you provide shows a physical iOS device (not a simulator).

Next Steps

I updated the TestFlight test details with a link to a video, and a couple of days later, the app was approved for public TestFlight testing!

What next?

Now comes the scary part. Will anyone care about my new app? I guess I’ll find out!

All these hassles do sometimes make me question my decision to use Xamarin for Apple Watch apps, especially when it seems as though there may be doubt about support going forward.

I am tempted to use Swift for my next app, just to get the experience. But I do love C# – going back to using closures to handle async task completion rather than using async/await seems like going back to the dark ages.

I realize I’ve not said what my app does. If you’ve made it this far, and you use GitHub and have an Apple Watch, please give my app, called WatchGit, a spin. You can use it to create issues using Siri, view issues, comment on issues, add emoji, close issues, assign issues, and change labels. It’s all about the issues.

Get it here.

I provide Xamarin consulting via my company Atadore. I’ve built and released apps using Xamarin for iOS, WatchOS, MacOS, Windows, Xbox, Android, Tizen, and WearOS. I’m engaged until the end of 2020, but feel free to contact me at damian@mehers.com if you have opportunities, or need a hand.