An Overview of SwiftUI

Now that Xcode has been installed and the basics of the Swift programing language covered, it is time to start introducing SwiftUI.

First announced at Apple’s Worldwide Developer Conference in 2019, SwiftUI is an entirely new approach to developing apps for all Apple operating system platforms. The basic goals of SwiftUI are to make app development easier, faster and less prone to the types of bugs that typically appear when developing software projects. These elements have been combined with SwiftUI specific additions to Xcode that allow SwiftUI projects to be tested in near real-time using a live preview of the app during the development process.

Many of the advantages of SwiftUI originate from the fact that it is both declarative and data driven, topics which will be explained in this chapter.

The discussion in this chapter is intended as a high-level overview of SwiftUI and does not cover the practical aspects of implementation within a project. Implementation and practical examples will be covered in detail in the remainder of the book.

UIKit and Interface Builder

To understand the meaning and advantages of SwiftUI’s declarative syntax, it helps to understand how user interface layouts were designed before the introduction of SwiftUI. Up until the introduction of SwiftUI, iOS apps were built entirely using UIKit together with a collection of associated frameworks that make up the iOS Software Development Kit (SDK).

To aid in the design of the user interface layouts that make up the screens of an app, Xcode includes a tool called Interface Builder. Interface Builder is a powerful tool that allows storyboards to be created which contain the individual scenes that make up an app (with a scene typically representing a single app screen).

The user interface layout of a scene is designed within Interface Builder by dragging components (such as buttons, labels, text fields and sliders) from a library panel to the desired location on the scene canvas. Selecting a component in a scene provides access to a range of inspector panels where the attributes of the components can be changed.

The layout behavior of the scene (in other words how it reacts to different device screen sizes and changes to device orientation between portrait and landscape) is defined by configuring a range of constraints that dictate how each component is positioned and sized in relation to both the containing window and the other components in the layout.

Finally, any components that need to respond to user events (such as a button tap or slider motion) are connected to methods in the app source code where the event is handled.

At various points during this development process, it is necessary to compile and run the app on a simulator or device to test that everything is working as expected.

SwiftUI Declarative Syntax

SwiftUI introduces a declarative syntax that provides an entirely different way of implementing user interface layouts and behavior from the UIKit and Interface Builder approach. Instead of manually designing the intricate details of the layout and appearance of components that make up a scene, SwiftUI allows the scenes to be described using a simple and intuitive syntax. In other words, SwiftUI allows layouts to be created by declaring how the user interface should appear without having to worry about the complexity of how the layout is actually built.

This essentially involves declaring the components to be included in the layout, stating the kind of layout manager in which they are to be contained (vertical stack, horizontal stack, form, list etc.) and using modifiers to set attributes such as the text on a button, the foreground color of a label, or the method to be called in the event of a tap gesture. Having made these declarations, all the intricate and complicated details of how to position, constrain and render the layout are handled automatically by SwiftUI.

SwiftUI declarations are structured hierarchically, which also makes it easy to create complex views by composing together small, re-usable custom subviews.

While the view layout is being declared and tested, Xcode provides a preview canvas which changes in realtime to reflect the appearance of the layout. Xcode also includes a live preview mode which allows the app to be launched within the preview canvas and fully tested without the need to build and run on a simulator or device.

Coverage of the SwiftUI declaration syntax begins with the chapter entitled “Creating Custom Views with SwiftUI”.

SwiftUI is Data Driven

When we say that SwiftUI is data driven, this is not to say that it is no longer necessary to handle events generated by the user (in other words the interaction between the user and the app user interface). It is still necessary, for example, to know when the user taps a button and to react in some app specific way. Being data driven relates more to the relationship between the underlying app data and the user interface and logic of the app.

Prior to the introduction of SwiftUI, an iOS app would contain code responsible for checking the current values of data within the app. If data is likely to change over time, code has to be written to ensure that the user interface always reflects the latest state of the data (perhaps by writing code to frequently check for changes to the data, or by providing a refresh option for the user to request a data update). Similar problems arise when keeping the user interface state consistent and making sure issues like toggle button settings are stored appropriately. Requirements such as these can become increasingly complex when multiple areas of an app depend on the same data sources.

