SwiftUI

SwiftUI: Is It Time? 

Hello, long time no see! Yes, I haven’t been here for a while (a few big changes in my personal life – less time) but I’m back with this article. 

Not long ago, I had the pleasure of being a speaker at HackYeah (the biggest stationary hackathon in Europe). This article is a summary of the topic of my presentation. 

Let’s smoothly transition to the topic. 

As the digital landscape is continuously shifting, so too are the tools we use to shape it. In the realm of iOS development, Apple’s SwiftUI has sparked a transformative discussion. Today, we’ll be diving deep into this very subject, pondering the all-important question: Is SwiftUI the way forward for new projects? My analysis will be based on the iOS platform.

Disclaimer: I don’t consider myself an expert in SwiftUI and haven’t yet deployed a commercial project using it, I’ve built several personal ones. Furthermore, I’ve spent considerable time reading, observing, and analyzing its potential. Like many developers, I’m at a crossroads, determining whether it’s time to initiate commercial projects with SwiftUI. 

SwiftUI: A Brief Overview 

Introduced by Apple in 2019, SwiftUI heralded a new approach to UI development across all Apple platforms using a declarative syntax. 

Its simplicity and user-friendly approach have rapidly made it popular among developers. 

But to truly appreciate the value of SwiftUI, we must rewind and consider the landscape before its inception. 

The Legacy: Imperative Programming Paradigm 

Before the age of SwiftUI, native application development hinged upon platform-specific frameworks. iOS had UIKit, macOS leaned on AppKit, and WatchOS was powered by WatchKit.

The commonality between these? They all operated using an imperative programming paradigm. 

At its core, we’re comparing two distinct methodologies: 

  • Declarative (exemplified by SwiftUI).
  • Imperative (which includes UIKit, AppKit, and others).

The declarative approach is now a prevailing trend in UI development across platforms. Native Android has embraced this with Jetpack Compose (source), while Flutter is fundamentally designed around a similar philosophy (source). The popularity of such frameworks underscores the industry’s shift towards more intuitive and efficient ways of designing user interfaces.

Declarative vs. Imperative: A Simple Analogy 

For those unfamiliar with these terms, let’s ground the discussion with a real-life situation. 

Imagine you need a bottle of water from the supermarket (If a bottle of water is unconvincing, you can replace it with a bottle of something else 😉 ). 

Imperative Approach: 

  • Stand up. 
  • Walk to the door. 
  • Open the door. 
  • Exit the office. 
  • Take the lift. 
  • Exit the building. 
  • Enter the supermarket. 
  • Purchase the bottle of water. 

Declarative Approach: 

  • Please get me a bottle of water. 

While the imperative outlines every single step (the “how”), the declarative method simply states the desired outcome (the “what”). It’s a difference in clarity and focus. Keep this in your mind, imperative is HOW, declarative is WHAT

The Magic of Code: UIKit vs. SwiftUI 

As we dig deeper into our exploration of SwiftUI versus the more traditional UIKit, it’s essential to understand them from a coding perspective. 

// UIKit

class ContentViewController: UIViewController, UITableViewDataSource {
    private var tableView: UITableView!
    private var persons: [Person] = [Person(name: "Tom"), Person(name: "Unknown")]
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
    }
    private func setupTableView() {
        tableView = UITableView()
        tableView.dataSource = self
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.bottomAnchor, constant: CGFloat(16)),
            tableView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        ])
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return persons.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let personCell = tableView.dequeue(dequeueableCell: PersonTableViewCell.self, forIndexPath: indexPath)
        personCell.textLabel?.textColor = .blue
        personCell.textLabel?.text = persons[indexPath.row].name
        return personCell
    }
}

// SwiftUI

struct ContentView: View {
    var persons: [Person] = [Person(name: "Tom"), Person(name: "Unknown")]
    var body: some View {
        List(persons) { person in
            Text(person.name)
                .font(.subheadline)
                .foregroundColor(.blue)
        }
    }
}

The UIKit Paradigm 

UIKit’s approach requires developers to meticulously define each step of the UI creation process. This level of detailed instruction results in longer code, which can be more challenging to understand and maintain. For example, when setting up a table view you have to deal with constraints, table view data source methods, delegate methods, and more. 

The Simplicity of SwiftUI 

On the other hand, SwiftUI offers a refreshing brevity. Expressing a UI element, like a List with text rows, becomes astonishingly straightforward. Developers no longer need to specify constraints or handle table view data source methods. The magic is under the hood. 

