When Is Swift UI Better Than UIKit? Comparing the Two iOS App Development Choices.
Table of Contents

When Is Swift UI Better Than UIKit? Comparing the Two iOS App Development Choices.

There is no better time than now to write about the reasoning behind using Swift UI or UI Kit in your native iOS apps. Why now? iOS 16 brings many new, exciting features. What’s more important, is that it gave us developers all the goodies that come with the latest iteration of the Swift UI; we can use the latest APIs that were recently unavailable.

Lucky Coincidence? A Well-Planned Strategy?

You may call it a lucky coincidence or well-planned strategic decisions by Apple engineers when crafting the complete update experience, but the reality is that iOS users tend to keep their devices updated more than not. That’s important because it defines what APIs can be used and keeps the balance between supporting older OS versions and being efficient at developing with latest APIs. The fragmentation of system versions is not as visible as it used to be (and still is) in the Android world.

Obviously, not all users update their devices the same day an update is available (one aspect is the bugs, another is not everybody has the mind or the internet connection for it). That would have been too good to be true; at least for us, developers. That’s why we rarely see an app that supports just the latest version, especially if we’re still freshly after the date the update was made available.

From our experience, apps usually support the latest 2 major releases of the operating system, i.e., iOS 15 and iOS 14. The time in the fall after Apple publishes the new major OS update is always the time when we would drop the oldest version and (eventually) clean up the code to go with the new set of the latest features supported by the last 2 major OS versions. For example, following that logic now we’re getting into the period where it is time to increment the minimum OS version needed to run our apps to iOS 15. Keep in mind, the decision about what’s the minimum version that we should keep supporting was always made by looking at usage statistics for the app. It’s just that repeatedly, year over year, we were finding out that eventually in the fall most users were running one of the 2 latest iOS versions. If most of your users are still using the iOS version from 3 years ago, don’t drop the support for them.

That’s good news for Swift UI as when targeting iOS 14 there are some significant APIs missing, and we would easily hit major obstacles if we wanted to do “too much” with Swift UI and still wanted to support iOS 14.  

We’ll show some code examples about how we could implement some features that would be not worth the effort if we wanted to create those features in the declarative Swift framework. On the other hand, iOS 15 provided solutions to all those listed issues, and we will demonstrate how a solution would be created with Swift UI.

If our job was to create a UI element that was supposed to display a text, where every character could have its own attributes, we would choose UI Kit when targeting iOS14. Since now, we can drop iOS14 and keep only iOS15 and iOS16, we may safely use Swift UI. The code examples below show how to achieve more-or-less the same result, but first using Swift UI, and then using UIKit.

Let's dive in.

Examples

Let's start off with a simple task.

Creating a Text View Where the Text Is Colored as a Rainbow, Every Character in Its Own Color.

Swift UI

The ContentView implementation

And the RainbowText implementation

import SwiftUI 

 

struct RainbowText: View { 

    private var attributedString: AttributedString 

 

    private static let colors: [Color] = [ 

        .rainbowRed, 

        .rainbowOrange, 

        .rainbowYellow, 

        .rainbowGreen, 

        .rainbowBlue, 

        .rainbowIndigo, 

        .rainbowViolet 

    ] 

 

    var body: some View { 

        Text(attributedString) 

    } 

 

    init(withString string: String) { 

        self.attributedString = RainbowText.annotateRainbowColors(from: string) 

    } 

 

    private static func annotateRainbowColors(from source: String) -> AttributedString { 

        var attrString = AttributedString(stringLiteral: source) 

        var characterIndex = attrString.startIndex 

        let rainbowColors = RainbowText.colors 

        var colorCounter = 0 

        while characterIndex < attrString.endIndex { 

            let nextIndex = attrString.characters.index(characterIndex, offsetBy: 1) 

            attrString[characterIndex ..< nextIndex].foregroundColor = rainbowColors[colorCounter] 

            colorCounter += 1 

            if colorCounter >= rainbowColors.count { 

                colorCounter = 0 

            } 

            characterIndex = nextIndex 

        } 

        return attrString 

    } 

} 

UIKit

For the sake of comparison, here’s a UIKit implementation for a similar UI component:

import UIKit 

 

