From inspiration to production, Part 3

Christoffer Winterkvist
hyperoslo
Published in
9 min readMay 29, 2017

--

This is the third part in the From inspiration to production series.
The previous part shared some technical details how about we leverage from composition. This part tackles the many speed-bumps and mistakes that we made along the way.

Homage, the hard way

Giving something a perfect name can be a demanding task. If it represents UI it should probably be prefixed with something related within that domain, like: ComponentView or SubmitButton.

We felt a big need to pay tribute to our muse which was Spotify, hence us going so far as to rename our entire library from Components to Spots. We still think that Spots is a good name as it is a nice play-on-words. It uses parts of the company that we wanted to play tribute to but it also has a completely different meaning.

spot
spɒ
t/
noun
plural noun: spots
1. a small round or roundish mark, differing in colour or texture from the surface around it.
2. a particular place or point.

For us, it infers composition because of the pluralization. Spots can be small, big and come in all shapes and sizes. It felt so perfect that we decided to include it that in public API naming convention. As the framework grew we started to realize two things, the naming does not scale and secondly, it hurts discoverability. We went too far and introduced our own naming convention that ultimately felt foreign. Here is a brief description on how things worked prior to Spots 6.0.

Spotable was a protocol that all core types conformed to. The core types consisted of ListSpot, GridSpot, CarouselSpot and RowSpot. All Spotable objects where powered by the same Component model, we just didn’t call it ComponentModel. Because we had both Spotable and Component, it was hard to distinguish what was UI and what was model. All of them had their own own implementation and came together by conforming to the protocol. To make matters just a tad more complicated, we also had two additional protocols called Listable and Gridable that added implementations for table view and collection centric components. We had the protocol because we wanted the opportunity to build custom Spotable objects that were specific for the application that we were building it for (e.g. MapSpot, PageSpot, SegmentSpot…). So when performing operations on the objects you would use methods like.

func spotDidSelectItem(spot: Spotable, item: Item)
func spotsDidReload(refreshControl: UIRefreshControl, completion: Completion)
func spotDidReachEnd(completion: Completion)

Before we realized it the entire public API was riddled with names that only we understood.

When it was time to release and find a fitting description, something that people instantly understood, we were at a loss. If we stuck with our internal naming convention, which would make sense, we would spend large chunks of our README to just explain what everything actually was. It would be more of an glossary than a README. This is something that we find off-putting when other do it and now we found ourselves repeating that exact “mistake”. The tag-line that we ended up giving Spots was “a cross-platform view controller framework for building component-based UI’s.” After writing that sentence the first time, we knew what would have to do eventually. So in version 6.0, we did a huge refactoring, changing the names to something more relatable to anyone who is familiar with composition. We ended up removing some of the features that we previously saw as very important. More specifically, we remove the Spotable protocol. Instead we rely on our new Component class. If you guessed that Component is a container for the UI you want to display, then you guessed correctly. We had Component before, but it was the name for the model. Now the model that was previously called Component is called ComponentModel to clearly indicate where it belongs. We no longer have core types, instead Component is polymorphic, meaning that it will use the UI foundation that you specify on the model. So if the ComponentModel.kind is .list, the Component will initialize with a table view as its view. The ComponentModel currently supports .list, .grid and .carousel. Grid and carousel both render itself using a collection view, the difference is the orientation. Grid scrolls vertically and carousel horizontally.

The ironic part here is that the framework was initially called Components.

Abstraction — your UI foundation does not matter

Why spend time arguing if you should use a table view or a collection view for your new feature. Both can pretty much do the same thing. One gives you a lot more dynamic opportunities and the other gives you more out of the box. We found ourselves discussing this back and forwards, both camps had great arguments but ultimately, no matter what you based it on, how much did it really matter? This led us to the choice of having a polymorphic Component. As described earlier, the model decides, this means that you can switch UI foundation by changing a value on your model. So the dreaded “Could we turn this list into a grid” was now a thing of the past. Prior choices made this a bit icky, when we had core types, we registered views on the core components not to have them be mixed up. They also had to inherit differently. This was especially tricky on macOS as collection view cells (they aren’t even called cells, they are called items), don’t inherit from NSView, they actually inherit from NSViewController. If views were to be used in a table view, they had to inherit from the table views dequeuable type, the same went for collection views. What this meant it that you couldn’t just dynamically switch between them without providing the view twice. This was solved by providing list and grid wrappers. So instead of building cells, you would go with the most natural component of all, the view. For iOS that meant building UIView’s and on macOS you build NSView’s, even for collection views. The interesting part here is that you can easily integrate existing views by implementing two methods, the view life cycle is pretty much as agnostic as if you would build it from scratch without any frameworks.

Convenience is not always convenient