In Conclusion: Declarative vs. Imperative 

As a conclusion, we can deduce such statements:

Declarative (SwiftUI): 

  • Focuses on expressing what needs to be done without specifying how to do it. 
  • The developer declares the desired end state, and the system figures out how to achieve that state. 

Imperative (UIKit): 

  • Details the steps that need to be taken to achieve a desired goal. 
  • The developer tells the system how to change its state step by step. 

In terms of reactivity :

SwiftUI: 

  • Is inherently reactive. The UI automatically updates when the underlying data changes. 
  • Uses a data-driven approach, so components reflect the state of the application. 

UIKit: 

  • Typically requires manual updates to the UI when data changes. 
  • The developer needs to observe data changes and update the UI accordingly. 

SwiftUI seems to be better 

Drawing from our discussion and code insights, we can say that SwiftUI is better: 

  • We have one framework for all platforms.
  • Development is easier and faster. 
  • We have less code – easier to understand and easier to maintain.
  • It’s less error-prone – you don’t have to handle all possible UI states on your own. 

For instance, in a provided example, ‘TextField’ and ‘Text’ fields are bound to a ‘name’ property. Any alteration to this property prompts an automatic refresh of these fields. This auto-update feature contrasts with the imperative method, where you’d have to manually invoke an update function. 

struct ContentView: View {
    
    @State private var name = ""
    
    var body: some View {
        Form {
            TextField("Enter your name", text: $name)
            Text("Your name is \(name)")
        }
    }
}

The Advancements of SwiftUI: What Else Makes It Stand Out? 

Let’s look at what else makes SwiftUI a good choice.

1. Complex Lists Simplified: SwiftUI brings ease to building intricate lists. 

Let’s look at this simple example: 

struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData

    var body: some View {
        NavigationView {
            List {
                Text("Hello!")
                
                modelData.features[0].image
                    .resizable()
                    .scaledToFill()
                    .frame(height: 200)
                    .clipped()
                    .listRowInsets(EdgeInsets())

                ForEach(modelData.categories.keys.sorted(), id: \.self) { name in
                    CategoryRow(categoryName: name, items: modelData.categories[name]!)
                }
                .listRowInsets(EdgeInsets())
            }
            .navigationTitle("Featured")
        }
    }
}

struct CategoryRow: View {
    var categoryName: String
    var items: [Mark]

    var body: some View {
        VStack(alignment: .leading) {
            Text(categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)

            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(items) { mark in
                        NavigationLink {
                            MarkDetail(mark: mark)
                        } label: {
                            CategoryItem(mark: mark)
                        }
                    }
                }
            }
            .frame(height: 185)
        }
    }
}

As you can see, gone are the days of exhaustive data source methods or fumbling with index checks in UIKit. Embedding content like scroll views within lists or introducing new elements like text at any position is both intuitive and clean. Headers, footers, and sections too, find a seamless integration. 

2. Large amount of beautiful animations and transitions for free.

In this repository, you can find a lot of helpful animations:

Open SwiftUI Animations

A small example of Apple Music Play/Pause Button Clone:

As you can see, thanks to the declarative nature of SwiftUI, we can easily create many interesting animations.

3. Live preview.

It renders your code modifications in real-time. This immediate visual feedback accelerates development pace and also reduces the “build-run-check” cycles dramatically. Although there are some initial hitches, the potential for this feature to revolutionize development is immense.

I told my non-programmer friend that with live preview “what you see is what you get”, he asked if it works with dating apps too 🙂

4. Integration with the Combine Framework.

SwiftUI and Combine together are like bread and butter, these two are made for each other. The Combine framework introduces powerful property wrappers like @State and @ObservedObject, so that data handling and user interface updates are simplified. 

The Evolutionary Journey of SwiftUI

SwiftUI has been developing very rapidly since its inception. 

The initial release, while groundbreaking, had its limitations. Fundamental UI elements like text, images, and foundational layouts like VStack and HStack were introduced. But, it wasn’t without its set of bugs and constraints. It was like my attempts at cooking: some raw parts, burnt edges and definitely some missing ingredients, like sandwich without bread.

However, the iterations that followed showcased remarkable progress. iOS 14, for example, brought in components like maps and menus. 

iOS 15 improved the list, offering swipe actions, pull-to-refresh, a .searchable modifier, and more. 

But for me personally, most transformative shift related to navigation came with iOS 16. But before diving into that, let’s consider the state of navigation before this update. 

