Views are a foundational building block in iOS apps.
As you gain more experience, or start to work on complex projects, the way you write views will evolve and you will start paying more attention to design patterns, testability and consistency. There are many approaches to try out! Our experience at iZettle led us to an interesting functional way of thinking about views that we would like to share.
Let’s assume we have a hierarchy of views that are reused in a few places. It consists of a label and a text field, they look like this:
How would we usually organize the code related to them?
Starting with a familiar approach
A popular way to implement this hierarchy is to create a subclass of UIView that will know how to render its subviews.
We’ll call it PriceView.
As you can see in the image, in this case the two subviews are simple so there is no need for any special rendering code. We will just instantiate them, add them as subviews and specify the styling and layout rules. Like this:
Passing input data
Our PriceView has some inputs and outputs. Let’s discuss how it will receive the amount updates and the description placeholder.
- The way we did it in the above example, the subviews were exposed as public properties. This way, PriceView doesn’t encapsulate its implementation details and we will end up with a tight coupling wherever we display it 😬. If the text field needs to be replaced with a text view, for example, we will have to change code in multiple places. We can do better!
- We could make the subviews private properties and expose only configuration methods that take the inputs and internally pass them to the subviews. This means that in our implementation we will create the subviews in one place and set their state in another one.
- Now let’s try a more reactive way to handle the input data. We could pass all the sources of data through the the initializer. For things that are supposed to be changed externally we can pass an observable type. There are many takes on this in different reactive frameworks — in our example we will use iZettle’s Flow Framework and its ReadSignal type. For things that don’t change, like the placeholder text, we can just pass the value.
This approach allows us to create the subviews, subscribe to them and react to the observables all in one place.
Handling user input
What about touch events and other user interactions?
- We could use the familiar delegate pattern — where PriceView will be configured with an object that can handle events. This approach works well if we want to have a single object that reacts to PriceView’s output but doesn’t scale to multiple objects.
- Alternatively, for broadcasting to multiple objects, we can use notification and observation patterns. Implementing observer management, however, can be a tedious task .
- As with the inputs, we could use reactive patterns here as well. If PriceView exposes an observable type, we can broadcast the events through it without having to care how many objects observe it — zero, one or many. Flow’s Signal type can help us here too.
Putting it all together
So, we’ve seen that using signals for the input and output has some benefits and we can put them together.
At this point, we can question why we should subclass UIView in the first place as we are not drawing anything sophisticated.
We could clean up by moving the inputs to properties of a struct and the view creation to a function that returns a simple UIView and the output signal.
Note: We changed the name of PriceView to PriceDescriptionEntry as it more clearly expresses the intent of what we want to represent. PriceDescriptionEntry is now a definition that can be passed around and materialized to create views multiple times.
How we use this
iZettle’s app has evolved over eight years and the codebase and team is growing rapidly. To keep things consistent, we prefer applying similar solutions to similar problems. We are using reactive patterns in many parts of the app and like the idea of applying what we’ve learned to views as well.
So how do we apply it in practice?
- We try and follow the ‘combined’ method described above. We formalized it with a generic protocol so anything that is Presentable can be materialized. We also have a bunch of helpers that allow us to compose presentables.
- If we need custom rendering / sophisticated processing of touch events, we will create a subclass of UIView that will handle that and return it from the materialize() function.
Is that all? Of course not!
To read more about how this idea evolved into a Framework, check out
our blogpost about creating the Presentation Framework.
Stay tuned for more blog posts or if you can’t wait, jobs.izettle.com 😜.
: Read more about the challenges of developing a robust and flexible API for asynchronous event handling.