class ViewController: UIViewController { 

 

    @IBOutlet weak var label: UILabel! 

    let text = "This rainbow is so fun!!!!" 

 

    override func viewDidLoad() { 

        super.viewDidLoad() 

        label.attributedText = rainbowText(from: text) 

    } 

 

    func rainbowText(from text: String) -> NSAttributedString { 

        let attrString = NSMutableAttributedString(string: text) 

        let rainbowColors = Rainbow.colors 

        var index = 0 

        var colorCounter = 0 

        while index < attrString.length { 

            let range = NSRange(location: index, length: 1) 

            attrString.addAttribute(.foregroundColor, value: rainbowColors[colorCounter], range: range) 

            colorCounter += 1 

            if colorCounter >= rainbowColors.count { 

                colorCounter = 0 

            } 

            index += 1 

        } 

        return attrString 

    } 

} 

 

Navigation on an iPad

Here’s an example with navigation, UIKit vs. SwiftUI (iPad-specific)

Swift UI

import SwiftUI 

  

struct MyView: View { 

  

    struct Item: Identifiable { 

        let id: Int 

    } 

  

    var items = [1, 2, 3, 7].map(Item.init(id:)) 

  

    var body: some View { 

        NavigationView { 

            List { 

                ForEach(items) { item in 

                    NavigationLink { 

                        Text("Detail view for item \(item.id)") 

                    } label: { 

                        Text("Item \(item.id)") 

                    } 

                } 

            } 

        }.navigationViewStyle(.columns) 

    } 

} 

UIKit

import UIKit 

  

final class BasicCell: UITableViewCell { 

  

    let label = UILabel() 

  

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 

        super.init(style: style, reuseIdentifier: reuseIdentifier) 

        let marginsGuide = contentView.layoutMarginsGuide 

        label.translatesAutoresizingMaskIntoConstraints = false 

  

        contentView.addSubview(label) 

        NSLayoutConstraint.activate([ 

            label.widthAnchor.constraint(equalTo: marginsGuide.widthAnchor), 

            label.heightAnchor.constraint(equalTo: marginsGuide.heightAnchor), 

            label.centerXAnchor.constraint(equalTo: marginsGuide.centerXAnchor), 

            label.centerYAnchor.constraint(equalTo: marginsGuide.centerYAnchor) 

        ]) 

    } 

  

    required init?(coder: NSCoder) { 

        fatalError("init(coder:) has not been implemented") 

    } 

} 

  

final class RootViewController: UITableViewController { 

  

    var items = [1, 2, 3, 7] { 

        didSet { 

            tableView.reloadData() 

        } 

    } 

  

    override func viewDidLoad() { 

        super.viewDidLoad() 

        tableView.register(BasicCell.self, forCellReuseIdentifier: "BasicCell") 

    } 

  

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 

        items.count 

    } 

  

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 

        let cell = tableView.dequeueReusableCell(withIdentifier: "BasicCell", for: indexPath) 

        (cell as? BasicCell)?.label.text = "Item \(items[indexPath.row])" 

        return cell 

    } 

  

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 

        let viewController = DetailViewController() 

        viewController.item = items[indexPath.row] 

        splitViewController?.setViewController(UINavigationController(rootViewController: viewController), for: .secondary) 

        splitViewController?.show(.secondary) 

    } 

} 

  

final class DetailViewController: UIViewController { 

  

    var item: Int? 

  

    override func viewDidLoad() { 

        super.viewDidLoad() 

        view.backgroundColor = .systemBackground 

  

        let label = UILabel() 

        label.translatesAutoresizingMaskIntoConstraints = false 

        label.text = item.map { "Detail view for item \($0)" } 

  

        view.addSubview(label) 

        NSLayoutConstraint.activate([ 

            label.centerXAnchor.constraint(equalTo: view.centerXAnchor), 

            label.centerYAnchor.constraint(equalTo: view.centerYAnchor) 

        ]) 

    } 

} 

// ...

let viewController = UISplitViewController(style: .doubleColumn) 

viewController.setViewController(RootViewController(), for: .primary) 

viewController.preferredDisplayMode = .oneBesideSecondary 

viewController.preferredSplitBehavior = .displace 

