Child Views Should Be Seen and Not Heard

Monty Harper - Nov 12 '23 - - Dev Community

(Illustration by DALL-E 2)

No, I'm not endorsing misguided Victorian-era platitudes about how to raise your children. This article is about my learning process. In particular, I'm trying to wrap my head around something basic to modern coding: dependencies. Or more broadly, where do I put things in my app?

Yesterday I got some feedback on a chunk of code that wasn't doing what I wanted. (Thanks to Flock of Swifts, the most awesomest group of Swifties on the interwebs!) I had confused one index for another, but also my code was a mess, and I got happily schooled on a number of issues, including this one: child views should be "dumb."

This rang particularly true, as I had gotten more or less the same feedback recently from ChatGPT, in response to my question, "why isn't this working??"

As it turns out, everything is easier when the basic structure of your app doesn't fight against the way object-oriented code was designed to work. Hopefully by writing about this I will solidify the ideas in my head once and for all. Maybe it'll help you do the same...

Chaos and Confusion

I know what I want my calendar app to look like, but I've been struggling for weeks, wiring it up. How do I get information from the source, Apple's Calendar App, to the destination, the layout I want to see on screen???

As a learner new to app design, I completely lost myself in the details. My code was going in circles. It seemed no matter what I did, one feature or another wouldn't work that way.

In desperation, I typed all my confused thinking into ChatGPT along with a request for new insights. My original prompt was quite the ramble at 500+ words. But after some back and forth, a big picture began to emerge.

The Problem

They way I had organized things was like so:

Array of EKEvents (raw events) fetched from Apple's Calendar App
-> Array of event views
-> Main view (displays array of event views)
Enter fullscreen mode Exit fullscreen mode

I was keeping all my event views permanently handy in an array. My thinking was that this would be needed, since the main view calculated each event's location and re-drew it on the screen once per second. This way child views didn't need to be continuously re-constructed.

My eventViews got updated whenever the EKEvents were updated. When I tapped an event view on screen, an isExpanded value got passed from the main view into the eventView.

However, the main view did not know to re-draw the eventView in expanded form, and that's where I was stuck.

One thing I learned from ChatGPT was that keeping my subviews around in an array is un-necessary. It's fine to let the main view re-generate its child views once per second. That's really how this was designed to work. Swift is smart enough not to re-generate any parts it doesn't need to.

Another hint from ChatGPT (which I knew but didn't really KNOW if you know what I mean) was to create a binding...

And from my Flock of Swifts advisors, use a View Model!

The Solution

So here's how I've re-organized:

Array of EKEvents (raw events) fetched from Apple's Calendar App
-> Array of Event View Models
-> Main View (contains an array of isExpanded Booleans)
-> Child Views based on Event View Models (with a binding to the isExpanded array)
Enter fullscreen mode Exit fullscreen mode

Now changes to the Apple Calendar update an array of Event View Models, not Views.

Metaphorically speaking, the difference is that View Models are hands, where Views are gloves. When you construct a view it should be "dumb" or empty; just a "what this will look like" shell, to be filled with specifics from the View Model.

So my view models (not views) are accessed by the main view. Then the main view constructs actual child views based on the models as needed.

As for the isExpanded property; this is changeable by the user with a tap gesture, so it belongs in the child view, but the main view needs to be aware so it can re-draw the child accordingly. This is accomplished with a binding.

Reasons

My larger struggle was (and still is) developing a sense of where different bits of information should live and why. Ye olde View, Model, ViewModel pattern is king, keeping business logic (what goes on behind the scenes) separated from view logic (what the user sees and interacts with).

But that fact of life is easier said than internalized.

In this case I have three sources of data about Event views. Each provides a different type of information and needs to be handled in the correct way.

  1. Event information such as a title, description, start time, end time, etc. These are the properties that make an event an event. They belong in a structure called Event, which serves as the view model.

  2. Location of an event on the screen. This depends on each event's start time, in relation to the timeline currently visible on screen. The parent view has access to the timeline and calculates each child's position based on its start time from the view model. The child view doesn't need to "know" anything about its own position on the screen.

  3. Format of the view; whether it's expanded or not. The isExpanded property has nothing to do with the event itself. It belongs purely to the view. It needs to be read and changed by both the parent view and the child view. That requires a binding between parent and child. The isExpanded array places the "source of truth" with the parent, which can pass the right property value along to each child. The binding allows each child to change its own value in the array, so the parent can keep track and re-draw views accordingly.

Learning As I Go

The best way to learn is by doing. I've challenged myself with an app that was beyond my capabilities when I started. Having no experience to draw from, I tried every combination of where to put things. At various times all three of my information sources lived inside the eventView, or inside the View Model, or elsewhere as I moved things around, trying to cobble the whole operation together in a configuration that worked.

With persistence and help from much more experienced coders, plus a conversation with Chat GPT, I believe I've arrived at the correct solution. It works, so, that's "correct" enough for me!

Typing this up, it feels as if the fog has lifted quite a bit, and I do understand on a deeper level how and why the different parts are organized they way they are.

Hopefully with my next app, I'll get there much faster!

. . . . . . . . . . . . . . . . . . . . . . .