An Imperative Version of ContentView

In this post, I rewrite the usual declarative implementation of ContentView. I do this not because the imperative approach is better. Rather I find that rewriting ContentView imperatively aids understanding and appreciation for the declarative approach to programming. It helps to bridge the gap between the two worlds.

Here is a typical “Hello World” ContentView:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
    }
}    

ContentView “declares” the UI to consist of a property called body that is declared to be a View with a Text element that renders the string “Hello, World!”.

Text(“Hello, World!”) is inside two brackets {}. Syntactically, this construct is a closure that returns a Text element with implied assignment to body. The same result can be achieved very simply with an explicit assignment:

struct ContentView: View {
    var body: some View = Text("Hello, World!")
}  

In this example, Text is a struct and the call to Text(“Hello, World!”) is a function call to create an instance of the struct. This example is, admittedly, very simple and almost trivial. However, we can extend this example to more complex views. As an example, let’s consider creating a list containing two Text elements. Here is the declarative approach:

struct ContentView: View {
    var body: some View {
        List {
            Text("item 1")
            Text("item 2")
        }                
}

There are a couple of things going on in this example. body is initialized as a List and initialization is performed using the declared closures. Notice that there are two sets of brackets and, therefore, two closures. A first attempt at rewriting this code imperatively yields the following:

var body: some View = List {
    Text("item 1")
    Text("item 2")
} 

body is now created using a traditional assignment. However, List is still implemented declaratively. To implement List imperatively we need to consider its initializers. Ultimately, we want a function call that looks like var body: some View = List(…) We get closer to this result with the following:

var body: some View = List(content: {
    Text("item1")
    Text("item2")
})

In this case content is a parameter to the initializer for List and {Text(“item1”); Text(“item2”)} is a closure that is passed as an argument for content. There are many other initializers for List. For example, consider the following example where we create a List using an array of Item where Item is a struct.

struct Item {
    var name: String? = ""
    var description: String? = ""
}

var items: [Item] = [
    Item(name: "item 1", description: "baseball"), 
    Item(name: "item 2", description: "football")
]

struct ContentView: View {
    var body: some View = createList(content: items)

    static func createList(content: [Item])->
        List<Never, ForEach<[Item], String?, HStack<Text>>>
    {
        let body = List(items, id: \Item.name, 
            rowContent: textFromItem)
        return body
    }

    static func textFromItem(item: Item) -> Text {
        return Text(item.description!)
    }
}

Here I created an Item struct with two members: “name” to uniquely identify each item and a “description” of the item. I also declared a variable called items which is an array of Item. The array contains two items; one for a baseball and another for a football.

The static function createList creates a SwiftUI List. createList takes one argument – an array of Item and returns a List. More specifically, createList returns one of the generic forms of List. Inside createList the function creates an instance of List, assigns it to body, and returns body to the caller. The List initializer takes three arguments. The first is the data which in this case is our array of items. The second argument is a KeyPath to the member that identifies each item. The last argument is the row content – in other words, a function pointer to a function that is called for each Item to render each row of the list. In this case each row is rendered using a Text element. 

In this version our SwiftUI is created imperatively with function calls. The functions called are static because body is initialized before “self” is available. In other words an instance of ContentView is not fully available when body is initialized. Thus, we cannot call a member function. The simple way around this situation is to make the functions static.

But let’s say you don’t like using static functions. It turns out we can add an initializer to our ContentView to instantiate body. Here is how this looks:

struct ContentView: View {
    var body: List<Never, 
        ForEach<[Item], String?, HStack<Text>>>? = nil
                
    init() {
        body = createList(content: items)
    }
                    
    func createList(content: [Item])->
        List<Never, ForEach<[Item], String?, HStack<Text>>>
    {
        let body = List(items, id: \Item.name, 
            rowContent: textFromItem)
        return body
    }
                    
    func textFromItem(item: Item) -> Text {
        return Text(item.description!)
    }
}
                

Admittedly, the imperative version is a little lengthier than the comparable declarative version. And detailed documentation about the initializers for List and examples for using them are not readily available. I developed the imperative version after considerable study of the documentation that was available and experimentation. It was a time consuming and tedious task. However, I am glad I did it. I believe that doing so has helped me to better understand SwiftUI and I hope that it helps you. 

Leave a comment

Your email address will not be published. Required fields are marked *