viewController.show(.primary) 

viewController.modalPresentationStyle = .fullScreen 

present(viewController, animated: true) 

CoreData & Two-Directional Collection View

Do notice the difference in the length between the implementations.

Swift UI

import SwiftUI 

import CoreData 

  

struct ContentView: View { 

    @Environment(\.managedObjectContext) private var viewContext 

  

    @FetchRequest( 

        sortDescriptors: [SortDescriptor(\.name, order: .forward)], 

        animation: .default) 

    private var items: FetchedResults<ShoppingListItem>

  

    var body: some View { 

        NavigationView { 

            List { 

                ForEach(items) { item in 

                    HStack { 

                        Text(item.name!) 

                        Spacer() 

                        Text(item.quantity.map { "\($0)" } ?? "") 

                    } 

                } 

                .onDelete(perform: deleteItems) 

            } 

            .toolbar { 

                ToolbarItem(placement: .navigationBarTrailing) { 

                    EditButton() 

                } 

                ToolbarItem { 

                    Button(action: addItem) { 

                        Label("Add Item", systemImage: "plus") 

                    } 

                } 

            } 

        } 

    } 

  

    private func addItem() { 

        withAnimation { 

            let newItem = ShoppingListItem(context: viewContext) 

            newItem.name = ["Apples", "Oranges", "Bananas"].randomElement() 

            newItem.quantity = (2...10).randomElement().map { Decimal($0) as NSDecimalNumber } 

  

            do { 

                try viewContext.save() 

            } catch { 

                // Handle errors 

            } 

        } 

    } 

  

    private func deleteItems(offsets: IndexSet) { 

        withAnimation { 

            offsets.map { items[$0] }.forEach(viewContext.delete) 

  

            do { 

                try viewContext.save() 

            } catch { 

                // Handle errors 

            } 

        } 

    } 

} 

UIKit

import UIKit 

import CoreData 

  

final class ItemCell: UITableViewCell { 

  

    let nameLabel = UILabel() 

    let quantityLabel = UILabel() 

  

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 

        super.init(style: style, reuseIdentifier: reuseIdentifier) 

        quantityLabel.setContentHuggingPriority(.required, for: .horizontal) 

        quantityLabel.setContentCompressionResistancePriority(.required, for: .horizontal) 

        let stackView = UIStackView(arrangedSubviews: [nameLabel, quantityLabel]) 

        let marginsGuide = contentView.layoutMarginsGuide 

        stackView.translatesAutoresizingMaskIntoConstraints = false 

  

        contentView.addSubview(stackView) 

        NSLayoutConstraint.activate([ 

            stackView.widthAnchor.constraint(equalTo: marginsGuide.widthAnchor), 

            stackView.heightAnchor.constraint(equalTo: marginsGuide.heightAnchor), 

            stackView.centerXAnchor.constraint(equalTo: marginsGuide.centerXAnchor), 

            stackView.centerYAnchor.constraint(equalTo: marginsGuide.centerYAnchor) 

        ]) 

    } 

  

    required init?(coder: NSCoder) { 

        fatalError("init(coder:) has not been implemented") 

    } 

} 

  

class ViewController: UITableViewController { 

  

    struct Item { 

        let id: NSManagedObjectID 

        let name: String 

        let quantity: Decimal? 

  

        init?(fetched: ShoppingListItem) { 

            guard let name = fetched.name else { return nil } 

            self.id = fetched.objectID 

            self.name = name 

            self.quantity = fetched.quantity as Decimal? 

        } 

    } 

  

    var items: [Item] = [] { 

        didSet { 

            tableView.reloadData() 

        } 

    } 

  

    private var viewContext: NSManagedObjectContext { (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext } 

  

    private lazy var defaultBarItems: [UIBarButtonItem] = [ 

        .init(barButtonSystemItem: .edit, target: self, action: #selector(editButtonTapped)), 

        .init(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped)) 

    ] 

  

    override func viewDidLoad() { 

        super.viewDidLoad() 

        navigationItem.rightBarButtonItems = defaultBarItems 

        tableView.register(ItemCell.self, forCellReuseIdentifier: "ItemCell") 

        loadItems() 

    } 

  

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 

        items.count 

    } 

  

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 