SwiftUI addresses this complexity by providing several ways to bind the data model of an app to the user interface components and logic that provide the app functionality.

When implemented, the data model publishes data variables to which other parts of the app can then subscribe. Using this approach, changes to the published data are automatically reported to all subscribers. If the binding is made from a user interface component, any data changes will automatically be reflected within the user interface by SwiftUI without the need to write any additional code.

SwiftUI vs. UIKit

With the choice of using UIKit and SwiftUI now available, the obvious question arises as to which is the best option. When making this decision it is important to understand that SwiftUI and UIKit are not mutually exclusive. In fact, several integration solutions are available (a topic area covered starting with the chapter entitled “Integrating UIViews with SwiftUI”).

The first factor to take into consideration during the decision process is that any app that includes SwiftUI-based code that takes advantage of the latest features (such as WidgetKit) will only run on devices running iOS 14 or later. This means, for example, that your app will only be available to users with the following iPhone models:

  • iPhone 11
  • iPhone 11
  • iPhone 11 Pro
  • iPhone 11 Pro Max
  • iPhone XS
  • iPhone XS Max
  • iPhone XR
  • iPhone X
  • iPhone 8
  • iPhone 8 Plus • iPhone 7
  • iPhone 7 Plus
  • iPhone 6s
  • iPhone 6s Plus
  • iPhone SE (1st generation)
  • iPhone SE (2nd generation)
  • iPod touch (7th generation)

Analytics company Mixpanel estimated that, by November 2020, over 70% of all iPhone devices were running iOS 14, a percentage that will continue to increase with the passage of time. The latest adoption numbers can be viewed at the following URL:

https://mixpanel.com/trends/#report/ios_14

If supporting devices running older versions of iOS is not of concern and you are starting a new project, it makes sense to use SwiftUI wherever possible. Not only does SwiftUI provide a faster, more efficient app development environment, it also makes it easier to make the same app available on multiple Apple platforms (iOS, iPadOS, macOS, watchOS and tvOS) without making significant code changes.

If you have an existing app developed using UIKit there is no easy migration path to convert that code to SwiftUI, so it probably makes sense to keep using UIKit for that part of the project. UIKit will continue to be a valuable part of the app development toolset and will be extended, supported and enhanced by Apple for the foreseeable future. When adding new features to an existing project, however, consider doing so using SwiftUI and integrating it into the existing UIKit codebase.

When adopting SwiftUI for new projects, it will probably not be possible to avoid using UIKit entirely. Although SwiftUI comes with a wide array of user interface components, it will still be necessary to use UIKit for certain functionality not yet available in SwiftUI.

In addition, for extremely complex user interface layout designs, it may also be necessary to use Interface Builder in situations where layout needs cannot be satisfied using the SwiftUI layout container views.

Summary

SwiftUI introduces a different approach to app development than that offered by UIKit and Interface Builder. Rather than directly implement the way in which a user interface is to be rendered, SwiftUI allows the user interface to be declared in descriptive terms and then does all the work of deciding the best way to perform the rendering when the app runs.

SwiftUI is also data driven in that data changes drive the behavior and appearance of the app. This is achieved through a publisher and subscriber model.

This chapter has provided a very high-level view of SwiftUI. The remainder of this book will explore SwiftUI in greater depth.

An Introduction to Swift Property Wrappers

Now that the topics of Swift classes and structures have been covered, this chapter will introduce a related topic in the form of property wrappers. Introduced in Swift 5.1, property wrappers provide a way to reduce the amount of duplicated code involved in writing getters, setters and computed properties in class and structure implementations.

Understanding Property Wrappers

When values are assigned or accessed via a property within a class or structure instance it is sometimes necessary to perform some form of transformation or validation on that value before it is stored or read. As outlined in the chapter entitled “The Basics of Swift Object-Oriented Programming”, this type of behavior can be implemented through the creation of computed properties. Frequently, patterns emerge where a computed property is common to multiple classes or structures. Prior to the introduction of Swift 5.1, the only way to share the logic of a computed property was to duplicate the code and embed it into each class or structure implementation. Not only is this inefficient, but a change in the behavior of the computation must be manually propagated across all the entities that use it.

