Popover menu over cards containing WebKit views on iOS 13
By: João R.G. Sampaio
Tags: Webview, Webkit, Swift, iOS, Swift Programming

One new feature that came with the new iOS 13 is the introduction of the stack of card views when we push View Controllers on top of each other. It is quite a neat feature, in my opinion (depending on what kind of app you’re doing, of course), because you can visualise, at the top of the screen, the stack of views that you’ve been through and easily swipe them down to go back to the previous one when needed, removing the need of implementing a back button somewhere on your view, stealing a bit of your app’s precious screen space.

But then, there is one small problem that came with that.

If your app has embedded web views inside it (like many banking or payment apps usually have), you can end up having a crash in a situation that you didn’t have to account for before. This happens when you try to open a web link that’s a file input. When you do that in a regular full screen view (not a card), iOS will present you with a popover menu that looks like the one below:

The traditional iOS popover menu for file input fields coming from the bottom of the screen.

Although, if you try the same on a web view inside a card, you’ll get the following a crash with the message:

*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Your application has presented a UIDocumentMenuViewController (<UIDocumentMenuViewController: 0x105223de0>). In its current trait environment, the modalPresentationStyle of a UIDocumentMenuViewController with this style is UIModalPresentationPopover. You must provide location information for this popover through the view controller's popoverPresentationController. You must provide either a sourceView and sourceRect or a barButtonItem. If this information is not known when you present the view controller, you may provide it in the UIPopoverPresentationControllerDelegate method -prepareForPopoverPresentation.'

According to this message, the iOS doesn’t know where to position your popover menu over the app. I don’t know why they don’t just present it the way it was before but, since that’s not an option, let’s fix it!

First, let’s bring that web view to a View Controller inside a card and start our work.

Our WebKit view resting inside a card, patiently waiting for something to happen.

OK! Now, according to the message, what we are lacking here is basically the location of our popover message, and we can achieve this by making our View Controller conform to the UIPopoverPresentationControllerDelegate protocol. With that, we need to implement the method prepareForPopoverPresentation( _ popoverPresentationController: UIPopoverPresentationController) to handle the properties that are lacking for our popover menu to appear.

One problem here is that we are not the ones declaring a variable for our UIPopoverPresentationController but it is coming from the web view itself, so we need to find a way to tell the controller that our View Controller will be its delegate for this situation. We can achieve that by overriding the present method from our View Controller to be able to set it as the delegate of the popover.

This will set the popover from the new UIDocumentMenuViewController to be presented to have our View Controller as a delegate.

NOTE: when adding this code on your project, you’ll probably get a warning from Xcode informing that UIDocumentMenuViewController was deprecated in iOS 11 and that you should use UIDocumentPickerViewController directly, but, if you do that, it won’t work because the WebKit tries to present a UIDocumentMenuViewController (as you can read on the crash error message from the beginning of the article). It is weird and it is probably a bug on iOS or on WebKit. So, we can either stick to this warning or, if you are sure that this View Controller of yours will handle only web view stuff, you can remove that type check from it to kill the warning.

Moving on, after setting our View Controller as the delegate for the popoverPresentationController , we can then implement the required delegate method like this:

Try to run the app again and you’ll see this as result now:

The popover menu is now at the top of the screen. Not exactly an expected behaviour.

Nice! The crash doesn’t happen anymore, but having the popover menu hanging over there on the top of our screen is not quite optimal. The ideal way would be for it to be presented close to where we the user performed the action to open it. In other words, close to where the user tapped the screen.

For this to happen, we will have to intercept the user tap gesture and store its location so we can use it to position our menu on screen later. We can add a tap gesture recognizer to our web view as the code below shows:

One problem we will have with that is that, no matter how much we tap the web view, our webViewTapped method will never be called. That’s because our web view is already intercepting our taps to perform the necessary web view actions and, if we want to have our View Controller to recognize them simultaneously, we have to make our View Controller conform to the UIGestureRecognizerDelegate protocol and add the following method:

Now, we can just pass the position information to our popover menu through the prepareForPopoverPresentation method:

Then, we finally have a more pleasant result:

The popover menu pointing to the place on screen where we tapped. That feels way nicer!

Well, that was a bit of work but at least now we got rid of that annoying crash that appeared out of nowhere with the introduction of iOS 13’s cards. You can use that same effect if you want that behaviour present on your web views shown inside full screen views as well. Anyway, if you want to check how the whole View Controller looks like after all these alterations, the source code of the implementation of it can be find in this gist link.

Did it work for you? Do you have any other ideas or suggestions to make it different? Let me know in the comments below.