iosdev

SwiftUI for UIKit developers

Transcript of my talk from PragmaConf 2019, presented on Oct 11th in Bologna.

by Aleksandar Vacić 19 minute read

Note: slides for this talk are available at SpeakerDeck.

Video of the talk is available on PragmaConf’s YouTube channel.

Introduction of SwiftUI at this year‘s WWDC was surprise for, I believe, just about anyone. Many expected something along the line of next-gen UIKit which would unite all Apple’s platform UI frameworks.

Instead what happened is that Apple introduced a brand new framework as replacement for all its existing UI frameworks. SwiftUI is built on features introduced in Swift 5.1. It brings fundamentally new approach to layout, rendering and data flow when compared to UIKit.

SwiftUI and Combine have been raucously welcomed by people in alternative-UI-frameworks communities as the concepts they championed for years were now available as first party offering.

I, however, am not one of those people.

I’m a big proponent of leaning heavily on first-party frameworks for key parts of an app, especially for multi-year projects we usually do in Radiant Tap. That gives us the best chance to stay relevant, to react quickly to platform changes and adopt new features, since we don’t wait for anyone to catch up with Apple.

As “pure” UIKit developer, I was watching announcements in June a tiny bit anxiously. I am pretty sure I’m far from alone.

So today, I’ll take a look at SwiftUI and compare it with UIKit across 5 topics:

A big picture overview of essential Principles and Patterns on which both frameworks are built. How are view layouts defined and how they are rendered. How you switch from one view, or a screen, to another. How are tactile human interactions handled (taps, gestures etc.) Finally, look at the other side of the coin – how is data read and written throughout the UI.

Be advised: I’ll offer some very strong opinions that I hope will be the start of some good natured discussions.

Principles & Patterns

UIKit is heavily influenced by the language it was built in: Objective-C. Thus its primary implementation detail is that it’s based on class inheritance.

Layout is built by stacking and embedding UIViews where you are free choose do you want to declare the layout in Interface Builder or you want to build it imperatively at runtime.

Or do both, which is possible because entire view hierarchy is, regardless of how it was built, fully accessible to the developer at any point during app’s lifecycle. Due to that, developer is also almost entirely in control how that hierarchy will be drawn and re-drawn as needed.

Relevant design patterns are MVC, Target-Action used mostly by UIControls to handle events and Delegate used to offload various tasks from complex controls.

SwiftUI is also influenced by its originating language — it’s based on protocols and composition of lightweight components. Every single example uses struct instances which adopt View protocol and thus have no inherited baggage, making them incredibly lightweight.

Layout is exclusively declarable with no option what-so-ever by the developer to directly alter something during run-time.

Data flow in SwiftUI is built around reference semantics with most of the details kept outside of developer’s control.

Layout & Rendering

UIKit offers truly rich class hierarchy: UIResponder UIView → UIControl → UISwitch . Also UIResponder → UIViewController etc.

Every one of these classes is part of wonderful world of Objective-C dynamic runtime which is awesome since you can extend these classes on a whim and add useful capabilities that magically appear in all your instances.

It’s a great system which allows each level to handle a specific set of tasks. UIResponder handles touches and responder chain, UIView handles drawing, UIControl handles events etc.

Trouble with UIKit’s high-level components is very limited customizability. If you want anything different from what Apple envisioned, you need to drop back to UIControl or UIView and rebuild everything from scratch. Which is far from ideal.

SwiftUI is conceptually way simpler. Anything can be a View and the only thing it should do is return something that is also View from its body property. Behavior and configuration is done through view modifiers that also return View instances. That’s it.

Framework already includes bunch of small View components ( Text , Image etc) which you can use to build ever more complex components.

▪︎▪︎▪︎

Let’s see that in practice and compare how these two frameworks handle building one very simple custom component. It’s a greeting component that takes name as string parameter.

In UIKit you start with basic skeleton, subclassing UIView and adding required init s plus one String property to receive the data value.

To display that string, you setup reference to private UILabel and imperatively build the layout at runtime as soon as possible in the view’s lifecycle. Note that I am fully in control of rendering – how big the label will be and where it will be positioned.

