[indistinct chatter]

about · github · twitter

Airpin Diary #1: Inversion of Control

For my app, Airpin, which is a Pinboard client, I have a top level view controller which segregates a user’s bookmarks into several obvious categories: All, unread, untagged, public, and private. When a user taps one of the categories, they are taken to another view controller that shows them the appropriate list of bookmarks for the category that they selected.

When I initially implemented this list of bookmarks (BookmarkListViewController), I created a view model that would take in a category, switch on it, and perform the a specific operation to return the correct list of bookmarks for the selected category, like so:

class BookmarkListViewModel: BaseViewModel {
    init(category: CategoryViewModel.Category) {
        self.category = category
    }
        
    var title: String {
        switch category {
        case .all:
            return "All"
        case .unread:
            return "Unread"
        case .untagged:
            return "Untagged"
        case .public:
            return "Public"
        case .private:
            return "Private"
    }
    
    func fetchBookmarks(completion: @escaping () -> Void) {
        switch category {
        case .all:
            fetchAllBookmarks(completion: completion)
        case .unread:
            fetchUnreadBookmarks(completion: completion)
        case .untagged:
            fetchUntaggedBookmarks(completion: completion)
        case .public:
            fetchPublicBookmarks(completion: completion)
        case .private:
            fetchPrivateBookmarks(completion: completion)
        }
    }
    
    private func fetchAllBookmarks(completion: @escaping () -> Void) {
        /* Some code that fetches all bookmarks */
    }
    
    private func fetchUnreadBookmarks(completion: @escaping () -> Void) {
        /* Some code that fetches unread bookmarks */
    }
    
    private func fetchUntaggedBookmarks(completion: @escaping () -> Void) {
        /* Some code that fetches untagged bookmarks */
    }
    
    private func fetchPublicBookmarks(completion: @escaping () -> Void) {
        /* Some code that fetches public bookmarks */
    }
    
    private func fetchPrivateBookmarks(completion: @escaping () -> Void) {
        /* Some code that fetches private bookmarks */
    }
}

This works fine enough, but what happens when I eventually want to introduce a new category, say, I don’t know, another user’s public bookmarks? Well, then I’d have to add another enum case, .public(userID: String), add the case to the fetchBookmarks(completion:) switch and add the case to the computed title property.

This might not seem like that big of a deal, but what if I had even more methods that switched on the enum and performed some work based on its value? Maybe I want to change the background color of the table view based on the category, or the text color, or perform an additional transform to the list. The possibilities are endless.

Or, what if I didn’t have access to modify the CategoryViewModel.Category enum? Like if it was contained in a framework that I didn’t have permission to modify? Then I wouldn’t be able to add a new category whatsoever since it’s impossible to add cases to an enum in an extension.

Lastly, it’s just so much…code. I mean, look up there. All that…code. You most likely really only care about one category at a time. Say it’s the .unread category. So you’re only dealing with the fetchUnreadBookmarks(completion:). All the rest is just…code. Noise.

Let’s get it out of here.

Allow me to introduce to you…Inversion of Control ↩️.

Inversion of control allows us to define an interface (in Swift, a protocol), and create several concrete implementations of it. If you look above, what do we really need this interface to do? We need to fetch a list of bookmarks and return a title.

protocol BookmarkListViewModel {
    func fetchBookmarks()
    var title: String { get }
}

That seems like a good start.

Now, let’s create its implementation:

class UnreadBookmarksViewModel: BookmarkListViewModel {
    var title: String {
        return "All"
    }

