Thursday, July 27, 2017

WKWebView for Clueless Mac Programmers

I've been recently trying to package up my vector design web app Omber as a Macintosh app. Unfortunately, I had zero knowledge about Mac programming. Like, I never owned a Mac. I didn't even know how to get the cursor to go to the start of a line or skip a word using the keyboard without having to look up Stack Overflow. I tried using Electron, but after spending a long time going through various documentation to rebrand and package things (the nw.js documentation is so much better. The nw.js documentation is always such a joy to read compared to the electron docs), I wasn't too satisfied with the result. It worked, but it was sort of clunky, and I think there was some weird sandbox thing going on that caused file reading to sometimes work but sometimes not. With Windows, it makes sense to use Electron because the Windows default browser engine has weird behaviour and not everyone has the latest version of Windows. But on the Mac, everyone gets free OS upgrades to the latest version and the browser engine is fairly decent, so there's no need to include a 100MB browser engine with an app. So I figured I could whip together a quick Mac application that's just a window with a web view in it in about the same amount of time it would take to debug the Electron sandbox issues.

<rant about Mac programming>
Programming for the Mac is just like using a Mac. Apple hides important details and tries to force you to do things their way. Apple keeps changing things underneath you so all the documentation online or in books is always vaguely out of date. It's also expensive. I bought the cheapest Mac mini with 4GB RAM and a hard disk for development, thinking I could do mostly command-line stuff, but that's not the case. You really need to work from Xcode, and Xcode is a pig of a program that takes up a lot of RAM and is sort of slow. I almost immediately had to switch to using an external SSD on USB to get any reasonable responsiveness from my system. Apple is really trying to stuff Swift down everyone's throats, but I opted to go with Objective-C because of my Smalltalk background. It's not bad except the syntax is somewhat awful. My main issue with it is that part of what Smalltalk so productive is that it comes with an advanced IDE that's super fast and makes it easy to browse around large application frameworks to figure out how to use an API. Objective-C comes with an overwhelmingly huge application framework as well, but Xcode is slow and pokey and doesn't come with good facilities for diving through the framework. Code completion is not good enough. There should be a quick way to find how other people call the same method, check out the documentation for a method, and to check out the inheritance tree. Xcode is more of a traditional IDE with some code completion. It would be nice if Xcode actually labelled all of its inscrutable icons too. No one knows what any of those buttons mean, but using those buttons isn't optional either. The latest MacOS/OSX versions do include a web view, but I always get the feeling that Safari developers don't really understand web apps and want to discourage people from making them. I find that they only implement just enough features to Safari support their own uses and then lose all interest in implementing things in a general way that can have multiple uses. For example, for the longest time, they refused to implement the download attribute on links because Apple didn't need it, so why should anyone else need it? Then, when they did implement it, it initially didn't work on data-urls and blobs because they didn't understand how important that was for web apps. Similarly, the new WKWebView initially could only show files from the Internet and not load up anything locally, making it useless for JavaScript downloadable apps. Then, even when they did fix it, things like web workers or XMLHttpRequest are still broken, really limiting its usefulness. 
</rant about Mac programming>

Anyway, I found a great blog post that shows how to make a one window app with a web view in it. It lists every step, so it's easy to follow along even with no understanding of Mac programming. It worked for me, but Xcode has changed its default app layout to use storyboards so some of the instructions don't work any more, and it used the old WebView which is very limited. The new WKWebView is better because it allows for JIT compilation of the JavaScript, and it comes with proper facilities for letting the JavaScript send data to native code (the old web view required a bad hack to do that). So here are some updated instructions:
  1. Get Xcode and start it up
  2. Create a new Xcode project
  3. Make a MacOS Cocoa Application
  4. Fill in the appropriate application info, choose Objective-C for the language
  5. That should bring you to the screen where you can adjust the project settings
    1. If you want to run in a sandbox, I think you have to turn on signing. I think Xcode will take care of getting the appropriate certificates for you (I had already gotten them earlier).
    2. At the bottom of the General settings, under "Linked Frameworks and Libraries", you should add the WebKit.framework
    3. In the Capabilities tab, you can turn on the App Sandbox if you want (I think this is needed for the Mac App Store). Be careful, there seems to be a UI bug there. Once you turn on the app sandbox, you can't turn it off from the UI any more.
    4. If you do enable the App Sandbox, you also need to enable "Outgoing Connections (Client)" in the Network category. This is required even if you don't use the network. WKWebView seemed to have problems loading local files if the network entitlement wasn't enabled.
  6. Go to your ViewController.h, and change it to
  7. #import <Cocoa/Cocoa.h>
    #import <WebKit/WebKit.h>
    @interface ViewController : NSViewController
    @property(strong,nonatomic) WKWebView* webView;
  8. When using storyboards, the app delegate doesn't have direct access to the view, so you have to control the view from the view controller instead.
  9. Then go to your ViewController.m. Usually, you would draw a web view in the view of the storyboard and then hook it up to the view controller. Although this is possible with the WKWebView, all the documentation I've seen suggest manually creating the WKWebView instead. I think this might be necessary to pass in all the configuration you want for the WKWebView. To manually create the WKWebView, add these methods that show a basic web page:
  10. - (void)loadView {
        [super loadView];
        _webView = [[WKWebView alloc] initWithFrame: 
            [[self view] bounds] ];
        [[self view] addSubview:_webView];
        // Instead of adding the web view as a subview as
        // in above, you can also just replace the whole 
        // view with the web view using
        //     [self setView: _webView];
    - (void)awakeFromNib {
        [_webView loadRequest:
            [NSURLRequest requestWithURL:
            [NSURL URLWithString:@""]]];
  11. Now when you run the program, you should see the web page from there.
  12. The next step is to create a directory with all your local web pages that you want to show. Create a folder named html in the Finder (i.e. just a normal folder somewhere outside of Xcode)
  13. Drag that folder onto your project in the file list. Enable "Destination: Copy if needed" and "Added folders: Create folder references"
  14. You should now have a html folder in your project. You can delete the original html folder that you created earlier in the Finder since you no longer need it any more. (You can confirm that the html folder will be included in your project properly by looking at your project file under the Build Phases tab, and  the html folder should be listed in the Copy BundleResources section)
  15. Create an index.html file in your new html folder. Put some stuff in it.
  16. To show that page, go to your ViewController.m and change the awakeFromNib method to this:
  17. - (void)awakeFromNib {
        NSString *resourcePath = 
            [[NSBundle mainBundle] resourcePath];
        NSString *htmlPath = [resourcePath 
        NSString *htmlDirPath = [resourcePath 
            loadFileURL:[NSURL fileURLWithPath:htmlPath]
                [NSURL fileURLWithPath:htmlDirPath isDirectory:true]];