SwiftUI navigation before iOS 16 

Navigation was the main pain point of the framework from the very first day. 

In earlier versions Navigation and Views are strongly coupled and can not be decoupled. This poses the question of how to separate navigation and adopt the coordinator’s pattern

NavigationLink(destination: MyCustomView(item: item))

There are some workarounds and solutions in the community, but they are not perfect. 

The second question related to the above is, how to make reusable views that contain navigation?  

Navigation links are hard-coded in the view and we also need to hard-code and embed the destination when initializing this link. In larger applications this may be a problem. Of course there is a workaround and it can be solved by closures, but with a deep view hierarchy this can look bad and cause a bit of a mess, and we don’t want to work around it, we want native solution. 

// a workaround with closure

struct CategoriesView<Destination: View>: View {
    let categories: [Category]
    let buildDestination: (Category) -> Destination

    var body: some View {
        NavigationView {
            List(categories) { category in
                NavigationLink(destination: self.buildDestination(category)) {
                    Text(category.name)
                }
            }
        }
    }
}

struct CategoriesContainerView: View {
    @State private var categories: [Category] = [.init(), .init(), .init()]
    
    var body: some View {
        CategoriesView(categories: categories) { category in
            // build your destination view here
            // but what if this destination also has a link?
            // imagine you have a NavigationView which goes 10 levels deeper
        }
    }
}

In general we can say that programmatic navigation is difficult. We miss that popping to root, popping two steps back, dismissing the whole navigation stack in the middle of the stack and so on. 

iOS 16 – A Turning Point in SwiftUI Navigation 

Fortunately, Apple listens to us and introduced a new NavigationStack in iOS 16. 

The previous NavigationView (has become deprecated) has been replaced with new navigation APIs. This new system, comprising of NavigationStack and NavigationSplitView, provided a more flexible approach to handling navigation. 

Significant highlights include: 

  • The new navigation link is divided into two tools: navigation link for value-based navigation and navigation destination for specifying the destination view. 
  • The NavigationStack facilitates the separation of the destination view from the currently visible one through the .navigationDestination modifier. This paves the way for a clean coordinator pattern, enabling the easy reuse of views embedded with navigation. 
  • Navigation Stack Path Property: Developers can now modify the navigation path to push new views or navigate backward effortlessly, so finally we can say SwiftUI programmatic navigation has become much easier to implement 

Let’s look at this simple example: 

struct ContentView: View {
    private var bgColors: [Color] = [.indigo, .orange, .yellow, .green]
 
    @State private var path: [Color] = []
 
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                List(bgColors, id: \.self) { bgColor in
                    NavigationLink(value: bgColor) {
                        Text(bgColor.description)
                    }
                }
                .listStyle(.plain)
                
                Button {
                    path.append(.red)
                } label: {
                    Text("\(Color.red.description) - programmatically push")
                }
            }
            .navigationDestination(for: Color.self) { color in
                VStack {
                    Button {
                        path.removeLast()
                    } label: {
                        Text("< Go back")
                    }
                    
                    Text("\(path.count), \(path.description)")
                        .font(.headline)
 
                    HStack {
                        ForEach(path, id: \.self) { color in
                            color
                                .frame(maxWidth: .infinity, maxHeight: .infinity)
                        }
                    }

                    List(bgColors, id: \.self) { bgColor in
                        NavigationLink(value: bgColor) {
                            Text(bgColor.description)
                        }
                    }
                    .listStyle(.plain)
                }
            }
            .navigationTitle("Color")
        }
    }
}

Through the stack path property, developers can control navigation, push new views, or navigate to prior ones. The separation of destination definition via a new modifier enables developers to go beyond rigid view definitions. 

iOS 16 – what else 

  • Swift Charts: A streamlined way to visualize data, Swift Charts allows developers to create comprehensive and customizable charts with an impressively succinct amount of code. 
  • Table Support: Moving beyond the confines of lists, tables in iOS 16 offer diversified row and column options. This addition grants developers a richer toolset for data presentation. 
  • Layout Grid: The layout grid facilitates element placement within a structured row-column matrix, making complex UI design more intuitive. 

So as you can see, we have great new options for building a complex UI and customizing it freely. 

Besides the ones mentioned above, it’s worth checking out the others; there are still quite a few of them (MultiDatePicker, ShareLink, Expandable Textfield, Gauge, etc.). 

What’s new in iOS 17 