    func fetchBookmarks() {
        /* Some code that fetches unread bookmarks */
    }
}

And that’s it! Now, if we have a bug in our unread bookmarks code, we can focus on just the code that we need to without having to filter through all that other noise. Additionally, if we need to define new categories, we can create a new BookmarkListViewModel implementation, implement the required function and property getter, and we’re done!

This also gives us much more flexibility when testing. Instead of needing to subclass the BookmarkListViewModel and override methods, we can simply create a MockBookmarkListViewModel implementation of the protocol and we’re good to go.

So next time you find yourself with a long class that’s doing mostly the same thing over and over again, think about whether you can invert the control and instead program to an interface with lots of small, concrete implementations.

Update: You can see the finished implementation in a pull request.

How to hide private API keys in an open source project

While preparing to publish my simple little open source app, Glancify, on GitHub, I ran across an interesting problem when I decided to integrate Crashlytics.

The following information is inspired by the guide located at https://www.herzbube.ch/blog/2016/08/how-hide-fabric-api-key-and-build-secret-open-source-project, but I wanted to preserve the content here in case that link ever goes down. Also that link gave me a sketchy “unsecure” warning in Chrome when I visited it, ¯_(ツ)_/¯. Give those folks a high five for the original guide.

So, you want to publish an open source project that contains private keys? Here’s how you can safely share your code with the world while maintaining the privacy of your information:

We’ll use Fabric/Crashlytics as an example

NOTE: If you’ve already pushed any commit to the internet that contained your private API key, you’ll need to first remove that commit.

  1. Create 2 files in the root of your project directory called fabric.apikey and fabric.buildsecret and enter your respective API key and build secret into each appropriate file.
  2. Add each of the newly created files to your .gitignore.

     /fabric.apikey
     /fabric.buildsecret
    
  3. Add fabric.apikey to your Xcode project, making sure that the option to add it to your target is selected since the file is needed at runtime. Additionally, ensure that the file is included as part of the “Copy Bundle Resources” build phase.

    Screenshot of adding file to target

  4. If you want, you can also copy the fabric.buildsecret to the project also, but make sure that the option to add to target is unchecked. It is not needed at runtime and should not be included in the “Copy Bundle Resources” build phase.
  5. If you’ve already created a “Run Script” build phase as per the Crashlytics integration instructions, replace it with the following. Otherwise, add a new “Run Script” build phase and paste the following:

     FABRIC_APIKEY_FILE="${SRCROOT}/Resources/fabric.apikey"
     FABRIC_BUILDSECRET_FILE="${SRCROOT}/Resources/fabric.buildsecret"
        
     if test ! -f "$FABRIC_APIKEY_FILE" -o ! -f "$FABRIC_BUILDSECRET_FILE"; then
       echo "This build wants to upload dSYM files to Crashlytics."
       echo "Uploading is possible only if a Fabric API key and a Fabric build secret are"
       echo "available. This build is failing because at least one of these pieces of"
       echo "information is missing."
       echo ""
       echo "To fix the problem, create the following files and store the API key and"
       echo "build secret, respectively, within those files:"
       echo ""
       echo "  $FABRIC_APIKEY_FILE"
       echo "  $FABRIC_BUILDSECRET_FILE"
       echo ""
       echo "If you forked the project then you must register with Crashlytics and"
       echo "get your own API key and build secret."
        
       # Let the build fail
       exit 1
     fi
        
     FABRIC_APIKEY=$(cat "$FABRIC_APIKEY_FILE")
     if test $? -ne 0; then
       echo "Cannot read $FABRIC_APIKEY_FILE"
       exit 1
     fi
        
     FABRIC_BUILDSECRET=$(cat "$FABRIC_BUILDSECRET_FILE")
     if test $? -ne 0; then
       echo "Cannot read $FABRIC_BUILDSECRET_FILE"
       exit 1
     fi
        
            
     echo "Uploading dSYM files to Crashlytics"
     "${PODS_ROOT}/Fabric/run" "$FABRIC_APIKEY" "$FABRIC_BUILDSECRET"
    
  6. Replace the line that initializes Crashlytics (Fabric.with([Crashlytics.self]), usually located in AppDelegate.swift) with the following:

     do {
       if let url = Bundle.main.url(forResource: "fabric.apikey", withExtension: nil) {
         let key = try String(contentsOf: url, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines)
         Crashlytics.start(withAPIKey: key)
       }
     } catch {
       NSLog("Could not retrieve Crashlytics API key. Check that fabric.apikey exists, contains your Crashlytics API key, and is a member of the target")
     }
    