Now when string value is set, I can change the label content because I hold the reference to it. This means that in UIKit, I’m choosing when and which subviews will be redrawn as consequence of the model data change.

final class GreetingView : UIView { var name : String ? { didSet { label . text = name } } private var label : UILabel ! override init ( frame : CGRect ) { super . init ( frame : frame ) let label = UILabel ( frame : frame . inset ( by : layoutMargins ) ) addSubview ( label ) self . label = label } required init ? ( coder : NSCoder ) { fatalError ( " init(coder:) has not been implemented " ) } }

SwiftUI requires way less code, simply placing the string inside a Text view. But it does not provide any reference to this internal Text view.

Thus the only way to change the displayed string is to redraw the entire GreetingView . Every time.

struct Greeting : View { var name : String var body : some View { Text ( name ) } }

Important question here is: what do we lose by this dramatic change, from imperative to declarative approach? Think about it: when do you really need this ability to rebuild your hierarchy at will, at runtime?

The only scenario it makes sense to do so is if your view is fetching some random data on its own from an external source and then changes what it’s showing per some predefined algorithm.

What I just described is not a view anymore — it’s a business logic implementation that just happens to also display stuff. Views should never perform network calls, never fetch data from database etc.

Views should always be given known data to show and if you stick to that philosophy then declarative layout will be the only one you ever use. In SwiftUI, UIKit or any other UI framework.

Thus declarative approach here is the right approach.

SwiftUI completely removes the ability to even try the wrong thing. So 👍🏻 — this is my jam, this is what I teach, what I train developers to do.

▪︎▪︎▪︎

OK…if the declarative approach is the only correct one, SwiftUI must be clear winner here since its syntax is far more friendly and simpler to write than UIKit? Well…it depends.

SwiftUI is so obviously created by people who create layouts in code and only in code.

SwiftUI syntax is way better than addSubview() coupled with LayoutAnchor s, SnapKit or TinyConstraints or whatever other helper library you can find on GitHub. It’s honestly no competition.

But imperative approach is not the only game in town for UIKit. You can use Interface Builder to declaratively layout your components and then it’s not that clear anymore.

final class GreetingView : UIView { var name : String ? { didSet { label . text = name } } @IBOutlet private var label : UILabel ! }

Some of you may be thinking now: “But in Xcode 11 I have Instant Previews to help!”

That is true but that tooling pair is far worse than UIKit + IB for one very important group of people: the beginners.

In Interface Builder, they only need to learn what the panels on the right are for. Then drop any UI control on the canvas and simply read what its purpose is. What its properties and allowed values are. Change something in the canvas or panel and see it re-rendered. It’s a great way to learn.

None of that is possible with code-based approach, because in most cases they can’t even guess; as soon you start typing, Swift compiler starts throwing warnings and errors. For every change. All the time. Or it will offer a myriad of options with some horribly cryptic names.

Trust me — to a newbie, that’s nightmare. I was teaching iOS development in Swift for 3 years and have witnessed first-hand just how demoralizing something like this can be, where whatever they try to change results in an error.

▪︎▪︎▪︎

The cognitive load required for SwiftUI is very, very high. Instant Previews in Xcode 11 are utterly useless here.

But — once you already know SwiftUI, they become far superior tool compared to IB. You can check how your UI is actually working with real data, with actual animations and behaviors rendered right next to your code.

It’s no wonder then that early adopters of SwiftUI are loving it, because they are mostly iOS veterans and are able to just plow through these errors.

SwiftUI’s biggest practical strength compared to UIKit is ridiculously simple composing of controls. Because everything is View – Text , Image , *Stack etc – there’s a finite but powerful list of modifiers you can apply to just about any combination. Which makes it trivially easy to create custom button layouts, for example, which in UIKit is a pain in the butt.

All together, I give somewhat hesitant 👍🏻 for this.

It has so much potential but I really hope that Apple or some other developer tools company will create visual layout builder for SwiftUI. I would buy something like that in a heart beat.

Navigation

