5 Rules for Mac Developers

The Makings Of Proxy.app

Fri Jul 17 2015 14:35:31 GMT+0100 (BST)

In the last blog post, as well as on twitter, I promised that we will start pushing more in-depth articles on how we go about doing things internally - you know programming, research and development, etc. As a young company you learn the tricks of the trade by trial and error, which is not always optimal. While it is not guaranteed that you will ever adopt the solutions other people are writing about, in many cases they could be very insightful.

So, without further ado, here is a list of 5 rules to follow when writing Mac OS X desktop applications. We've learned all of these things the hard way, so you don't have to.

1. Write Small Parts, Test and Integrate

There is no special way of writing software. You have to start from somewhere and grow it from there. Once you have the basics going you will end up adding features, improving old features, fixing bugs and adding even more features. The problem is that once you start adding features, testing gets more complicated especially when these features are not immediately accessible.

For example, Proxy.app has a dialog that is presented to the user when intercepting requests and responses. This dialog is only triggered when the proxy is put in interception mode and only when there is some data coming in. This sort of feature would require doing a lot of back and forth between the proxy, the proxied application, XCode, etc. It is not an optimal solution but there is a better way of tackling this problem.

Proxy.app Interception Screen

So, instead of directly adding the feature and dealing with the complexity of your setup, it is much better to start a new empty project in XCode. Copy the most basic parts you will require and develop the feature as a standalone application. Testing will be easier because you don't need to deal with all other parts of the application and you can start from a fresh canvas, which is always, well, refreshing.

Once your feature is complete in a semi-working state, move the relevant parts into the main project and integrate. On average we found out that this approach has a higher turnaround then working directly on the entire software solution. We think the reason is down to the level of hesitation not to mess up other working parts.

2. Mix Swift With Objective-C

Swift is a great language and we have adopted it in all of our solutions. However, Swift, regardless how cool and safe as a language it is, is not a replacement for the mountain of examples and working code that is available online and targeted towards Objective-C. The key is to mix them both. Use Objective-C or Swift depending on your situation.

For example, Proxy.app has a lot of low level components which deal with some BSD apis. Now all of them are available in the sandboxed version of the application but they are there. While, it is possible to write all of them in Swift, porting is not straightforward. Swift enforces certain level of type-safetiness which are not only unnecessary but often annoying especially when working with C code. These parts in Proxy.app are written in pure C and very often Objective-C for encapsulation and easier access from other components.

Proxy.app code

All of our projects have both. This works much better than trying to solve everything in Swift - we tried and we failed.

3. Do The Hard Things First

It is easier said than done but it is the truth. As a developer you may decide to avoid doing the hard things first simply because you have no idea what to do about them initially. However, this situation never changes. Nobody has any idea initially unless they have dealt with the same problem before. So, we always start with the hardest part first and once it is done we move on to the easier bits. By the end of the project we are dealing mostly with boring stuff.

Proxy.app V1 was not built like that. We spent a lot more time on the user interface than on the actual proxy. The proxy was added later which wasn't a good idea because the user interface had be reshuffled to accommodate certain design decisions. We started with CoreData. We wrote the interface on top of the CoreData model. Then we wrote the proxy to interact directly with the CoreData model. Sounds logical but it also meant that we had to use CoreData even for the HTTP Request and Response interception screen, which wasn't very nice because big responses may update the user interface while you were typing your changes. It was confusing.

ProxyLite screen

With PAV2 we started with the core of the problem - the proxy. Using principles 1 and 2 - we created a standalone proxy tool that has all features, including SSL interception, dynamic certificate generation, etc. Everything. That was the hardest bit. The tool was called ProxyLite. Once we knew that ProxyLite was working, we moved the relevant bits to the main project and integrated them. The ProxyLite project was decommissioned afterwords.

4. Keep Things Simple First - Add The Details Later

This point sort of builds on top of the previous one. The bottom line is that you don't want to add the details early on. You want to be as spartan as possible because it is easier to trace bugs. Don't work on the user interface unless the core of the product is complete. Leave the details for later once you are certain about what you want.

ProxyLite storyboad

This rule is especially true for Cocoa application which are notorious difficult to customize. I mean, you can spend days figuring out how to extend some system components to behave or display differently only to find out that they are never meant to be customized. You can play with these stuff later when you have more time.

5. Don't Take Shortcuts - Always Assert

One of the added benefits of using Swift is that you are sort of forced to do a lot of type checks when you deal with low-level code. In Objective-C everything is a NSObject so you can easily take the shortcut and cast to whatever you want. While this saves time, it is a nightmare for bugs. Swift uses optional unwrapping to provide a much safer casting mechanism. You don't have to assume that things are in some way. You can assert that they really are.

In addition to that. Always break your code as yearly as possible. My favorite function is fatalError("your message here"). I use it all the time when I write code that assumes too much. Stick a fatalError to catch unexpected conditions. While your program may break a lot initially, it is a godsend overtime because it helps you iron out a lot of the bugs.

The way I personally do that is by using a self-written debug expression I try to use everywhere I can. This is what it looks like more or less:

#if DEBUG
    private func debug(block: (() -> Void)) {
        block()
    }
#else
    private func debug(block: (() -> Void)) {}
#endif

In my code I will write something like this:

debug {
    if length != expectedLength {
        fatalError("unexpected length")
    }
}

Sure you can use the builtin assert but this gives you a greater control with closures and it reads nicely. I don't know how well this is optimized without the DEBUG flag on but in my experience it works very well. I use a couple more helpers like this one to catch unexpected conditions in the program early on.

Alright, I know that this article is not going to make your a better Mac OS X developer over night, and you may say that most of the stuff I wrote here are BS depending on your level of experience, but it is what it is. This is how software is written at Websecurify. Without this, we wont be able to push so much code, so regularly. Now you know.

Comments Powered ByDisqus