To address this shortcoming, Swift 5.1 introduced a feature known as property wrappers. Property wrappers essentially allow the capabilities of computed properties to be separated from individual classes and structures and reused throughout the app code base.

A Simple Property Wrapper Example

Perhaps the best way to understand property wrappers is to study a very simple example. Imagine a structure with a String property intended to contain a city name. Such a structure might read as follows:

struct Address {
    var city: String
}

If the class was required to store the city name in uppercase, regardless of how it was entered by the user, a computed property such as the following might be added to the structure:

struct Address {
    
    private var cityname: String = ""
    
    var city: String {
        get { cityname }
        set { cityname = newValue.uppercased() }
    }
}

When a city name is assigned to the property, the setter within the computed property converts it to uppercase before storing it in the private cityname variable. This structure can be tested using the following code:

var address = Address() 
address.city = "London" 
print(address.city)

When executed, the output from the above code would read as follows:

LONDON

Clearly the computed property performs the task of converting the city name string to uppercase, but if the same behavior is needed in other structures or classes the code would need to be duplicated in those declarations. In this example this is only a small amount of code, but that won’t necessarily be the case for more complex computations.

Instead of using a computed property, this logic can instead be implemented as a property wrapper. The following declaration, for example, implements a property wrapper named FixCase designed to convert a string to uppercase:

@propertyWrapper
struct FixCase {
    private(set) var value: String = ""
 
    var wrappedValue: String {
        get { value }
        set { value = newValue.uppercased() }
    }
 
    init(wrappedValue initialValue: String) {
        self.wrappedValue = initialValue
    }
}

Property wrappers are declared using the @propertyWrapper directive and are implemented in a class or structure (with structures being the preferred choice). All property wrappers must include a wrappedValue property containing the getter and setter code that changes or validates the value. An optional initializer may also be included which is passed the value being assigned. In this case, the initial value is simply assigned to the wrappedValue property where it is converted to uppercase and stored in the private variable.

Now that this property wrapper has been defined, it can be reused by applying it to other property variables wherever the same behavior is needed. To use this property wrapper, simply prefix property declarations with the @FixCase directive in any class or structure declarations where the behavior is needed, for example:

struct Contact {
     @FixCase var name: String
     @FixCase var city: String
     @FixCase var country: String
 }
  
 var contact = Contact(name: "John Smith", city: "London", country: "United Kingdom")
 print("(contact.name), (contact.city), (contact.country)")

When executed, the following output will appear:

JOHN SMITH, LONDON, UNITED KINGDOM

Supporting Multiple Variables and Types

In the above example, the property wrapper accepted a single value in the form of the value to be assigned to the property being wrapped. More complex property wrappers may also be implemented that accept other values that can be used when performing the computation. These additional values are placed within parentheses after the property wrapper name. A property wrapper designed to restrict a value within a specified range might read as follows:

struct Demo {
    @MinMaxVal(min: 10, max: 150) var value: Int = 100
}

The code to implement the above MinMaxVal property wrapper could be written as follows:

@propertyWrapper
struct MinMaxVal {
  var value: Int
  let max: Int
  let min: Int
    
    init(wrappedValue: Int, min: Int, max: Int) {
    value = wrappedValue
    self.min = min
    self.max = max
  }
 
  var wrappedValue: Int {
    get { return value }
    set {
       if newValue > max {
        value = max
       } else if newValue < min {
        value = min
       } else {
        value = newValue
      }
    }
  }
}

Note that the init() method has been implemented to accept the min and max values in addition to the wrapped value. The wrappedValue setter checks the value and modifies it to the min or max number if it falls above or below the specified range.

The above property wrapper can be tested using the following code:

struct Demo {
    @MinMaxVal(min: 100, max: 200) var value: Int = 100
}
 
var demo = Demo()
demo.value = 150
print(demo.value)
 
demo.value = 250
print(demo.value)

When executed, the first print statement will output 150 because it falls within the acceptable range, while the second print statement will show that the wrapper restricted the value to the maximum permitted value (in this case 200).

