Posted by Will Thomas on Jan 05, 2015

Faking native interfaces in an iOS web view, part 1

Apple’s approval time on the app store usually takes just under a week. When launching an app for the first time it is essential to be able to react quickly to bug reports to avoid getting negative reviews. Some time a week is too long to wait.

By using web views you can dynamically change the content on your app without having to go through the rigmarole of the approval process. The problem with this is - if you don’t want a disjointed app experience - you may find yourself having to recreate the native iOS interface.

There are lots of front end libraries out there already for creating iOS style interfaces for example Chocolatechip UI. However integrating a library like this with an existing project can mean lots of compromises and changes with your code. Not only that but reducing overhead on iOS web views is essential, as iOS7 and lower don’t support Nitro or asset caching. Sometimes we just don’t need all the bells and whistles that these libraries offer.

I’ve collected some pragmatic advice for dressing up existing web views to look native with minimal changes to your existing templates. This time we’ll be looking at how to detect whether your site is running in your app’s web view using Swift and PHP, and styling lists to look native using LESS.

Detecting your app

The first thing to do is set a reliable way to check to see if your web view is being viewed, specifically viewed in your app’s webview. One way to do this is by modifying the user agent string with your app name that you can use to create conditionals in your web app.

let userAgent = UIWebView()
    .stringByEvaluatingJavaScriptFromString("navigator.userAgent")!
NSUserDefaults
    .standardUserDefaults()
    .registerDefaults(["UserAgent": userAgent + " [MyApp]"])

As previously stated, it’s a good idea to try and eliminate as many overheads as possible. You might want to use conditionals in your web app to only use custom stylesheets and JavaScipt needed for your web views. Here is a simple example using PHP.

function isApp() {
  return strpos($_SERVER['HTTP_USER_AGENT'], '[MyApp]') !== false;
}

List views

List views are a mainstay of many iOS apps, you’re bound to run into these at some point. Remember that lots of iPhones now have retina support and - since iOS7 - Apple has started using “half pixel” keylines between list items. If you’re using half pixel keylines on an older evice, they won’t appear at all! We can use pixel density media queries to detect what thickness borders we should be using.

border-bottom: 1px solid #C8C7CC;

@media (-webkit-min-device-pixel-ratio: 2),
(min-resolution: 192dpi) {
    border-bottom: 0.5px solid #C8C7CC;
}

Typically list views also feature borders that are offset from the left edge of the screen and flush with the right. However when in the hover or active states this border is carried over to span the full width of the screen - this is a detail even Apple themselves have missed on their own web views.

iOS native and web view active states

Native active state on the left and web view on the right.

Here is an example of how you recreate the correct active states using LESS. First a quick tweak to the typography.

.ios {
    font-family: 'helvetica neue', helvetica, arial, sans-serif;
    color: #000;
    font-size: 17px;
    line-height: 26px;
}

Here we can use the anchors bottom border to create the keylines between list items, and the margin on the anchor to create the left offset. On the active state we replace the margin with a padding to allow the keyline to span the full width of the list. Finally we add a top border and a negative margin of half a pixel to the anchor so it overlaps the offset border of the list item above it.

ul {
    list-style: none;
    margin: 0;
    padding: 0;
    background-color: #fff;
    border-top: 0.5px solid #C8C7CC;
    border-bottom: 0.5px solid #C8C7CC;
    li {
        a {
            display: block;
            padding: 9px 0 8.5px;
            margin: 0 0 0 15px;
            border-bottom: 0.5px solid #C8C7CC;
            color: #000;
            text-decoration: none;
            -webkit-tap-highlight-color: rgba(0,0,0,0);
        }
        &:last-child {
            a {
                border-bottom: 0;
            }
        }
        &.active {
            margin-top: -0.5px;
            border-top: 0.5px solid #C8C7CC;
            a {
                padding-left: 15px;
                margin: 0;
                background-color: #D9D9D9;
            }
        }
    }
}

As you can see in the example above we’re using a class for the active state as opposed to the CSS pseudo class :active. This is because in iOS the active state persists after the on touch end event, we’ll need to create this behavior using Javascript and jQuery.

We want to active state to appear if the anchor is touched, but we want to be able to cancel that action if the touch event moves 10px vertically either direction away from the point of origin. This allows a touch event to pan the page without accidentally selecting something.

activeStates = function() {
    var $listItems = $("li");
    $listItems.on("touchstart", function(e) {
        var $touchedItem = $(this),
            initialY = e.originalEvent.changedTouches[0].pageY;
        $listItems.removeClass('active');
        $touchedItem.addClass("active");
        $(document).on("touchmove", moved);
    });
};

Here we’ve created our active states function, cached a selector for all our list items and created an on touch start function. On touch start we cache a selector for the touched item, recorded the touch point of origin, remove all active classes and apply a active class for the touched item. We also have added a new event, on touch move. Now we need to add the moved function.

Here we compare the touch point of origin from the new on move event with our original point of origin we stored as “initialY”, and see if there is a difference of 10px. if so, we remove the active class and unbind the on move event that was originally set with the touch start event.

function moved(e) {
    var y = initialY - e.originalEvent.changedTouches[0].pageY;
    if (Math.abs(y) > 10) {
        remove();
    }
}
function remove() {
    $touchedItem.removeClass("active");
    $(document).off(moveEvent, moved);
}

Next time we’ll look into how to spoof native text input fields, buttons and checkboxes.

Jobs at MyBuilder

We need a brilliant designer, who loves the web and can implement their designs with solid HTML and CSS skills.

View vacancies
comments powered by Disqus