When research wouldn’t yield an answer, our mobile solutions lead made his own way.
We were recently asked by a client to implement 3D Touch in their iOS application. “No problem,” we thought, except that to this point the entire app had been written solely in Xamarin.Forms. To my surprise, Google had no answers or hints about implementing 3D Touch features (especially Peek/Pop) in Forms, and I thought that maybe we were doomed to re-write pieces of the application – or worse, hack in some horrible workaround. So we got creative. And as it turns out, a lot of the 3D Touch features are actually pretty simple to work into your Xamarin.Forms application. We thought we’d share some solutions here. Thanks for the inspiration, Google!
Note: we use a PCL for our shared code
It probably won’t come as a surprise that Quick Actions were simple to plug into the app. We just have to follow the Xamarin documentation to add the necessary data to our Info.plist to enable the feature:
We don’t like “magic strings” here, so next we created a static class to hold onto the identifier.
Then, you can add a public variable for an app shortcut item in your AppDelegate (we’ll get to the
IMessageSender in a second) and check the options in
Still following the Xamarin Docs, override
PerformActionForShortcutItem and create a completion handler for when an Item is selected.
In that snippet, you’ll see how we’re getting Forms into the mix using Messaging Center. For this simple example, we’ve just created an empty interface called
IMessageSender as a generic type to Subscribe and Publish from.
You’ll note that we shoved another static class in here to avoid “magic strings.”
OK – so we created our quick actions and we’re handling them properly in our AppDelegate, complete with message sending. Next, head over to your App class in your PCL so that we can listen for the messages and react appropriately. When the message is published by the AppDelegate, our event handler in the PCL (
QuickActionAddItem here) will handle the Quick Action!
Including Peek and Pop functionality in the app was my biggest worry – I wasn’t exactly sure how the feature needed to be implemented in either the native platform or Forms, and Google was still coming up empty. My concern was that I’d have to rip out entire views from the PCL and rebuild them in the native projects to get this feature working. Luckily, the reality wasn’t nearly that grim – though we will have to do a couple of strange things here to get this to work properly.
Let’s go over how we got Peek/Pop working on a ListView, allowing the user to “peek” at the detail page of a cell item using 3D Touch before “popping” it into view.
I found it easiest to abstract most of the 3D Touch logic into an abstract
BaseForceTouchViewRenderer class. (In our project, all views/view models inherit from BaseView and BaseViewModel, respectively.) We’ll get to
App.PushPageFrom3DTouch in a moment.
There are a few important things to note in this class:
1. Our app uses a NavigationPage as its main navigation structure. In order for our Forms app to continue to function as expected, we needed a reference to our NavigationPage in our Base ForceTouch class. This mostly has to do with the Navigation bar and Toolbar items not appearing on Pop in our case. (This is why you see us using our own navigation logic in
CommitViewController rather than the suggested
ShowViewController call in other sample documentation.)
2. We’re holding onto a reference to the Page of our current view. You’ll see why in the subclasses, but it will help us get a hold of our individual subviews in the Forms implementation of the view.
3. We’re also holding a reference to the ViewModel for our current view. I don’t love having our PageRenderer know about our ViewModel, but we’re going to need to figure out later what item the user has selected by an index, and our ViewModel has that collection.
4. We’re having this Base class handle all of the implementation of
IUIViewControllerPreviewingDelegate, so all that logic is in one place. Subclasses will use the abstract methods to implement the feature rather than the standard interface.
Before we look at a subclass, however, let’s see what we need to do in the Forms part of our application to make this work.
That’s it as far as the PCL goes — we’ll need to hold a reference to the native UITableView somewhere in the Forms view, and this is the most reliable solution at runtime I’ve found so far.
The renderer for this is also pretty simple:
We’re just storing that native UITableView as a generic object here in our Forms ListView for now. We’ve also added a static method to recover that native object in this class, just so that nasty cast is in one place and hopefully future devs won’t have to deal with it.
The strangest behavior that comes out of all this occurs when the app is displaying a preview of the ViewController when the user is Peeking. It seems that behind the scenes, the Peek actually resets our Xamarin.Forms Application’s MainPage to the ViewController being previewed. This caused me issues with my MasterDetail and Navigation pages down the line, so I wrote this hacky static method to correct the issue in my Application subclass:
MainMasterDetailPage is the instance of the
MainPage that I set in the constructor of my Application subclass, and it’s a MasterDetail page that will always have a NavigationPage as its Detail property. I need to reset the MainPage here to correct the issue described above. The only adverse effect I’ve noticed from this is the app ResourceDictionary being reset, so to correct that I’m reinitializing my ResourceDictionary using a StyleBuilder class I wrote that simply builds the app’s Global Styles. This is the nastiest part of getting this to work, but it’s working without any noticeable issues in the UX as of Xamarin.Forms 2.2.
That’s all for the ListView customization and PCL work. Let’s take a look at a subclass of
The simplicity of this is the fruit of the base class abstraction – now any developer jumping on this project can quickly and easily spin up a Peek/Pop implementation if they need to. But let’s go through we’re doing here anyway.
In the constructor, we’re grabbing the ListView we want 3D Touch to interact with by its declared
x:Name in the XAML view defined in the PCL. Our base class already grabbed the instance of our Forms page, so we just have to call FindByName and we’ve got what we need.
All that’s needed then is to implement the abstract members of the base class.
ShouldHandleForceTouchForLocation just tells our base class whether or not to react to 3D Touch depending on the coordinates of the interaction. In this case, we’re returning
false if the touch is not within the bounds of the
GetPreviewControllerForLocation expects us to return what controller we’ll be peeking and popping with the user interaction. You can see we’re calling that static
NativeTableViewForControl we showed above to get the UITableView – now we finally get to explain why we had to jump through that nasty casting hoop! The method that makes this feature work in a native project is
IndexPathForRowAtPoint. It’s a lovely method on UITableView that allows us to get an index path for a table cell based on a coordinate. Forms’ ListView does not have any method of the sort, so in order to figure out where our user is “peeking,” we needed to get the native control and call that wonderful, wonderful method.
There was one other thing that I got snagged on as I was trying to wrap this up. The coordinate I was passing into
IndexPathForRowAtPoint was always returning the wrong item – i.e., the coordinates were incorrect. This was likely an issue for me because my ListView was not taking up the entire Page, but by calling ConvertPointToView, I was able to get a correct coordinate and get an accurate index back.
Finally, we call
GetPage<TViewModel, TView> – a static method added to our Forms App subclass to get the detail page. Since we’re setting that to the base class’
PageToCommit member, we don’t have to worry about popping the view – the base will handle that for us.
This solution isn’t perfect (or even pretty, not yet), but it works for us for the present, and we wanted to stay true to our open source roots by sharing it with you. While your project may have different needs and your mileage may vary, this should be a good starting point to hacking together your own 3D Touch implementation in your existing Xamarin.Forms application. If you find a more elegant solution in your own project, make sure to let us know!