With iOS 17, the limelight was largely on ScrollView enhancements: 

  • ScrollView Snap: Implementing paging or snapping between child views no longer demands a custom code, for example combination of HStack and custom ViewModifier. This new addition greatly simplifies the developer experience. 
// iOS 17 implementation

struct ContentView: View {
    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack(spacing: 15) {
                ForEach(0..<10) { i in
                    RoundedRectangle(cornerRadius: 25)
                        .fill(Color(hue: Double(i) / 10, saturation: 1, brightness: 1).gradient)
                        .frame(width: 300, height: 400)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
        .safeAreaPadding(.horizontal, 40)
    }
}

// implementation before iOS 17

struct ContentViewOldWay: View {
    
    var colors: [Color] = [.blue, .green, .red, .orange]
    
    var body: some View {
        HStack(alignment: .center, spacing: 15) {
            ForEach(0..<colors.count) { i in
                colors[i]
                    .frame(width: 300, height: 400, alignment: .center)
                    .cornerRadius(25)
            }
        }.modifier(ScrollingHStackModifier(items: colors.count, itemWidth: 300, itemSpacing: 15))
            .safeAreaPadding(.horizontal, 40)
    }
}

// this modifier has 80 lines of code!
struct ScrollingHStackModifier: ViewModifier {}
// the full implementation of this modifier can be found here: https://levelup.gitconnected.com/snap-to-item-scrolling-debccdcbb22f

scrollview snap

  • ScrollPosition Modifier: Another alleviation from custom coding.
  • Custom Scroll Transitions and Orientation: Options to customize transitions and even make ScrollView start at the bottom gives developers more flexibility. 

In addition to the improvements to the ScrollView, we have received the following enhancements, among others: 

  • In-App Purchases in SwiftUI: With StoreView, SubscriptionStoreView, and ProductView, the monetization route becomes smoother. 
  • Animating SF Symbols: Improvements for animations, now we can even animate SF symbols. 
  • TipKit: You can use it to show contextual tips that highlight new, interesting, or unused features people haven’t discovered on their own yet.

SwiftUI limitations 

Now let’s talk about limitations. 

While SwiftUI boasts a slew of capabilities, certain limitations continue to cast a shadow. 

Comprehensive SwiftUI adoption can be challenging, given its limited API coverage. This often pushes developers to oscillate between SwiftUI and UIKit. 

Initial versions lacked essential components and functionalities, pushing developers to rely on custom implementations or UIKit. 

The first version of SwiftUI didn’t encompass essential components like maps or menus. Additionally, it lacked modal navigation APIs. 

Pre-iOS 15 versions faced challenges in customizing lists, such as changing backgrounds, adding swipe actions, pull-to-refresh capabilities, and the .searchable modifier. 

Before iOS 16, there were problems with navigation, we didn’t have such cool components as Grid and Table. 

Prior to iOS 17, ScrollView posed multiple challenges, including state determination (identifying if the view was dragging, scrolling, or even determining the offset wasn’t straightforward, there are some workarounds for this, but they can be unstable), and the absence of native paging. 

The current version of SwiftUI still doesn’t have native support for WebView or SafariViewController, so we don’t have a native way to present an in-app browser in SwiftUI. 

Text input is still very limited – for example, if we want to do advanced formatting, validation. 

And there is a crucial point to remember – each and every version of SwiftUI is explicitly tied to a specific version of iOS. Therefore, using SwiftUI 4.0 in iOS 14 isn’t feasible. You’d be limited to SwiftUI 2.0 and its associated constraints. Hence, it’s vital to decide the minimum iOS version your project supports, especially considering the aforementioned limitations. 

Bugs – The Unwanted Companions 

Like any evolving framework, bugs in SwiftUI can disrupt the development workflow. 

For example, we can even find a painful bug in iOS 16. It’s a bug where the onReceive and onAppear handlers for list elements are not always being called, which caused that some thumbnails to not appear in the list in applications. 

The Reason was that under iOS 16, List views are no longer backed by table views but by collection views, and Apple developers probably forgot to support/handle/move a few methods. 

Of course we can fix this by replacing the list with a scroll view and a lazy stack. 

But shame on you, Apple, for releasing something like this into production. 

And there are many other bugs in production, like putting a LazyVStack or LazyHStack in a ScrollView causes stuttering (in some project such behaviour may be unacceptable) and some of the bugs are still not fixed.  

My recommendation – if something doesn’t work, check in the community, it’s probably a bug. 

Someone may say that this is not a bug, but a “feature in progress”. 

Is it now? SwiftUI Today: A Commercial Perspective 

And we’ve come to the point of answering the question – Is now the time to use SwiftUI for commercial projects? 

It’s like deciding when to eat an avocado.

Please remember that this is my personal point of view, opinion. 

If we have to support iOS 13 – please no! Let’s stick to UIKit, as you have seen the first version of SwiftUI has many limitations, is immature and has many bugs. 

iOS 14 – it depends, if it’s a small project, simple user interface (compliant with Apple guidelines), then maybe – if it’s a more complex project – I would hold off.

IOS 15 – it’s better than 14, we have more options to customize the list, so for small to medium-sized projects, it could be a viable choice. 

iOS 16, 17 – These versions introduced significant improvements in navigation, new components such as tables and grids, and improvements to scroll views. They appear promising for even larger, sophisticated projects, so I would say yes. 

But remember. This is a general gut feeling, these recommendations aren’t set in stone. The specific needs of a project, especially its UI demands, should always be the guiding factor, so I recommend to do a very good analysis based on the UI. 

If you see that you need to do more workarounds in UIKit compared to the code in SwiftUI, please stay with UIKit as your main framework. And keep in mind that UIKit is mature and battle-tested, rich in components with detailed customization options, and great support, tons of resources and documentation available. 

Gleanings from the SwiftUI Experience

Here are some tips I learned during my adventure with SwiftUI.

1. Class vs. Struct.

In UIKit, we leaned on classes for UI and structs for data. SwiftUI flips the script. In UIKit, every view descended from a class called UIView that had many properties and methods and every UIView and UIView subclass had to have them, because that’s how inheritance works. 

Structs are simpler and faster than classes, and in SwiftUI views are trivial structs that’s why they are almost free to create. No extra values inherited from parent classes, or grandparent classes. They contain exactly what you can see and nothing more. It’s important in terms of modifiers, as whenever we apply a modifier to a SwiftUI view, we actually create a new view with that change applied (and so such creation of new instances can be really many), we don’t just modify the existing view in place, an additional point to remember is that the order of modifiers is therefore important. 

2. List row and button inside (.buttonStyle), plus ContentShape. 

By default, when you put a Button in a List row it will make the whole row a Button, and it doesn’t matter that it doesn’t fill the entire cell, list does a lot of automatic rearrangement of things placed in it that isn’t well or fully documented. 

However, adjusting the button style property to ‘plain’ ensures only the button remains clickable: 

struct MultipleCategorySelectionRow: View {
    @ObservedRealmObject var category: Category
    var action: () -> Void

