UIWebViews are a handy way to show web content in an iOS app. They are especially useful for showing PDFs that are stored remotely. Apple largely treats UIWebViews
like a black box: Documentation warns that they should not be subclassed. So what if you want to customize how they show PDFs? Recently, I went after two customizations to change the default UIWebView
behavior.
- Scale the PDF so the entire thing fills the view–whether or not it is a landscape or portrait PDF.
- Get rid of the default drop shadow around the PDF.
They both required some tinkering, so I’m sharing them in case others need to do something similar.
Customizing How the PDF is Scaled
Default behavior
There’s a property on the UIWebView called scalesPageToFit. If you set it to true, the PDF will start off scaled so that its width fills the width of the view. You can set it to true in your code or in the UIBuilder. And right out of the box, you get pinch and zoom. It’s great!
Desired behavior
Well, it’s great until you have a tall and skinny PDF and you want the whole PDF to show up when the user gets to that view. In other UIViews
, you can set the contentMode property to AspectToFit
to do this. However, the UIWebView
actually has a couple of layers in the view hierarchy between it and the PDF we’re scaling.
Solution
After a lot of poking around in the subviews of my UIWebView
, UIBuilder, and StackOverflow, I ended up writing a little function to scale the PDF so that the whole PDF is visible if it’s too tall and skinny to fit with the scalePageToFit
default zoom. Here it is:
func updateZoomToAspectToFit(webView: UIWebView) {
let contentSize: CGSize = webView.scrollView.contentSize //PDF size
let viewSize:CGSize = webView.bounds.size
let extraLength = contentSize.height - viewSize.height
let extraWidth = contentSize.width - viewSize.width
let shouldScaleToFitHeight = extraLength > extraWidth
let zoomRangeFactor:CGFloat = 4
if shouldScaleToFitHeight {
let ratio:CGFloat = (viewSize.height / contentSize.height)
webView.scrollView.minimumZoomScale = ratio / zoomRangeFactor
webView.scrollView.maximumZoomScale = ratio * zoomRangeFactor
webView.scrollView.zoomScale = ratio
}
}
All this does is check if the view is more overly-tall than overly-wide compared to the view’s content size. If it’s overly-wide, we don’t have to do anything because the scalePageToFit
property magically takes care of that situation. If it’s overly-tall, we find the ratio
of the view’s height to the content (a.k.a. the PDF I care about). That ratio is then assigned to the zoomScale
. That zoomRangeFactor
is a magic number that you can play around with. It needs to be greater than one to allow the user to pinch and zoom.
For this to work, updateZoomToAspectToFit
needs to be called from the webViewDidFinishLoad(webView: UIWebView)
callback. For webViewDidFinishLoad
to be called, the UIWebView
needs to be its own delegate: myWebView.delegate = self
Hiding the Drop Shadow
Default behavior
I was surprised to see that there was a drop shadow around the PDF in the UIWebView
out of the box.
Desired behavior
The designer I was working with was not only surprised, but displeased. So my mission was to get rid of the drop shadow. I tinkered in UIBuilder to no avail. I dissected the view hierarchy in debug mode. StackOverflow suggestions were full of code that goes below Apple’s documentation and could break with the tiniest update.
Solution
Long story short, I found that I could legally (according to Apple’s documentation) traverse the subviews of the UIWebView
and set the shadowOpacity
attribute on all of them:
func removeShadow(webView: UIWebView) {
for subview:UIView in webView.scrollView.subviews {
subview.layer.shadowOpacity = 0
for subsubview in subview.subviews {
subsubview.layer.shadowOpacity = 0
}
}
}
Like updateZoomToAspectToFit
, in order for this to work, removeShadow
must be called from the webViewDidFinishLoad(webView: UIWebView)
callback. That’s because the subviews won’t be available until after the PDF has loaded. For webViewDidFinishLoad
to be called, the UIWebView
needs to be its own delegate: myWebView.delegate = self
.
The Bigger Picture
Figuring these two customizations improved my fluency with iOS’s UIKit. I enjoyed the challenge of figuring out how to go just underneath the hood but not hack it so badly that it becomes fragile and susceptible to silently break in updates. This is what I love about making software at Atomic–we can take the time to do things right, and share when we figure out how to accomplish something.