Does SwiftUI View Not Compile? Time To Start Making It Lighter

Posted in SwiftUI

May 27th, 2022

⏱ Reading Time: 9 mins

Well, this post is about a specific issue that I recently met in SwiftUI and I would like to share here. Comparing to UIKit, SwiftUI is still an infant, yet it’s not that new so we still have many dark points about it. Regardless, only by using it under various scenarios one can really see its ins and outs, and discover weaknesses that add friction to the development process.

Such a friction came to slow down my work significantly into an app just a few days ago, where a SwiftUI view could not be compiled, and as Xcode said, “in reasonable time”. Issues like that turn me towards the opinion of those people saying that most probably SwiftUI is not production-ready yet; a statement that I mostly disagree in general. Admittedly, chances are that SwiftUI won’t do everything we want out of the box, but with a few tweaks based mostly on UIKit (or AppKit on macOS) we can make everything work just fine. Despite any problems, I personally vote for SwiftUI and I’ll keep using it, having UIKit and AppKit as my assistants to my ongoing and future programming tasks (bold statement, but I don’t see myself going back to using pure UIKit or AppKit again).

Anyway, my intention is not to make a lecture about SwiftUI here, but to be practical and focus on a real issue and how I worked around it, putting things back on track again. Before that, I’d like to make clear that it’s possible the problem I’ll describe next to be known, or not to be a problem at all, just a natural behavior. Or it could be related to a deep detail about how SwiftUI operates internally. In any case, something that I’m just not aware of. But even in that case, the proposing solution presented later could still be interesting to use.

That said, it’s time to start being specific. To make it easy to follow along, I’ll be providing sample code while describing, so you can try things by yourself too.

The original issue

To give the general context, the “problematic” view for me was a custom container; a view that was presenting a really small number of child views conditionally. However, that container view was observing for a -probably long- list of notifications using the onReceive(_:perform:) view modifier. In total, there were sixteen (16) notifications bringing messages about various actions that should be triggered in the container view. On top of that, there were also present seven onChange(of:perform:) modifiers in order to observe for changes in a few state properties.

After having added the last two onReceive(_:perform:) modifiers and tried to run the app, I noticed that it was taking extremely too long for Xcode to compile the project. Cleaning the build folder (menu Product > Clean Build Folder) and restarting Xcode did not help as I was originally hoping for; subsequent builds have had the exact same result. At some point I let it keep trying to compile without any interruption on my part, just to find out after some moments that the compilation failed with a message explaining that Xcode could not pull it off within a reasonable amount of time! Note that, all that was taking place in an Intel-based 2017 MacBook Pro. In a newer computer, the project would probably manage to be built, but it still would fail to do so for a bit bigger number of view modifiers.

Reproducing the issue

Let’s get practical, and let’s see everything through examples. All you need is a new SwiftUI based project, and optionally to create one or two new Swift files to add code that you’ll see next.

For starters, let’s suppose that we have the following sample list of custom notification names:

In a simple SwiftUI view, let’s start observing for notifications using the above names:

Let’s add to the mix a few onChange(_:perform:) view modifiers as well:

As you may guess, val1, val2, etc, are state properties in the view:

If you are following along, then it’s a good point now to run the project. Given that it’s generally still quite lightweight, most probably it will compile. If it does so, you could just copy-paste all the notification related view modifiers so you end up with an even longer list of modifiers and then build again. Hopefully (or not), at some point Xcode will show the following error:

“The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions”

What expression exactly?

That’s the key; it means the entire view, with all of its contents and after having applied all modifiers. Don’t forget that SwiftUI views are not real views, rather configurable “descriptions” that are being rendered into actual views later. All these “descriptions” are expressions that the compiler evaluates.

Anyway, this is the point where one has to start being resourceful. By commenting out all modifiers that receive notifications and building again, the problem just vanishes. That was not so obvious in my real-world project, as I had to repeat that process in various parts of code before ending up with this conclusion.

So, it’s becoming clear that observing for such a big number of notifications is not manageable, but how can we really get around this, and still be able to use notifications? They are necessary after all; if they weren’t, they wouldn’t be present in the project in the first place.