    var body: some View {
        Button(action: self.action) {
            HStack {
                HStack {
                    Image(systemName: category.symbol)
                        .foregroundColor(category.color)
                        .frame(width: 45)
                    Text(category.name)
                        .foregroundColor(.text)
                }
                Spacer()
                if category.isSelected {
                    Image(systemName: "checkmark").foregroundColor(.accentColor)
                }
            }.contentShape(Rectangle())
        }.buttonStyle(.plain)
    }
    
}

Another point of interest is the contentShape() modifier.  

If you add a tap gesture to container views like VStack or HStack, SwiftUI interprets the gesture for only the occupied spaces. To modify the tappable area, one can utilize the contentShape() modifier. You can look at the same example above.

3. TabView Navigation.

Each tab can have a NavigationStack but a TabView shouldn’t be inside one. They should be “mutually exclusive”. So if you have a transition, e.g. from onboarding to a view with tabs, you can’t do it with basic navigation, you have to change the root view. 

Recommendations 

For those on the fence about SwiftUI vs. UIKit, the “SwiftUI Companion App” might be a worthy download: 

SwiftUI Companion App

This app offers exhaustive interactive documentation for SwiftUI views, shapes, protocols, scenes, styles, property wrappers, and environment values across all platforms. 

App has more than 2000 entries. 

Each entry in the application comes with examples. Many of these are interactive and run directly within the app. 

I haven’t tested it personally yet, but looking at the description and demo video it should be useful. 

For those interested in learning SwiftUI, I personally recommend “100 Days with SwiftUI” by Paul Hudson: 

100 Days with SwiftUI

Other links: 

Top 7 resources to learn SwiftUI

Furthermore, for a peek into the vast potential of SwiftUI, the exyte group on GitHub showcases intriguing SwiftUI-based libraries: 

Exyte Group

Thank you 😉 

Leave a Reply

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