Blog

Upgrading from UIWebView to WKWebView

From later this year you won't be able to submit new app releases containing the deprecated UIWebView. Apple recommend swapping these for the new WKWebView, which is all fine, but they don't give any useful examples, particularly in Objective-C, so here are some tips.

If you are targetting iOS before 11, you can't add this view using Interface Builder! If you try you will get an error, and your project won't compile. So instead you'll need to add it programmatically with your ViewController.

In the header file switch out the UIWebViewDelegate for the new WKNavigationDelegate:

@interface MyViewController : UIViewController <WKNavigationDelegate>

and add in a property to hold the new WKWebView:

// Needs to be added programmatically
// See: https://stackoverflow.com/questions/46221577/xcode-9-gm-wkwebview-nscoding-support-was-broken-in-previous-versions
@property (strong, nonatomic) WKWebView *webView;

Then in the main ViewController file update your viewDidLoad function:

- (void)viewDidLoad {
  [super viewDidLoad];
...
  // Create the new configuration object to set useful options
  WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
  configuration.suppressesIncrementalRendering = YES;
  configuration.ignoresViewportScaleLimits = NO;
  configuration.dataDetectorTypes = WKDataDetectorTypeNone;

  // Set the position - Use the full page, but above my tabBar
  CGRect frame = self.view.bounds;
  CGFloat tabBarHeight = frame.size.height - self.tabBarController.tabBar.frame.origin.y;
  frame.size.height -= tabBarHeight;

  // Create the new WKWebView
  _webView = [[WKWebView alloc] initWithFrame:frame configuration:configuration];

  // Set the delegate - note this is 'navigationDelegate' not just 'delegate'
  _webView.navigationDelegate = self;

  // Add it to the view
  [self.view addSubview:_webView];
  [self.view sendSubviewToBack:_webView];

  // Load a blank page
  [_webView loadHTMLString:@"<html style='margin:0;padding:0;height:100%;width:100%;background:#fff'><body style='margin:0;padding:0;height:100%;width:100%;background:#fff'></body><html>" baseURL:nil];

...

Then you can load up some content. I do this in viewWillAppear, and here's how to load a file called filename.html from bundle resources:

- (void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
...
 
  // Load HTML File
  [_webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"filename" ofType:@"html"] isDirectory:NO]]];

...

UPDATE: In iOS 13, Xcode prefers you to use - (WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL instead, so you can also load the local file using:

  // Load HTML File
  NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"filename" ofType:@"html"] isDirectory:NO];
  NSURL *dir = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"filename" ofType:@"html"] isDirectory:YES];
  [_webView loadFileURL:url allowingReadAccessToURL:dir];

I have an activity indicator on the page, which spins until the content is first loaded. This helps if the app is ever a bit laggy. This is added via Interface Builder and centered in the view. Once the content first loads I want to hide it. I used to use - (void)webViewDidFinishLoad:(UIWebView *)webView, but we need to swap this out for - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation.

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
  [UIView animateWithDuration:0.15 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
    self->_activityIndicator.alpha = 0.0;
  } completion:^(BOOL finished){
    if (finished) {
      self->_activityIndicator.hidden = YES;
      self->_activityIndicator.alpha = 1.0;
      [self->_activityIndicator stopAnimating];
    }
  }];
}

I also like to track clicks on the page, because this is just an easy way to display nicely formatted text, and I want external web links to open in the default browser. This used to be done using - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request  navigationType:(UIWebViewNavigationType)navigationType, but now you need to use - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler instead.

Here I use the default browser to open the external webpage if the link contains 'http'. It would be more robust to check the first four characters, but I know this will work for my app.

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
  NSString *url = [NSString stringWithFormat:@"%@",navigationAction.request.URL.absoluteString];
  BOOL httpRequest = [url containsString:@"http"];
  if (navigationAction.navigationType == WKNavigationTypeLinkActivated && httpRequest) {
    [[UIApplication sharedApplication] openURL:navigationAction.request.URL];
    decisionHandler(WKNavigationActionPolicyCancel);
  } else {
    decisionHandler(WKNavigationActionPolicyAllow);
  }
}

Reading/decoding BSON files from a MongoDB mongodump backup

MongoDB is an extremely useful database format, especially for projects where the scope is developing and changing. The 'documents' that represent items of data don't need to have identical elements, despite belonging to the same table.  This makes evolving requirements much easier to implement.

In a production environment, the live database can be backup up with a simple mongodump command, and some service providers implement regular backups automatically. So if you need to delve back in time you can expand the backup and take a look inside.

The actual data documents are saved in BSON format, one file per Collection. Opening these files up as they are, reveal some of the keys and string values, but much of the useful data and structure is encoded and therefore unavailable.

To tranform this data into a more useful format the best tool is bsondump, which is provided as part of the command line tools available from MongoDB. Either get it from MongoDB's Server download page, or on MacOS you can use Homebrew.

brew tap mongodb/brew

brew install mongodb-community@4.2

This will include the bsondump function. I always like to make web tools to provide a nice UI for actions like this, so you can lever php's exec function to run the command for you on your localhost server, using something like:

if (!empty($_FILES['file']['tmp_name'])) {
  $original = $_FILES['file']['name'];
  $output = 'output.json';
  if (!empty($original)) {
    if (substr($original,-5) == '.bson') $output = str_replace('.bson', '.json', $original);
  }
  $command = '/usr/local/bin/bsondump --outFile=' .$output. ' ' . $_FILES['file']['tmp_name'];
  exec($command, $retArr, $retVal);
  $json = file_get_contents($output, FILE_TEXT);
}

Now you can exploit regex tools to make custom searches, of a string stored in $seek, like:

$results = array();
if (!empty($seek)) {
    $matches = array();
    preg_match_all('/^.*?'.$seek.'.*?$/mi', $json, $results);
}

The HTML will include the input form and ouput the results, along the lines of:

<form method="post" enctype="multipart/form-data">

<div class="input-item">
<label for="seek">Seek String</label>
<input type="text" name="seek" id="file" size="100" value="<?php print ($seek)?$seek:''; ?>" /><br>
BSON to decode:
<input type="file" name="file" id="file" size="100" value="" />
</div>

<div class="input-item">
<input class="submit" type="submit" name="submit" id="submit" value="Seek" width="180" />
</div>

<!-- Results -->
<div class="output">
<?php

if (!empty($results)) :
  print '<p>Found: ' . count($results[0]) . '</p>' . "\n";
  print '<textarea rows="20">';
  foreach ($results[0] as $i=>$r) print $i . ':' . "\n" . $r  . "\n\n";
  print '</textarea>' . "\n";

  print '<p>Decoded</p>' . "\n";
  print '<textarea rows="20">';
  foreach ($results[0] as $i=>$r) {
    print $i . ':' . "\n";
    $d = json_decode($r, true);
    if (!empty($d)) {
      foreach ($d as $key=>$val) {
        // Customise output based on $key and $val, or just continue if $key is now wanted.
        print $kkey . ':' . $val . ', ';
      }
    }
    print "\n\n";
  }
  print '</textarea>' . "\n";
endif;

?>
</div>
</form>

iOS 10 Privacy Settings

Since the iOS upgrade apps need to register their use of various phone features, which if you are developing an old app can cause unexpected results.

Our TheatreSound app suddenly started to create two strange results:

  • The Media Picker wouldn't work. It would just open as a blank screen and then immediately close again. No warnings or notifcations were posted on the phone.
  • When connected to Xcode, the same procedure produced:
    2017-01-16 22:16:37.612964 TheatreSound[271:10452] [MC] System group container for systemgroup.com.apple.configurationprofiles path is /private/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles
    2017-01-16 22:16:37.620851 TheatreSound[271:10452] [MC] Reading from public effective user settings.

The media picker should have been fine. It was called via a simple IBAction

- (IBAction) showMediaPicker: (id) sender {
    MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes: MPMediaTypeAnyAudio];
    picker.delegate = self;
    picker.allowsPickingMultipleItems = YES;
    picker.showsCloudItems = NO;
    picker.prompt = NSLocalizedString (@"Choose loops", @"");
   
    [self presentViewController:picker animated:YES completion:nil];
}

The first of these problems was harder to diagnose because no warnings were posted. But I had a feeling the problem might be to do with privacy settings, having had isses with connection to the microphone previously. Also, reviewing the Apple Media Player API reference, (the parent of the MPMediaPickerController), there is posted a notice saying:

Important  Starting in iOS 10, accessing a user's media library requires the user's consent. You must provide the reason for accessing the media library in the app's info.plist.

The solution is to add the following into the app info.plist (Property List) file:

    <key>NSAppleMusicUsageDescription</key>
    <string>The library is used to source tracks, loops and sounds for your shows</string>

This information can also be added via Xcode, by clicking on the app name at the top of the file list tree to bring up the various project settings, selecting the main app target from TARGETS, and selecting the 'Info' tab. Expand 'Custom iOS Target Properties' and hover over the bottom line to bring up the + - buttons and click on the +. Then scroll down the list until you find the items starting with 'Privacy - '.

Xcode plist privacy settings selection

After that it all works as expected again.

Removing security exceptions in Firefox 44.0

Firefox warns you when you try to visit a website via HTTPS and the site does not have a valid security certificate, or the certificate is expired or missing. It's sometimes helpful to bypass this security alert by adding an exception, which can be temporarily or permanent. If you added the exception permanently it ignores the certificate forever; this could be a security risk.

You can remove permanent exception by doing the following:

  1. Open Firefox preferences and click on the Advanced page.
    Firefox preferences - Advanced
  2. Select the Certificates tab, and then click on the View Certificates button.
    Firefox preferences - Advanced - View Certificates
  3. Find the certificate you want to delete - I find the servers tab the most useful for this - click on it and then click the Delete... button at the bottom
    Firefox preferences - Advanced - View Certificates

Job done!

1 & 1 email rejection

We were having a lot of trouble with users on 1&1 not receiving emails from our server. The answer was down to the MX records pointing to a CNAME not an A record.

The bounce report looks like:

Action: failed
Status: 5.0.0
Remote-MTA: dns; mx01.1and1.co.uk
Diagnostic-Code: smtp; 550-Requested action not taken: mailbox unavailable 550
   invalid DNS MX or A/AAAA resource record

Simply, switch from CNAME to A and the problem goes!

http://jheslop.com/2009/07/31/1and1-tightens-up-email-spam-rules/