Adding convenience or proxy methods for accessing data might seem like a good idea when trying to build a tidy neat public API because less typing is always good, right? That is not always the case, and in our case we had gone a bit overboard with our implementation. We had convenience methods on the component to gain access to the items. Looking at it from the outside you would expect the component to be the owner of the items but when looking underneath, it would just relay it to the model.

var items: [Item] {
set(items) {
model.items = items
}
get {
return model.items
}
}

When you have to explain where stuff comes from or belongs to, you know that you are walking on a path that could end up being a slippery slope. So in 6.0 we decided to remove a lot of these methods to make it more accessible and understandable from the outside.

Having a clear separation of concerns, is a good goal to have.

https://github.com/hyperoslo/Spots/pull/538

Protocol extensions at its core and why it’s a bad thing

During Swift’s inception there was a lot of buzz about protocol extensions, we went with this and fueled the entire framework with protocol extensions. This is something that does not scale well in a framework of this size. We essentially ended up in the same slope as we did with adding convenience methods, it greatly decreased discoverability and it hurt the core foundation as it felt like a bunch of spaghetti code when interacting with it. So now we are gradually transitioning out of it by doing more composition. What we mean is that we added worker classes like ComponentManager. It’s in charge of performing mutating operations on the component and calling a completion closure when the UI is in sync with the data source. We still use and love protocol extensions, we just went a bit overboard with adding super powers to Spots implementation. With that said, protocol extensions can be very useful when doing cross platform setups, more on that later.

Preprocessors statements, use with caution

Preprocessors statements is a nifty way of telling the compiler that it should opt-out on specific parts of a function or an entire function inside of a class.

However, it does make the code harder to read as it is no longer linear. The compiler might be an expert on knowing when to turn a blind eye to a piece of code, but developers aren’t. One nifty thing that you can do, that we do a lot, is to use preprocessor statements to decide which UI framework to include at the top of the file. Instead of using it to do inline opt-out, instead try and extract that piece into a protocol extension and include the platform in the naming of the file.

Supporting multiple platforms is hard, but it can be done

This is something that is relative to the size of the project and how much it relies on different frameworks. In our situation, we were building on top of Apple’s UI frameworks. For macOS it meant that we were targeting AppKit/Cocoa and for iOS and tvOS we built our abstraction on top of UIKit. The frameworks are same same but also very different. The initial constraint that you need to add for yourself is not to mention any of the platform specifics in your code. We solved this with a series of type aliases. Instead of declaring UIView as the type, we used View.

#if os(macOS)
typealias View = NSView
#else
typealias View = UIView
#endif

Now we can reference View on both platforms. If you look at the example we combine both pre-processors and typealias to get what we want. You could also make two type alias files and just add them to each target. However when doing things that are shared, it felt better to keep them combine so that you don’t have to jump back and forward when exploring what they actually point to.

If you compare Cocoa with UIKit you can see that they are very much alike, they have the same method but they might be named differently. This can also be solved with protocol extensions, one important thing to think about before you start adding proxy method names is to make your framework cross platform, think about if you are adding them out of convenience or necessity. As I mentioned before, convenience is not always convenient, you could end up changing “the game” as it will add another frame in the stack trace. Might not seem very harmful but doing this too much can cause stack trace nausea because of all the jumping back and forwards.

Documentation is key

Adding documentation is very much like adding tests to a project. If you start doing it from the beginning it will cost you very little extra effort but going back and adding it afterwards can feel impossible at times.

So why is documentation important? Good code is self explanatory right? Even if that is true most of the time, it can only describe itself in isolation. Code lacks the ability to explain how it fits into the bigger picture. By adding documentation you also apply a voice to your implementation that can help to answer questions when you are not around.

Don’t build puzzles for other people to solve.

Reasoning and summarizing your code into a fluent text is also a brilliant exercise to verify that you’re achieving what you set out to do. If things are hard to describe it could be an indication that something is doing too much and/or that your implementation could use a small overhaul. I’d like to propose a challenge, start with the function documentation before writing the implementation to see how that affects the way you write your implementation, if you do that, you’ll never forget to add documentation.

You don’t do it for others, do it for yourself. I think Marcin Krzyzanowski hit the nail on the head when he wrote the following on Twitter.

This is especially true with your own code. So do yourself a favor and start adding documentation, if not for others but for your future self.

This is the end of the third part in the From inspiration to production series.
In the next part, we will touch on why we are still sticking with native and the road ahead.

You can find me on Twitter @zenangst and on GitHub @zenangst.

--

--

Christoffer Winterkvist
hyperoslo

random hero at @fink-oslo by day, cocoa vigilante by night, dad at dawn. my life is awesome. Previously @hyperoslo