  7. Lastly, remove the key from your Info.plist!
  8. Celebrate by pushing your code to the internet! 🎉

As mentioned, these steps outline the exact steps you would take for initializing Crashlytics, but the framework should hold for most other services as well. Good luck!

One Downside

One downside that I will concede about static blogs is that you have to post something in order to have the site update the copyright date. Whoops!

Find My Friends On the Mac

Davis Sparks over at MacSparky:

The one piece of this I don’t understand though is why Apple hasn’t found a way to put Find Friends on the Mac.

I’ve wondered the same thing. It would be very convenient to have the ability pull up Find My Friends and see how close my wife is to being home while sitting at my Mac. Fortunately, while Apple hasn’t provided a bonafide Find My Friends solution, they have given us a way to see where our friends are (provided they’ve previously given us permission) with the new Messages features in Yosemite.

Just pull up a conversation in Messages and click the details button in the top right corner of the conversation. You will be given Details, such as options for screen sharing, FaceTime, or a phone call, photos you’ve shared, Do Not Disturb, and last but not least, a map with their current location.

Apple often adds many features to the OS every year that wind up being forgotten or that fall by the wayside, but this is one I’ve found myself using more and more.

This Is A Static Blog

Disclaimer: Wordpress is a wonderful content management system. Wordpress sites account for a metric butt-ton of traffic on the Internet and are responsible for giving millions of people a voice in a very loud world. It is a terrific solution for people who just want to get a blog or website up and running very quickly and easily and don’t have time or expertise to configure a static site. Automattic, the company behind Wordpress, is a terrific company and one I’ve looked at over the years as a shining example of how a company should be run.

Static Blogs

This site is built using Jekyll, a static blogging engine. Unlike Wordpress or other blogging engines of that ilk, which generate an entire website from scratch based on a database each time the page is visited, a static blogging engine only generates the site anew whenever the site itself changes.

For Example

Lets say John blogs at johnawesomebloggreatjob.com. Since the dawn of the blog, he has written quite a few posts and amassed an audience of literally dozens. John’s blogging platform of choice is Wordpress, which means that whenever someone visits his site, the Wordpress engine living at johnsawesomebloggreatjob.com must do a database lookup to get all of his posts, build the HTML by looping through the list of posts, and then serve the HTML to the visitor.

While not a problem most days, this became a problem when one day John wrote a particularly interesting blog post on the perils of basket weaving. Basket Weavers United picked up the article, and suddenly John’s normal audience of dozens transformed into hundreds of angry basket weavers. As John’s readership grew, so did the time it took for his website to serve out pages to visitors, until finally the website went down altogether. It turns out that while johnawesomebloggreatjob.com handled dozens of visitors everyday just fine, it choked as soon as that number grew to hundreds due to all the database lookups and processing it was having to do each time someone visited.

This is precisely the problem that static blogs solve. Instead of generating the site from scratch each time someone visits the site, the site generates only when the site itself is changed - mainly when a new blog post is published. So when a website grows from dozens to hundreds of visitors, the impact is the same - just serve out the HTML that’s already been generated. Obviously scalability and other concerns still exist with static blogs, but at least the issue of database lookups and processing time have been optimized out.

Even though most websites (including this one) will never reach the level of traffic that Daring Fireball or Hacker News see in one minute, the programmer in me can sleep well tonight knowing that my blog is not wasting precious CPU cycles.

Other Benefits

Another great benefit to using Jekyll is its support for writing posts in Markdown. Of course sites like Wordpress offer this capability, but Jekyll offers it out of the box, by default, dead simple. The benefit to writing in Markdown is of course A) its expressive syntax, and B) its portability. Since I’m writing posts in plain text files with non-complicated syntax in a markup language that is very widely supported, if I ever decide to change blogging engines, it’s as easy as moving text files around. I have complete control over what happens with the content I’ve created. With engines like Wordpress, posts are stored in a database which could become corrupted, or you may not know how to query it to get the posts out, or they could be written in some weird proprietary format. The point is, who knows?