The idea behind the solution

If you work with SwiftUI, then you have probably noticed that every view modifier returns an instance of the view that is applied to. Obviously, a big series of modifiers that return view instances give a real hard time to the compiler. The only way to make things normal again is by removing some load from the compiler, and therefore to get rid of some, or even better, as many as possible, view modifiers.

Thankfully, the concept of notifications is not new in SwiftUI. We have been working with the notification center for years before SwiftUI comes to the surface, so we can definitely deal with them someplace else! At the bottom line, what we actually need is the information a notification carries, and occasionally its payload. We don’t really care how we’ll be receiving notifications, even though it’s so straightforward with the onReceive(_:perform:) view modifier. But if the latter triggers unwanted headaches, then we’ll engineer new solutions!

Cutting to the chase, the whole idea in order to escape from this unfortunate situation lies upon the definition of a new custom type, and more particularly, a class, with a quite specific purpose; to be receiving notifications! Just not all of them, only those that we are interested in. Once a notification is received, it will be handed off to the SwiftUI view. That way, the view will be relieved from the additional load of receiving notifications, and it will still be able to deal with them as it would originally do; just in a bit more indirect way.

Implementing the solution

With that in mind, let’s define the following new type which we’ll name NotificationReceiver. As you see next, it contains one stored property only, a closure with a notification object as a parameter value:

The notificationHandler closure is the way to communicate back to the call site of the class (in this occasion the SwiftUI view), and provide the notification that has been received.

This class needs an initializer, and the one that we’ll implement in our next step will have one parameter value; a closure similar to the above. The argument that will be provided for this parameter will be stored in the notificationHandler so we can call it right when a notification arrives:

But the work in this initializer is not finished. This is also the place where we’ll be watching for incoming notifications. Before we continue, here’s a small, yet hugely time and effort saving trick that we are going to apply:

Normally, we should set as many observers as the number of notifications that we want to be receiving. There are a couple of methods accessible through the shared instance of the NotificationCenter class that let us do that, but here we’ll pick the one that allows to handle the notification into a closure. Here’s an example:

In this particular example that I’m demonstrating, I’ve declared fifteen notification names, matching to an equal number of hypothetical notifications. Doing the above in order to be watching for all of them would result to several lines of code and a noticeable level of effort. It doesn’t feel quite nice, right?

Well, cheer up, as we can do things much faster than that! Simply enough, we’ll include the names of the notifications that we are interested in into an array, and then we’ll start observing for them into a loop.

Here is all that in the next code:

And just like that, we manage to observe for a long list of notifications, while having avoided to write lengthy code at the same time. Notice that in the closure where we handle a received notification, we call the notificationHandler supplying the notification as argument. That way, a notification is sent straight to the SwiftUI view in order to be handled as necessary there right when it arrives.

There is one last thing missing from the NotificationReceiver class; to stop observing for notifications. We do that in the deinit method:

Using the notification receiver

Back to the SwiftUI view, where we are just about to use the above class. What we primarily need is a NotificationReceiver property, which initially is going to be nil. Initializing at declaration time is impossible, as we need to provide a closure as argument in order to work with received notifications:

We will initialize this new property in the onAppear(_:) view modifier, but after we make sure that it’s not initialized already:

Every single time a notification is received in the NotificationReceiver class, the SwiftUI view will become aware of that in the above closure. Right there is the spot to handle each notification, but in favor of keeping things as tidy as possible, this is something that takes place in a method called handle(_:). You already see it being used, so let’s go now to implement it. That’s the last thing to do here:

Building the project again now is going to have a happy end, as it will compile successfully! You will notice at the same time that the compile time has been dramatically decreased. So, mission accomplished, problem fixed, and the project runs even better than before.

Conclusion

Coming across various unexpected issues is part of the development process, but what really matters is to be able to find effective solutions that will normalize things and let the implementation flow keep going on. In this post I presented such an issue that I faced in a real project, and then how I worked around it with a quite specific solution. I want to believe that what you just read in the above lines will be proved helpful to you, and it will work as a guide in order to handle similar situations. Even more, that you will adopt the presented implementation if necessary.

Thank you for reading, take care! ????

Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.