Navigation in UIKit is performed by switching between UIVCs contained inside some special subclass of UIVC, like UINavigationController .

In simplest terms:

let targetVC = TargetController ( ) show ( targetVC , sender : self )

One important philosophical note here: in UIKit, UIViewController should be considered a one-off extension of UIView , sort of UIResponder -> UIView -> UIViewController “subclass”.

Hence what navigation means is that you are switching views.

Lô and behold, SwiftUI way:

let targetView = TargetView ( . . . ) NavigationLink ( destination : targetView ) { . . . }

Oh, just kidding - you can’t do exactly do that in SwiftUI, it must be:

NavigationLink ( destination : TargetView ( . . . ) ) { . . . }

because SwiftUI‘s view builder closures only looks like you can write whatever Swift code you want – they actually allow a very limited subset of language constructs.

That technicality aside, conceptually it is identical to UIKit just done with different syntax.

Of course, it means they are both equally horrible.

Don’t get me wrong, switching views by itself is fine. The way it’s implemented is horrible because of who/what makes the switch. That source view needs to know how to instantiate and configure the target view. Or a handful or a dozen other views, depending on how much branching happens at particular screen.

So, 👎🏻 for this.

This was primary motivator for me to adopt Coordinator pattern in UIKit and even build my own Coordinator library.

I re-iterate my long-held position that there is absolutely no reason what’ so ever that any given view should know anything about views not embedded into it.

Interactions

Tap to switch to another view is just one of possible interactions in an UI framework. There are also button taps, switch toggling and various other ways to perform actions, usually handled with closures.

Arbitrary gesture handlers are also possible in SwiftUI, again through closures; nothing really different from UIKit here – just learn the syntax and you’re good to go.