As currently implemented, the property wrapper will only work with integer (Int) values. The wrapper would be more useful if it could be used with any variable type which can be compared with another value of the same type. Fortunately, protocol wrappers can be implemented to work with any types that conform to a specific protocol. Since the purpose of this wrapper is to perform comparisons, it makes sense to modify it to support any data types that conform to the Comparable protocol which is included with the Foundation framework. Types that conform to the Comparable protocol are able to be used in equality, greater-than and less-than comparisons. A wide range of types such as String, Int, Date, Date Interval and Character conform to this protocol.

To implement the wrapper so that it can be used with any types that conform to the Comparable protocol, the declaration needs to be modified as follows:

@propertyWrapper
struct MinMaxVal<V: Comparable> {
  var value: V
  let max: V
  let min: V
    
    init(wrappedValue: V, min: V, max: V) {
    value = wrappedValue
    self.min = min
    self.max = max
  }
 
  var wrappedValue: V {
    get { return value }
    set {
       if newValue > max {
        value = max
       } else if newValue < min {
        value = min
       } else {
        value = newValue
      }
    }
  }
}

The modified wrapper will still work with Int values as before but can now also be used with any of the other An Introduction to Swift Property Wrappers types that conform to the Comparable protocol. In the following example, a string value is evaluated to ensure that it fits alphabetically within the min and max string values:

struct Demo {
    @MinMaxVal(min: "Apple", max: "Orange") var value: String = ""
}
 
var demo = Demo()
demo.value = "Banana"
print(demo.value)
// Banana <--- Value fits within alphabetical range and is stored.
 
demo.value = "Pear"
print(demo.value)
// Orange <--- Value is outside of the alphabetical range so is changed to the max value.

Similarly, this same wrapper will also work with Date instances, as in the following example where the value is limited to a date between the current date and one month in the future:

struct DateDemo {
     @MinMaxVal(min: Date(), max: Calendar.current.date(byAdding: .month, 
           value: 1, to: Date())! ) var value: Date = Date()
}

The following code and output demonstrate the wrapper in action using Date values:

var dateDemo = DateDemo()
 
print(dateDemo.value)
// 2019-08-23 20:05:13 +0000. <--- Property set to today by default.
 
dateDemo.value = Calendar.current.date(byAdding: .day, value: 10, to: Date())! // <--- Property is set to 10 days into the future.
print(dateDemo.value)
// 2019-09-02 20:05:13 +0000 <--- Property is within acceptable range and is stored.
dateDemo.value = Calendar.current.date(byAdding: .month, value: 2, to: Date())! // <--- Property is set to 2 months into the future.
 
print(dateDemo.value)
// 2019-09-23 20:08:54 +0000 <--- Property is outside range and set to max date (i.e. 1 month into the future).

Summary

Introduced with Swift 5.1, property wrappers allow the behavior that would normally be placed in the getters and setters of a property implementation to be extracted and reused through the codebase of an app project avoiding the duplication of code within the class and structure declarations. Property wrappers are declared in the form of structures using the @propertyWrapper directive.

Property wrappers are a powerful Swift feature and allow you to add your own custom behavior to the Swift language. In addition to creating your own property wrappers, you will also encounter them when working with the iOS SDK. In fact, pre-defined property wrappers are used extensively when working with SwiftUI as will be covered in later chapters.

An Introduction to Swift Structures and Enumerations

Having covered Swift classes in the preceding chapters, this chapter will introduce the use of structures in Swift. Although at first glance structures and classes look similar, there are some important differences that need to be understood when deciding which to use. This chapter will outline how to declare and use structures, explore the differences between structures and classes and introduce the concepts of value and reference types.

An Overview of Swift Structures

As with classes, structures form the basis of object-oriented programming and provide a way to encapsulate data and functionality into re-usable instances. Structure declarations resemble classes with the exception that the struct keyword is used in place of the class keyword. The following code, for example, declares a simple structure consisting of a String variable, initializer and method:

struct SampleStruct {
    
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func buildHelloMsg() {
        "Hello " + name
    }
}

Consider the above structure declaration in comparison to the equivalent class declaration:

class SampleClass {
    
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func buildHelloMsg() {
        "Hello " + name
    }
}