        let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) 

        if let cell = cell as? ItemCell { 

            let item = items[indexPath.row] 

            cell.nameLabel.text = item.name 

            cell.quantityLabel.text = item.quantity.map { "\($0)" } 

        } 

        return cell 

    } 

  

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 

        guard editingStyle == .delete else { 

            return 

        } 

  

        let id = items.remove(at: indexPath.row).id 

        let viewContext = viewContext 

        viewContext.perform { 

            viewContext.delete(viewContext.object(with: id)) 

            do { 

                try viewContext.save() 

            } catch { 

                // Handle errors 

            } 

        } 

    } 

  

    private func loadItems() { 

        let viewContext = viewContext 

        viewContext.perform { [weak self] in 

            do { 

                self?.items = try viewContext.fetch(ShoppingListItem.fetchRequest()).compactMap(Item.init(fetched:)) 

            } catch { 

                // Handle errors 

            } 

        } 

    } 

  

    @objc 

    private func addButtonTapped() { 

        let viewContext = viewContext 

        viewContext.perform { 

            let object = ShoppingListItem(context: viewContext) 

            object.name = ["Apples", "Oranges", "Bananas"].randomElement() 

            object.quantity = (2...10).randomElement().map { Decimal($0) as NSDecimalNumber } 

            do { 

                try viewContext.save() 

            } catch { 

                // Handle errors 

            } 

        } 

  

        loadItems() 

    } 

  

    @objc 

    private func editButtonTapped() { 

        tableView.setEditing(true, animated: true) 

        navigationItem.rightBarButtonItem = .init(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped)) 

    } 

  

    @objc 

    private func doneButtonTapped() { 

        tableView.setEditing(false, animated: true) 

        navigationItem.rightBarButtonItems = defaultBarItems 

    } 

} 

 

In the projects from the past five years, it's hard to find an edge case that would make using UIKit a better choice. Surely, there still are completely valid use cases where it makes sense implementing a specific UI feature with UIKit. If a few years ago you wanted to have an app that’s mostly built with UIKit, and occasionally you would include a Swift UI view in just some parts of the UI; as a component wrapped in a container. Currently, we could say that we have the opposite situation: we should be able to build apps efficiently using the new framework and eventually if we run into an edge case where a Swift UI implementation would exceed the expected complexity, then you implement that component using UIKit and just wrap it with a Swift UI view.

Closing Words

One last thing; Swift UI's learning curve. Probably the majority of us would agree it’s quite a gentle learning curve. Especially when looking at Swift UI demo videos from Apple or following some tutorials, the learning curve seems even gentler - you can easily achieve all the results you want...  

...except when you don’t. Until you get enough mileage, it’s easy to make an error and spend hours to find a solution and eventually giving up and falling back to good old UIKit. The better you know it, the easier it is to fall back to it, too. But the time is right, embrace Swift UI with open hands, it won’t bite. Alternatively, if you need help migrating to SwiftUI, contact us, and we will be glad to help.

Liked the article? subscribe to updates!
360° IT Check is a weekly publication where we bring you the latest and greatest in the world of tech. We cover topics like emerging technologies & frameworks, news about innovative startups, and other topics which affect the world of tech directly or indirectly.

Like what you’re reading? Make sure to subscribe to our weekly newsletter!
Relevant Expertise:
No items found.
Share

Join 17,850 tech enthusiasts for your weekly dose of tech news

By filling in the above fields and clicking “Subscribe”, you agree to the processing by ITMAGINATION of your personal data contained in the above form for the purposes of sending you messages in the form of newsletter subscription, in accordance with our Privacy Policy.
Thank you! Your submission has been received!
We will send you at most one email per week with our latest tech news and insights.

In the meantime, feel free to explore this page or our Resources page for eBooks, technical guides, GitHub Demos, and more!
Oops! Something went wrong while submitting the form.

Related articles

Our Partners & Certifications
Microsoft Gold Partner Certification 2021 for ITMAGINATION
ISO-IEC 27001:2013 ITMAGINATION ISO 9001 ITMAGINATION
© 2024 ITMAGINATION, A Virtusa Company. All Rights Reserved.