Text ( " Tap me! " ) . onTapGesture { // d o s o m e t h i n g } Image ( " some-image " ) . onLongPressGesture { // d o s o m e t h i n g }

There’s something far more interesting here then just taps and swipes though. Remember that both UIView and UIViewController inherit from UIResponder ?

This is the basis of responder chain. Each UIResponder instance has a next property which is also UIResponder .

Thus for any UIView , its .next value is its .superview .

, its value is its . For UIViewController , it’s .parent controller if it exists or the superview of its .view .

This gives us free path upwards through any view hierarchy; from most deeply nested views all the way up to UIWindow and AppDelegate .

Responder chain is one of the most useful tools in the UIKit framework and one that regularly slips under the radar of most developers.

Its primary purpose is to facilitates touch bubble-up through the hierarchy. UIKit builds on that simple concept to allow some very neat and advanced tricks you can perform, like skipping some links in the chain or re-routing to another responder chain.

You can search up the chain for an ancestor of specific type, regardless how deep you currently are. We use this in our form-building library, Fields:

extension UIResponder { var fieldsController : FieldsController ? { if let c = self as ? FieldsController { return c } if let c = next as ? FieldsController { return c } return next ? . fieldsController } }

You can also send data and call arbitrary actions upwards through the chain, through as many layers of nesting as you may have. That’s the essence of our Coordinator library:

extension UIResponder { @objc func accountPerformLogout ( onQueue queue : OperationQueue ? = . main , sender : Any ? = nil , callback : @ escaping ( Error ? ) -> Void = { _ in } ) { coordinatingResponder ? . accountPerformLogout ( onQueue : queue , sender : sender , callback : callback ) } }

No delegates, no extra properties on each view/cell, just a couple lines of shared code.

This allows us to make independent views that don’t care about other sibling views in the app. And all of that works practically instant, taking unmeasurably little time (<1ms) to pass through the hierarchy.

▪︎▪︎▪︎

Now a million dollar question:

Is there a replacement for responder chain in SwiftUI?

Is there a way to send some data or call a method somewhere upwards through the hidden hierarchy that SwiftUI builds for us?

The answer may just be — yes. This innocuous view modifier seems to hide a great power.

var body : some View { NavigationView { List { . . . } . listStyle ( GroupedListStyle ( ) ) . navigationBarTitle ( " Menu " ) } }

At the end of the SwiftUI Essentials talk, the presenter said something very interesting:

…And then we can use the navigationBarTitle modifier to produce that large beautiful title for our form. Now this modifier is a little bit special. It provides information that’s able to be interpreted by a NavigationView ancestor. …this is an example of one that flows information upwards using something called preferences.

There’s zero documentation at the moment about this part of the SwiftUI and the only usable stuff I found are 3-part article series on SwiftUI-Lab.

(Will I be looking at preferences as a way to avoid that horrible navigation i just ranted against? You bet I will!)

Data Flow & Management

The main selling point presented in the Data Flow through SwiftUI talk at WWDC is: single point of truth for any piece of data.

Instead of each view having its own stored copy of data it’s rendering, there will be only one stored copy of this particular value, “attached” to this one view with all subviews using that same value through a reference.

This is important to note – it does not matter if your data is value or reference Swift type; once you add @State and @Binding that value will be accessed by reference.

So, that’s the story Apple sells.

However, if you go back to the starting graph you can see some embedded component and/or control, both sharing a piece of model data given to the parent view by its controller.

This is textbook MVC, the most basic building block of UIKit. There are actually 3 MVC components in that diagram: the main view and two components/controls inside.

There is absolutely nothing stopping you to use same reference semantics in UIKit as they do in SwiftUI.

But there’s a reason why that’s not the primary choice — reference semantics across multiple UIKit components are not always this easy; actually the only things that is easy is creation of reference cycles among them. Because UIKit components are classes, as in – reference types.

That’s one of the reasons why we had so much talk over the years about unidirectional architectures, value types in data flows and what not.

SwiftUI gets around this issue because in just about all code examples out there, View is a protocol implemented by struct – as in – value type. So they can freely use reference semantics for the data, since the views are values.

Which brings me to the final point in this thinking process: in UIKit you can choose between reference or value semantics because you have access to each of these components and you know exactly how to access them.

In SwiftUI that’s not possible; not only you don’t have reference to inner components, you don’t know do they even exist. Say…if Apple chooses to render the whole parent view as one flat bitmap.

So the only thing Apple could do here is offer us a way to declare what data we need inside those fictional runtime components and then SwiftUI runtime will handle the data transfer because only that runtime knows how things looks like inside.

Hence that story about single point of truth is irrelevant, because it matters only to SwiftUI maintainers. For all of us, it’s just syntax.

So…that’s how localized data are shared from parent view to its subviews.

▪︎▪︎▪︎

To share data among independent views they gave us Environment .

The recommendation goes like this:

throw any data or objects you may need in multiple independent views across your app into Environment instance.

😨😱

I can understand putting some settings and configuration parameters into something like that but throwing things like say, ManagedObjectContext instance…that’s really bad practice.

final class SomeService : ObservableObject { } struct MainView : View { @ EnvironmentObject var service : SomeService } class SceneDelegate : UIResponder , UIWindowSceneDelegate { lazy var service = SomeService ( ) func scene ( _ scene : UIScene , willConnectTo … ) { let mainView = MainView ( ) . environmentObject ( service ) } }

After decade of building best practices around data flows, we get to witness the grand comeback of Singleton pattern in its worst possible use-case: the global storage for an entire app. Just throw everything in there, it’s cool. 🙎🏻‍♂️

It boggles my mind even more so because there is something else here, something genuinely amazing. The part of SwiftUI/Combine partnership I really, really like is @Published & @ObservedObject .

You have an object that holds data used by particular view which needs to always show the latest data.

final class DataManager : ObservableObject { @ Published private ( set ) var events : [ Event ] = [ ] } struct EventsList : View { @ ObservedObject var dataStore : DataManager private var events : [ Event ] { return dataStore . events } }

With just few property wrappers and one protocol you have what in UIKit would take considerably more code.

Here’s an over-simplified snippet.

final class DataManager { private ( set ) var events : [ Event ] = [ ] { didSet { NotificationCenter . default . post ( name : NSNotification . Name ( " DataManagerDidUpdateEventsNotification " ) , object : self ) } } } final class EventsListController : UIViewController { var events : [ Event ] = [ ] { didSet { render ( ) } } override func viewDidLoad ( ) { super . viewDidLoad ( ) render ( ) setupNotificationHandlers ( ) } } private extension EventsListController { func render ( ) { } func setupNotificationHandlers ( ) { NotificationCenter . default . addObserver ( forName : NSNotification . Name ( " DataManagerDidUpdateEventsNotification " ) , object : nil , queue : . main ) { [ weak self ] notification in guard let self = self else { return } guard let dataManager = notification . object as ? DataManager else { return } self . events = dataManager . events } } }

The only application-specific parts here are:

data store itself

transferring updates into the view

re-rendering the view.

Everything else is boilerplate which in SwiftUI is hidden-away and handled by the framework.

Even better, if you need some custom behavior, you can remove @Published and write your own willChange publishing code.

final class DataManager : ObservableObject { let eventsWillChange = ObservableObjectPublisher ( ) private ( set ) var events : [ Event ] = [ ] { willSet { eventsWillChange . send ( ) } } } struct EventsList : View { @ ObservedObject var dataStore : DataManager private var events : [ Event ] { return dataStore . events } }

This is the loveliest approach a framework can take – default behavior covers 95% of the cases and gives you enough freedom to do what you need in the remaining 5%.

Bottom line: Please use this and avoid throwing stuff in the Environment as best you can. The future you will thank you.

Summary

To summarize, SwiftUI is UI framework where you

declare how the UI should look like but do not control the render part

declare what data you want to use but don’t actually have any control over the flow nor management of the data

This is equally amazing and worrying.

Amazing because you get to deal only with the essence of your app and don’t care about boiler plate. All the UITableViewDataSource and UITableViewDelegate and that stuff.

It’s troubling though, because if there’s a bug or something is not working or is not yet supported, there is more or less nothing you can do but wait for Apple.

Even more worrying is that unlike Swift itself, SwiftUI is not open source and there is no public roadmap of any kind. Thus you have no idea what’s coming up and when.

Right now, SwiftUI is far from ready. I would say it’s a solid ver 0.7 with some significant things missing, like these few:

No usable replacement for collection views.

Rudimentary support for scroll views.

During the beta period, Apple made significant changes in the data management: they renamed some property wrappers, updated publishing implementation to use WillChange instead of DidChange etc.

It’s really obvious that they are trying to do tremendous amount of long-term-facing work under very tight deadline.

This is not the framework you want to build your app on, today. Most likely not even in a year. I wish they waited another year.

If your upcoming projects must support iOS versions before 13, you have no choice — you must code them in UIKit.

My advice, in that case, is to identify tasks and challenges that are most important for your app’s UI and try to recreate pieces of them in small, focused SwiftUI projects.

Then also parts that give you the most grief and require a lot of code in UIKit – attempt to do those parts in SwiftUI. See if it’s any different.

This is the most real-world test case you can hope for.

You will know what’s possible and what’s not.

Keep all that in a git repo and revisit from time to time as you see other people try something similar or as SwiftUI updates over time.

If you are lucky enough that your project can immediately use iOS 13 as minimum deployment target, I still recommend to go with UIKit first and implement SwiftUI here and there, where it’s easy and appropriate.

In general — if you like the concepts you see in SwiftUI – try to apply the very same practices in UIKit too. It’s not that hard. UIKit is wonderful UI framework, which served us very well in the past decade and will likely continue to do so for another.

Lastly, keep this in mind: despite what it may seem when you read blogs or listen to talks, nobody really is an expert in SwiftUI nor Combine.

I, personally, are far from one and may have very well said something today that is not (entirely) correct. That’s OK. It’s the time of learning, experimenting and looking for best practices.

SwiftUI is changing rapidly, every second beta brought significant changes to the core API, which naturally means there are a lot of broken or deprecated code out there on blogs and especially on YouTube and StackOverflow.

Thus you don’t have to learn SwiftUI right now. You have time. Dabble into it here and there but don’t be stressed out nor feel like you are being left behind.

I hope this was useful. Thank you.