Other than the use of the struct keyword instead of class, the two declarations are identical. Instances of each type are also created using the same syntax:

let myStruct = SampleStruct(name: "Mark")
let myClass = SampleClass(name: "Mark")

In common with classes, structures may be extended and are also able to adopt protocols and contain initializers.

Given the commonality between classes and structures, it is important to gain an understanding of how the two differ. Before exploring the most significant difference it is first necessary to understand the concepts of value types and reference types.

Value Types vs. Reference Types

While on the surface structures and classes look alike, major differences in behavior occur when structure and class instances are copied or passed as arguments to methods or functions. This occurs because structure instances are value type while class instances are reference type.

When a structure instance is copied or passed to a method, an actual copy of the instance is created, together with any data contained within the instance. This means that the copy has its own version of the data which is unconnected with the original structure instance. In effect, this means that there can be multiple copies of a structure instance within a running app, each with its own local copy of the associated data. A change to one instance has no impact on any other instances.

In contrast, when a class instance is copied or passed as an argument, the only thing duplicated or passed is a reference to the location in memory where that class instance resides. Any changes made to the instance using those references will be performed on the same instance. In other words, there is only one class instance but multiple references pointing to it. A change to the instance data using any one of those references changes the data for all other references.

To demonstrate reference and value types in action, consider the following code:

struct SampleStruct {
    
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func buildHelloMsg() {
        "Hello " + name
    }
}
 
let myStruct1 = SampleStruct(name: "Mark")
print(myStruct1.name) 

When the code executes, the name “Mark” will be displayed. Now change the code so that a copy of the myStruct1 instance is made, the name property changed and the names from each instance displayed:

let myStruct1 = SampleStruct(name: "Mark")
var myStruct2 = myStruct1
myStruct2.name = "David"
 
print(myStruct1.name)
print(myStruct2.name)

When executed, the output will read as follows:

Mark
David

Clearly, the change of name only applied to myStruct2 since this is an actual copy of myStruct1 containing its own copy of the data as shown in Figure 12-1:

Figure 12-1

Contrast this with the following class example:

class SampleClass {
    
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func buildHelloMsg() {
        "Hello " + name
    }
}
 
let myClass1 = SampleClass(name: "Mark")
var myClass2 = myClass1
myClass2.name = "David"
 
print(myClass1.name)
print(myClass2.name)

When this code executes, the following output will be generated:

David
David

In this case, the name property change is reflected for both myClass1 and myClass2 because both are references pointing to the same class instance as illustrated in Figure 12-2 below:

Figure 12-2

In addition to these value and reference type differences, structures do not support inheritance and sub-classing in the way that classes do. In other words, it is not possible for one structure to inherit from another structure. Unlike classes, structures also cannot contain a de-initializer (deinit) method. Finally, while it is possible to identify the type of a class instance at runtime, the same is not true of a struct.

When to Use Structures or Classes

In general, structures are recommended whenever possible because they are both more efficient than classes and safer to use in multi-threaded code. Classes should be used when inheritance is needed, only one instance of the encapsulated data is required, or extra steps need to be taken to free up resources when an instance is de-initialized.

An Overview of Enumerations

Enumerations (typically referred to as enums) are used to create custom data types consisting of pre-defined sets of values. Enums are typically used for making decisions within code such as when using switch statements. An enum might, for example be declared as follows:

enum Temperature {
    case hot
    case warm
    case cold
}

Note that in this example, none of the cases are assigned a value. An enum of this type is essentially used to reference one of a pre-defined set of states (in this case the current temperature being hot, warm or cold). Once declared, the enum may, for example, be used within a switch statement as follows:

func displayTempInfo(temp: Temperature) {
    switch temp {
        case .hot:
            print("It is hot.")
        case .warm:
            print("It is warm.")
        case .cold:
            print("It is cold.")
    }
}

It is also worth noting that because an enum has a definitive set of valid member values, the switch statement does not need to include a default case. An attempt to pass an invalid enum case through the switch will be An Introduction to Swift Structures and Enumerations caught by the compiler long before it has a chance to cause a runtime error.

To test out the enum, the displayTempInfo() function must be passed an instance of the Temperature enum with one of the following three possible states selected:

Temperature.hot
Temperature.warm
Temperature.cold

For example:

displayTempInfo(temp: Temperature.warm)

When executed, the above function call will output the following information:

It is warm.

Individual cases within an enum may also have associated values. Assume, for example, that the “cold” enum case needs to have associated with it a temperature value so that the app can differentiate between cold and freezing conditions. This can be defined within the enum declaration as follows:

enum Temperature {
    case hot
    case warm
    case cold(centigrade: Int)
}

This allows the switch statement to also check for the temperature for the cold case as follows:

func displayTempInfo(temp: Temperature) {
    switch temp {
        case .hot:
            print("It is hot")
        case .warm:
            print("It is warm")
        case.cold(let centigrade) where centigrade <= 0:
            print("Ice warning: \(centigrade) degrees.")
        case .cold:
            print("It is cold but not freezing.")
    }
}

When the cold enum value is passed to the function, it now does so with a temperature value included:

displayTempInfo(temp: Temperature.cold(centigrade: -10))

The output from the above function all will read as follows:

Ice warning: -10 degrees

Summary

Swift structures and classes both provide a mechanism for creating instances that define properties, store values and define methods. Although the two mechanisms appear to be similar, there are significant behavioral differences when structure and class instances are either copied or passed to a method. Classes are categorized as being reference type instances while structures are value type. When a structure instance is copied or passed, an entirely new copy of the instance is created containing its own data. Class instances, on the other hand, are passed and copied by reference, with each reference pointing to the same class instance. Other features unique to classes include support for inheritance and deinitialization and the ability to identify the class type at runtime. Structures should typically be used in place of classes unless specific class features are required.

Enumerations are used to create custom types consisting of a pre-defined set of state values and are of particular use in identifying state within switch statements.

SwiftUI Essentials – iOS 14 Edition

  1. An Introduction to Swift Structures
  2. An Introduction to Swift Property Wrappers
  3. An Overview of SwiftUI
  4. Using Xcode in SwiftUI Mode
  5. SwiftUI Architecture
  6. The Anatomy of a Basic SwiftUI Project
  7. Creating Custom Views with SwiftUI
  8. SwiftUI Stacks and Frames
  9. SwiftUI Lifecycle Event Modifiers
  10. Working with SwiftUI State, Observable and Environment Objects
  11. A SwiftUI Example Tutorial
  12. SwiftUI Observable and Environment Objects – A Tutorial
  13. SwiftUI Data Persistence using AppStorage and SceneStorage
  14. SwiftUI Stack Alignment and Alignment Guides
  15. SwiftUI Lists and Navigation
  16. A SwiftUI List and Navigation Tutorial
  17. An Overview of SwiftUI List, OutlineGroup and DisclosureGroup
  18. A SwiftUI List, OutlineGroup and DisclosureGroup Tutorial
  19. Building SwiftUI Grids with LazyVGrid and LazyHGrid
  20. Building Tabbed Views in SwiftUI
  21. Building Context Menus in SwiftUI
  22. Basic SwiftUI Graphics Drawing
  23. SwiftUI Animation and Transitions
  24. Working with Gesture Recognizers in SwiftUI
  25. Creating a Customized SwiftUI ProgressView
  26. An Overview of SwiftUI DocumentGroup Scenes
  27. A SwiftUI DocumentGroup Tutorial
  28. An Introduction to SwiftUI and SiriKit
  29. A SwiftUI SiriKit Tutorial
  30. Customizing the SiriKit Intent User Interface
  31. A SwiftUI SiriKit NSUserActivity Tutorial
  32. An Overview of SwiftUI Siri Shortcut Integration
  33. A SwiftUI Siri Shortcut Tutorial
  34. Building Widgets with SwiftUI and WidgetKit
  35. A SwiftUI WidgetKit Tutorial
  36. Supporting WidgetKit Size Families in SwiftUI
  37. A SwiftUI WidgetKit Deep Link Tutorial
  38. Adding Configuration Options to a WidgetKit Widget
  39. Integrating UIViews with SwiftUI
  40. Integrating UIViewControllers with SwiftUI
  41. Integrating SwiftUI with UIKit