Iterating over design that needs to be reflected in code can be tedious and time-consuming.
Typically the designer works in graphics editing software and then submits flat art to the developer who implements the design in code. Refining the design requires going through the same laborious process.
Even harder if we want to support multiple themes in our apps. How would we even approach that if we were using Interface Builder?
Let's look at how we can approach implementing a simple library that could solve all of the above concerns.
Features
First, let's consider all the features that we would like to have
- Immediate feedback - we want to make changes and immediately see them reflected, rather than having to recompile and re-run application
- Ability to update live applications - because nothing looks cooler
- It has to support both Code and Interface Builder based UI
- It has potential to be used for more than UI, e.g. tweaking internal data
- Simple to integrate - We should have minimal impact on the codebase of the projects using the library
- We want to be able to test everything. It makes our life easier
- We want to be able to write the library in real-time, without having to recompile the project, in Xcode 8 using code injection
Designing
As much as I enjoy writing pure Swift for apps, I find myself leveraging a lot of Objective-C runtime powers for developer tools, and I think they are worth using.
I often joke that Mutation is a source of all evil, but it is very much true.
If we want to be able to perform real-time changes of running applications and have minimal impact on the codebase we need to try to avoid unnecesary state changes.
There is this concept in programming called Trait, it allows us to extend existing entities with additional functionality. Lets mimick that.
Trait
If the user of our library has some custom classes, we want to be able to modify them but in a restorable way.
In our library, Trait contains a pure function that can be passed in an entity to mutate its state, and it returns another function that would reverse that mutation.
So that when we apply and reverse, we will end up in the initial state.
let sourceView = view.copy
let reverse = Trait.apply(view)
reverse(view)
sourceView == view
This pattern will make it very easy to keep changing the live application with controllable side-effects, instead of allowing the mutation to go freely.
Persistence
Using just functions would be great for understanding the code.
However, it would make implementing our remaining features problematic, because of that our Trait
is a type containing the bespoke function along with required metadata.
Looking at the feature lists, especially around ability to update live applications we can see that we need some persistence so that we can send our settings via the network.
Our requirements dictate support for 2-way conversion (serialize and deserialize), since JSON is industry standard we can use Object-Mapper.
Outside of prototype scope, I would highly recommend using something easier to read/modify by humans e.g. YAML, JSON is better for machine than humans
We will be persisting the configuration of our functions, as to be able to use the same prototype for all different Traits, e.g DropShadow.
open override func mapping(map: Map) {
super.mapping(map: map)
color <- (map["color"], ColorTransform())
offset <- (map["offset"], SizeTransform())
opacity <- map["opacity"]
}
Real-time reloading
Leveraging the fact our data can be transformed to JSON format, we can now support both live-reloading and remote changes.
First off we need a daemon to observe changes, this is where you can leverage my KZFileWatchers for both local and remote observing.
Next, we need a way to inject the deserialized Traits into running application, that is the role of TraitsProvider
.
TraitsProvider
role is simple:
- Find existing traits
- Use their reverse functions to remove all existing side-effects and return to original state
- Apply new traits, storing their reverse functions for future use
traits.forEach {
var removeClosure = {}
$0.apply(to: target, remove: &removeClosure)
var stack = target.traitsReversal
stack.append(RemoveClosureWrapper(block: removeClosure))
target.traitsReversal = stack
}
I have had to introduce inout
param for remove closure because when migrating to Swift3 I discovered compiler bug with my previous approach of just returning closure
Identifying traits and minimizing user code impact
Each view might have different traits, so how can we identify which traits to apply to which entity? Simply, we can introduce identifiers using associated objects.
Simply write extension on the types we are interested in like:
public extension NSObject {
var traitSpec: String? {
get { return objc_getAssociatedObject(self, &Keys.specKey) as? String }
set {
if let key = newValue {
TraitsProvider.loadSpecForKey(key, target: self)
}
objc_setAssociatedObject(self, &Keys.specKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}
public extension UIView {
/// Defines an identifier for trait specification.
@IBInspectable override var traitSpec: String? { get { return super.traitSpec } set { super.traitSpec = newValue } }
}
When an identifier is set, ask our TraitsProvider
to load trait specification for it.
It can be used from IB and Code all the same.
Supporting live-programming the library itself
We want to be able to write a new Trait and have it supported immediately, without even recompiling the project.
The first step is to use code injection, second step is designing our system in a way that doesn't require us to register supported Traits manually.
We can use runtime to find all types that inherit from Trait
class, it's straightforward:
typealias Factory = (_ map: Map) -> Trait?
static func getTraitFactories() -> [String: Factory] {
let classes = classList()
var ret = [String: Factory]()
for cls in classes {
var current: AnyClass? = cls
repeat {
current = class_getSuperclass(current)
} while (current != nil && current != Trait.self)
if current == nil { continue }
if let typed = cls as? Trait.Type {
ret[String(describing: cls)] = typed.init
}
}
return ret
}
Then we only need to automatically re-register existing Traits on code injection:
/// This function will be called on code injection, thus allowing us to load new Trait classes as they appear.
class func injected() {
Trait.factories = Trait.getTraitFactories()
}
This means we can now write new Traits without having to restart the application at all, this is how I implemented CornerRadius
into Traits:
{{< img class="center" src="/2017/01/Traits-ExpandingInRealTime.gif" >}}
Ability to use for more than just UI
This pattern can be used for so much more than UI, and it would be great to leverage all of those realtime benefits for other parts of the app. How can we make it safer?
We want to avoid someone trying to add DropShadow to a Model object, so how can we define what kind of types a given Trait
support?
We can make each trait list Classes it supports:
var restrictedTypes: [AnyClass]? { return [UIView.self] }
Our TraitsProvider
might then verify type requirements before ever applying the Trait
, thus ensuring that apply
will only be called with targets that are supported.
Bonus: Avoiding Strings
Strings are ugly. We cannot avoid them in Interface Builder case but we can in the Code.
We can write a simple script that would scan all storyboards/xibs and create a typed interface for them. Then we just add it as build-phase, and we can use safer API.
That's what I used at the above live programming demo.
Conclusion
Full source code, with documentation and tests is available on GitHub.
The only thing that the users of the library has to do is to assign identifiers to the objects they want to be able to tweak, and run our daemon.
We designed architecture that allows rapid development of new Traits, without having to recompile the project, ability to change traits of your entities remotely (via JSON) or local via changing your code, all in real-time.
Configuring theming in our application would now be consistent between Views defined in Code and those defined in Interface Builder, and the whole library is very straighforward.