Creating Custom Views with SwiftUI

A key step in learning to develop apps using SwiftUI is learning how to declare user interface layouts both by making use of the built-in SwiftUI views as well as building your own custom views. This chapter will introduce the basic concepts of SwiftUI views and outline the syntax used to declare user interface layouts and modify view appearance and behavior.

SwiftUI Views

User interface layouts are composed in SwiftUI by using, creating and combining views. An important first step is to understand what is meant by the term “view”. Views in SwiftUI are declared as structures that conform to the View protocol. In order to conform with the View protocol, a structure is required to contain a body property and it is within this body property that the view is declared.

SwiftUI includes a wide range of built-in views that can be used when constructing a user interface including text label, button, text field, menu, toggle and layout manager views. Each of these is a self-contained instance that complies with the View protocol. When building an app with SwiftUI you will use these views to create custom views of your own which, when combined, constitute the appearance and behavior of your user interface.

These custom views will range from subviews that encapsulate a reusable subset of view components (perhaps a secure text field and a button for logging in to screens within your app) to views that encapsulate the user interface for an entire screen. Regardless of the size and complexity of a custom view or the number of child views encapsulated within, a view is still just an instance that defines some user interface appearance and behavior.

Creating a Basic View

In Xcode, custom views are contained within SwiftUI View files. When a new SwiftUI project is created, Xcode will create a single SwiftUI View file containing a single custom view consisting of a single Text view component. Additional view files can be added to the project by selecting the File -> New -> File… menu option and choosing the SwiftUI View file entry from the template screen.

The default SwiftUI View file is named ContentView.swift and reads as follows:

import SwiftUI
 
struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}
 
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The view is named ContentView and is declared as conforming to the View protocol. It also includes the mandatory body property which, in turn contains an instance of the built-in Text view component which is initialized with a string which reads “Hello, world!”.

The second structure in the file is needed to create an instance of ContentView so that it appears in the preview canvas, a topic which will be covered in detail in later chapters.

Adding Additional Views

Additional views can be added to a parent view by placing them in the body. The body property, however, is configured to return a single view. Adding an additional view, as is the case in the following example, will cause Xcode to create a second preview containing just the “Goodbye, world!” text view:

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
        Text("Goodbye, world!")
    }
}

To correctly add additional views, those views must be placed in a container view such as a stack or form. The above example could, therefore, be modified to place the two Text views in a vertical stack (VStack) view which, as the name suggests, positions views vertically within the containing view:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
                .padding()
            Text("Goodbye, world!")
        }
    }
}

SwiftUI views are hierarchical by nature, starting with parent and child views. This allows views to be nested to multiple levels to create user interfaces of any level of complexity. Consider, for example, the following view hierarchy diagram:

Figure 20-1

The equivalent view declaration for the above view would read as follows:

struct ContentView: View {
    var body: some View {
        VStack {
            VStack {
                Text("Text 1")
                Text("Text 2")
                HStack {
                    Text("Text 3")
                    Text("Text 4")
                }
            }
            Text("Text 5")
        }
    }
}

A notable exception to the requirement that multiple views be embedded in a container is that multiple Text views count as a single view when concatenated. The following, therefore, is a valid view declaration:

struct ContentView: View {
    var body: some View {
        Text("Hello, ") + Text("how ") + Text("are you?")
    }
}

Note that in the above examples the closure for the body property does not have a return statement. This is because the closure essentially contains a single expression (implicit returns from single expressions were covered in the chapter entitled “Swift Functions, Methods and Closures”). As soon as extra expressions are added to the closure, however, it will be necessary to add a return statement, for example:

struct ContentView: View {
    var body: some View {
 
    var myString: String = "Welcome to SwiftUI"
        
    return VStack {
            Text("Hello, world!")
                .padding()
            Text("Goodbye, world")
        }
    }
}

Working with Subviews

Apple recommends that views be kept as small and lightweight as possible. This promotes the creation of reusable components, makes view declarations easier to maintain and results in more efficient layout rendering.

If you find that a custom view declaration has become large and complex, identify areas of the view that can be extracted into a subview. As a very simplistic example, the HStack view in the above example could be extracted as a subview named “MyHStackView” as follows:

struct ContentView: View {
    var body: some View {
        VStack {
            VStack {
                Text("Text 1")
                Text("Text 2")
                MyHStackView()
            }
            Text("Text 5")
        }
    }
}
 
struct MyHStackView: View {
    var body: some View {
        HStack {
            Text("Text 3")
            Text("Text 4")
        }
    }
}

Views as Properties

In addition to creating subviews, views may also be assigned to properties as a way to organize complex view hierarchies. Consider the following example view declaration:

struct ContentView: View {
    
    var body: some View {
        
        VStack {
            Text("Main Title")
                .font(.largeTitle)
            HStack {
                Text("Car Image")
                Image(systemName: "car.fill")
            }
        }
    }
}

Any part of the above declaration can be moved to a property value, and then referenced by name. In the following declaration, the HStack has been assigned to a property named carStack which is then referenced within the VStack layout:

struct ContentView: View {
    
    let carStack = HStack {
        Text("Car Image")
        Image(systemName: "car.fill")
    }
    
    var body: some View {
        VStack {
            Text("Main Title")
                .font(.largeTitle)
            carStack
        }
    }
}

Modifying Views

It is unlikely that any of the views provided with SwiftUI will appear and behave exactly as required without some form of customization. These changes are made by applying modifiers to the views.

All SwiftUI views have sets of modifiers which can be applied to make appearance and behavior changes. These modifiers take the form of methods that are called on the instance of the view and essentially wrap the original view inside another view which applies the necessary changes. This means that modifiers can be chained together to apply multiple modifications to the same view. The following, for example, changes the font and foreground color of a Text view:

Text("Text 1")
    .font(.headline)
    .foregroundColor(.red)

Similarly, the following example uses modifiers to configure an Image view to be resizable with the aspect ratio set to fit proportionally within the available space:

Image(systemName: "car.fill")
    .resizable()
    .aspectRatio(contentMode: .fit)

Modifiers may also be applied to custom subviews. In the following example, the font for both Text views in the previously declared MyHStackView custom view will be changed to use the large title font style:

MyHStackView()
    .font(.largeTitle)

Working with Text Styles

In the above example the font used to display text on a view was declared using a built-in text style (in this case the large title style).

iOS provides a way for the user to select a preferred text size which applications are expected to adopt when displaying text. The current text size can be configured on a device via the Settings -> Display & Brightness -> Text Size screen which provides a slider to adjust the font size as shown below:

Figure 20-2

If a font has been declared on a view using a text style, the text size will dynamically adapt to the user’s preferred font size. Almost without exception, the built-in iOS apps adopt the preferred size setting selected by the user when displaying text and Apple recommends that third-party apps also conform to the user’s chosen text size. The following text style options are currently available:

  • Large Title
  • Title, Title2, Title 3
  • Headline
  • Subheadline
  • Body
  • Callout
  • Footnote
  • Caption1, Caption2

If none of the text styles meet your requirements, it is also possible to apply custom fonts by declaring the font family and size. Although the font size is specified in the custom font, the text will still automatically resize based on the user’s preferred dynamic type text size selection:

Text("Sample Text")
    .font(.custom("Copperplate", size: 70)) 

The above custom font selection will render the Text view as follows:

Figure 20-3

Modifier Ordering

When chaining modifiers, it is important to be aware that the order in which they are applied can be significant. Both border and padding modifiers have been applied to the following Text view.

Text("Sample Text")
    .border(Color.black)
    .padding()

The border modifier draws a black border around the view and the padding modifier adds space around the view. When the above view is rendered it will appear as shown in Figure 20-4:

Figure 20-4

Given that padding has been applied to the text, it might be reasonable to expect there to be a gap between the text and the border. In fact, the border was only applied to the original Text view. Padding was then applied to the modified view returned by the border modifier. The padding is still applied to the view, but outside of the border. For the border to encompass the padding, the order of the modifiers needs to be changed so that the border is drawn on the view returned by the padding modifier:

Text("Sample Text")
    .padding()
    .border(Color.black)

With the modifier order switched, the view will now be rendered as follows:

Figure 20-5

If you don’t see the expected effects when working with chained modifiers, keep in mind this may be because of the order in which they are being applied to the view.

Custom Modifiers

SwiftUI also allows you to create your own custom modifiers. This can be particularly useful if you have a standard set of modifiers that are frequently applied to views. Suppose that the following modifiers are a common requirement within your view declarations:

Text("Text 1")
    .font(.largeTitle)
    .background(Color.white)
    .border(Color.gray, width: 0.2)
    .shadow(color: Color.black, radius: 5, x: 0, y: 5)

Instead of applying these four modifiers each time text with this appearance is required, a better solution is to group them into a custom modifier and then reference that modifier each time the modification is needed. Custom modifiers are declared as structs that conform to the ViewModifier protocol and, in this instance, might be implemented as follows:

struct StandardTitle: ViewModifier {
   func body(content: Content) -> some View {
        content
            .font(.largeTitle)
            .background(Color.white)
            .border(Color.gray, width: 0.2)
            .shadow(color: Color.black, radius: 5, x: 0, y: 5)
    }
}

The custom modifier is then applied when needed by passing it through to the modifier() method:

Text("Text 1")
    .modifier(StandardTitle())
Text("Text 2")
    .modifier(StandardTitle())

With the custom modifier implemented, changes can be made to the StandardTitle implementation and those changes will automatically propagate through to all views that use the modifier. This avoids the need to manually change the modifiers on multiple views.

Basic Event Handling

Although SwiftUI is described as being data driven, it is still necessary to handle the events that are generated when a user interacts with the views in the user interface. Some views, such as the Button view, are provided solely for the purpose of soliciting user interaction. In fact, the Button view can be used to turn a variety of different views into a “clickable” button. A Button view needs to be declared with the action method to be called when a click is detected together with the view to act as the button content. It is possible, for example, to designate an entire stack of views as a single button. In most cases, however, a Text view will typically be used as the Button content. In the following implementation, a Button view is used to wrap a Text view which, when clicked, will call a method named buttonPressed():

struct ContentView: View {
    var body: some View {
        Button(action: buttonPressed) {
            Text("Click Me")
        }
    }
    
    func buttonPressed() {
        // Code to perform action here
    } 
}

Instead of specifying an action function, the code to be executed when the button is clicked may also be specified as a closure in-line with the declaration:

Button(action: {
    // Code to perform action here
}) {
    Text("Click Me")
}

Another common requirement is to turn an Image view into a button, for example:

Button(action: {
    print("Button clicked")
}) {
    Image(systemName: "square.and.arrow.down")
}

Building Custom Container Views

As outlined earlier in this chapter, subviews provide a useful way to divide a view declaration into small, lightweight and reusable blocks. One limitation of subviews, however, is that the content of the container view is static. In other words, it is not possible to dynamically specify the views that are to be included at the point that a subview is included in a layout. The only children included in the subview are those that are specified in the original declaration.

Consider the following subview which consists of three TextViews contained within a VStack and modified with custom spacing and font settings.

struct MyVStack: View {
    var body: some View {
        VStack(spacing: 10) {
            Text("Text Item 1")
            Text("Text Item 2")
            Text("Text Item 3")
        }
        .font(.largeTitle)
    }
}

To include an instance of MyVStack in a declaration, it would be referenced as follows:

MyVStack()

Suppose, however, that a VStack with a spacing of 10 and a large font modifier is something that is needed frequently within a project, but in each case, different child views are required to be contained within the stack. While this flexibility isn’t possible using subviews, it can be achieved using the SwiftUI ViewBuilder closure attribute when constructing custom container views.

A ViewBuilder takes the form of a Swift closure which can be used to create a custom view comprised of multiple child views, the content of which does not need to be declared until the view is used within a layout declaration. The ViewBuilder closure takes the content views and returns them as a single view which is, in effect, a dynamically built subview.

The following is an example of using the ViewBuilder attribute to implement our custom MyVStack view:

struct MyVStack<Content: View>: View {
  let content: () -> Content
  init(@ViewBuilder content: @escaping () -> Content) {
    self.content = content
  }
 
  var body: some View {
    VStack(spacing: 10) {
      content()
   }
   .font(.largeTitle)
  }
}

Note that this declaration still returns an instance that complies with the View protocol and that the body contains the VStack declaration from the previous subview. Instead of including static views to be included in the stack, however, the child views of the stack will be passed to the initializer, handled by ViewBuilder and embedded into the VStack as child views. The custom MyVStack view can now be initialized with different child views wherever it is used in a layout, for example:

MyVStack {
    Text("Text 1")
    Text("Text 2")
    HStack {
        Image(systemName: "star.fill")
        Image(systemName: "star.fill")
        Image(systemName: "star")
    }
}

Working with the Label View

The Label view is different from most other SwiftUI views in that it comprises two elements in the form of an icon and text positioned side-by-side. The image can take the form of any image asset, a SwiftUI Shape rendering or an SF Symbol.

SF Symbols is a collection of over 1500 scalable vector drawings available for use when developing apps for Apple platforms and designed to complement Apple’s San Francisco system font.

The full set of symbols can be searched and browsed by installing the SF Symbols macOS app available from the following URL:

https://developer.apple.com/design/downloads/SF-Symbols.dmg

The following is an example of the Label view using an SF Symbol together with a font() modifier to increase the size of the icon and text:

Label("Welcome to SwiftUI", systemImage: "person.circle.fill")
    .font(.largeTitle)

The above view will be rendered as shown in Figure 20-6 below:

Figure 20-6

By referencing systemImage: in the Label view declaration we are indicating that the icon is to be taken from the built-in SF Symbol collection. To display an image from the app’s asset catalog, the following syntax would be used instead:

Label("Welcome to SwiftUI", image: "myimage")

Instead of specifying a text string and an image, the Label may also be declared using separate views for the title and icon. The following Label view declaration, for example, uses a Text view for the title and a Circle drawing for the icon:

Label(
    title: {
        Text("Welcome to SwiftUI")
        .font(.largeTitle)
    },
    icon: { Circle()
        .fill(Color.blue)
        .frame(width: 25, height: 25)
    }
)

When rendered, the above Label view will appear as shown in Figure 20-7:

Figure 20-7

Summary

SwiftUI user interfaces are declared in SwiftUI View files and are composed of components that conform to the View protocol. To conform with the View protocol a structure must contain a property named body which is itself a View.

SwiftUI provides a library of built-in components for use when designing user interface layouts. The appearance and behavior of a view can be configured by applying modifiers, and views can be modified and grouped together to create custom views and subviews. Similarly, custom container views can be created using the ViewBuilder closure property.

When a modifier is applied to a view, a new modified view is returned and subsequent modifiers are then applied to this modified view. This can have significant implications for the order in which modifiers are applied to a view.

The Anatomy of a Basic SwiftUI Project

When a new SwiftUI project is created in Xcode using the Multiplatform App template, Xcode generates a number of different files and folders which form the basis of the project, and on which the finished app will eventually be built.

Although it is not necessary to know in detail about the purpose of each of these files when beginning with SwiftUI development, each of them will become useful as you progress to developing more complex applications. This chapter will provide a brief overview of each element of a basic Xcode project structure.

Creating an Example Project

If you have not already done so, it may be useful to create a sample project to review while working through this chapter. To do so, launch Xcode and, on the welcome screen, select the option to create a new project. On the resulting template selection panel, select the Multiplatform tab followed by the App option before proceeding to the next screen:

Figure 19-1

On the project options screen, name the project DemoProject. Click Next to proceed to the final screen, choose a suitable filesystem location for the project and click on the Create button.

Project Folders

SwiftUI is intended to allow apps to be developed which can, with minimal modification, run on a variety of Apple platforms including iOS, iPadOS, watchOS, tvOS and macOS. In a typical multiplatform project, there will be a mixture of code that is shared by all platforms and code which is specific to an operating system. In recognition of this, Xcode structures the project with a folder for the shared code and files together with folders to hold the code and files specific to macOS as shown in Figure 19-2. Additional folders may be added in which to place iPadOS, watchOS and tvOS specific code if needed:

Figure 19-2

The DemoProjectApp.swift File

The DemoProjectApp.swift file contains the declaration for the App object as described in the chapter entitled SwiftUI Architecture and will read as follows:

import SwiftUI
 
@main
struct DemoProjectApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

As implemented, the declaration returns a Scene consisting of a WindowGroup containing the View defined in the ContentView.swift file. Note that the declaration is prefixed with @main. This indicates to SwiftUI that this is the entry point for the app when it is launched on a device.

The ContentView.swift File

This is a SwiftUI View file that, by default, contains the content of the first screen to appear when the app starts. This file and others like it are where most of the work is performed when developing apps in SwiftUI. By default, it contains a single Text view displaying the words “Hello, world!”:

import SwiftUI
 
struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}
 
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Assets.xcassets

The Assets.xcassets folder contains the asset catalog that is used to store resources used by the app such as images, icons and colors.

Summary

When a new SwiftUI project is created in Xcode using the Multiplatform App template, Xcode automatically generates a number of files required for the app to function. All of these files and folders can be modified to add functionality to the app, both in terms of adding resource assets, performing initialization and de-initialization tasks and building the user interface and logic of the app. Folders are used to provide a separation between code that is common to all operating systems and platform specific code.

131

SwiftUI Architecture

A completed SwiftUI app is constructed from multiple components which are assembled in a hierarchical manner. Before embarking on the creation of even the most basic of SwiftUI projects, it is useful to first gain an understanding of how SwiftUI apps are structured. With this goal in mind, this chapter will introduce the key elements of SwiftUI app architecture, with an emphasis on App, Scene and View elements.

SwiftUI App Hierarchy

When considering the structure of a SwiftUI application, it helps to view a typical hierarchy visually. Figure 18-1, for example, illustrates the hierarchy of a simple SwiftUI app:

Figure 18-1

Before continuing, it is important to distinguish the difference between the term “app” and the “App” element outlined in the above figure. The software applications that we install and run on our mobile devices have come to be referred to as “apps”. In this chapter reference will be made both to these apps and the App element in the above figure. To avoid confusion, we will use the term “application” to refer to the completed, installed and running app, while referring to the App element as “App”. The remainder of the book will revert to using the more common “app” when talking about applications.

App

The App object is the top-level element within the structure of a SwiftUI application and is responsible for handling the launching and lifecycle of each running instance of the application.

The App element is also responsible for managing the various Scenes that make up the user interface of the application. An application will include only one App instance.

Scenes

Each SwiftUI application will contain one or more scenes. A scene represents a section or region of the application’s user interface. On iOS and watchOS a scene will typically take the form of a window which takes up the entire device screen. SwiftUI applications running on macOS and iPadOS, on the other hand, will likely be comprised of multiple scenes. Different scenes might, for example, contain context specific layouts to be displayed when tabs are selected by the user within a dialog, or to design applications that consist of multiple windows.

SwiftUI includes some pre-built primitive scene types that can be used when designing applications, the most common of which being WindowGroup and DocumentGroup. It is also possible to group scenes together to create your own custom scenes.

Views

Views are the basic building blocks that make up the visual elements of the user interface such as buttons, labels and text fields. Each scene will contain a hierarchy of the views that make up a section of the application’s user interface. Views can either be individual visual elements such as text views or buttons, or take the form of containers that manage other views. The Vertical Stack view, for example, is designed to display child views in a vertical layout. In addition to the Views provided with SwiftUI, you will also create custom views when developing SwiftUI applications. These custom views will comprise groups of other views together with customizations to the appearance and behavior of those views to meet the requirements of the application’s user interface.

Figure 18-2, for example, illustrates a scene containing a simple view hierarchy consisting of a Vertical Stack containing a Button and TextView combination:

Figure 18-2

Summary

SwiftUI applications are constructed hierarchically. At the top of the hierarchy is the App instance which is responsible for the launching and lifecycle of the application. One or more child Scene instances contain hierarchies of the View instances that make up the user interface of the application. These scenes can either be derived from one of the SwiftUI primitive Scene types such as WindowGroup, or custom built.

On iOS or watchOS, an application will typically contain a single scene which takes the form of a window occupying the entire display. On a macOS or iPadOS system, however, an application may comprise multiple scene instances, often represented by separate windows which can be displayed simultaneously or grouped together in a tabbed interface.

Using Xcode in SwiftUI Mode

When creating a new project, Xcode now provides a choice of creating either a Storyboard or SwiftUI-based user interface for the project. When creating a SwiftUI project, Xcode appears and behaves significantly differently when designing the user interface for an app project compared to the UIKit Storyboard mode.

When working in SwiftUI mode, most of your time as an app developer will be spent in the code editor and preview canvas, both of which will be explored in detail in this chapter.

Starting Xcode 13

As with all the examples in this book, the development of our example will take place within the Xcode 13.2 development environment. If you have not already installed this tool together with the latest iOS SDK refer first to the “Installing Xcode 13 and the iOS 15 SDK” chapter of this book. Assuming the installation is complete, launch Xcode either by clicking on the icon on the dock (assuming you created one) or use the macOS Finder to locate Xcode in the Applications folder of your system.

When launched for the first time, and until you turn off the Show this window when Xcode launches toggle, the screen illustrated in Figure 17-1 will appear by default:

Figure 17-1

If you do not see this window, simply select the Window -> Welcome to Xcode menu option to display it. From within this window, click on the option to Create a new Xcode project.

Creating a SwiftUI Project

When creating a new project, the project template screen includes options to select how the app project is to be implemented. Options are available to design an app for a specific Apple platform (such as iOS, watchOS, macOS, DriveKit, or tvOS), or to create a multiplatform project. Selecting a platform-specific option will also provide the choice of creating either a Storyboard (UIKit) or SwiftUI-based project.

A multiplatform project allows an app to be designed for multiple Apple platforms with the minimum of platform-specific code. Even if you plan to initially only target iOS the multiplatform option is still recommended since it provides the flexibility to make the app available on other platforms in the future without having to restructure the project.

Templates are also available for creating a basic app, a document-based app or a game project. For the purposes of this chapter, use the multiplatform app option:

Figure 17-2

Clicking Next will display the project options screen where the project name needs to be entered (in this case, name the project DemoProject).

The Organization Identifier is typically the reversed URL of your company’s website, for example “com. mycompany”. This will be used when creating provisioning profiles and certificates to enable testing of advanced features of iOS on physical devices. It also serves to uniquely identify the app within the Apple App Store when the app is published:

Figure 17-3

Click Next once again and choose a location on your filesystem in which to place the project before clicking on the Create button.

Once a new project has been created, the main Xcode panel will appear with the default layout for SwiftUI development displayed.

Xcode in SwiftUI Mode

Before beginning work on a SwiftUI user interface, it is worth taking some time to gain familiarity with how Xcode works in SwiftUI mode. A newly created multiplatform “app” project includes two SwiftUI View files named <app name>App.swift (in this case DemoProjectApp.swift) and ContentView.swift which, when selected from the project navigation panel, will appear within Xcode as shown in Figure 17-4 below:

Figure 17-4

Located to the right of the project navigator (A) is the code editor (B). To the right of this is the preview canvas (C) where any changes made to the SwiftUI layout declaration will appear in real-time.

Selecting a view in the canvas will automatically select and highlight the corresponding entry in the code editor, and vice versa. Attributes for the currently selected item will appear in the attributes inspector panel (D).

During debugging, the debug panel (E) will appear containing debug output from both the iOS frameworks and any diagnostic print statements you have included in your code. If the console is not currently visible, display it by clicking on the button indicated by the arrow in Figure 17-5:

Figure 17-5

The debug panel can be configured to show a variable view, a console view, or both views in a split panel arrangement. The variable view displays variables within the app at the point that the app crashes or reaches a debugging break point. The console view, on the other hand, displays print output and messages from the running app. Figure 17-6 shows both views displayed together with an arrow indicating the buttons used to hide and show the different views:

Figure 17-6

The button indicated in Figure 17-5 above may be used to hide the debug panel (E), while the two buttons highlighted in Figure 17-7 below hide and show the project navigator and inspector panels:

Figure 17-7

The tab bar (marked A above) displays a tab for each currently open file. When a tab is clicked, the corresponding file is loaded into the editor. Hovering the mouse pointer over a tab will display an “X” button within the tab which will close the file when clicked.

The area marked F in Figure 17-4 is called the minimap. This map provides a miniaturized outline of the source code in the editor. Particularly useful when working with large source files, the minimap panel provides a quick way to move to different areas of the code. Hovering the mouse pointer of a line in the minimap will display a label indicating the class, property or function at that location as illustrated in Figure 17-9:

Figure 17-8

Clicking either on the label or within the map will take you to that line in the code editor. Holding down the Command key while hovering will display all of the elements contained within the source file as shown in Figure 17-9:

Figure 17-9

The minimap can be displayed and hidden by toggling the Editor -> Minimap menu option.

The Preview Canvas

The preview canvas provides both a visual representation of the user interface design and a tool for adding and modifying views within the layout design. The canvas may also be used to perform live testing of the running app without the need to launch an iOS simulator. Figure 17-10 illustrates a typical preview canvas for a newly created project:

Figure 17-10

If the canvas is not visible it can be displayed using the Xcode Editor -> Canvas menu option.

The main canvas area (A) represents the current view as it will appear when running on a physical device. When changes are made to the code in the editor, those changes are reflected within the preview canvas. To avoid continually updating the canvas, and depending on the nature of the changes being made, the preview will occasionally pause live updates. When this happens, the Resume button will appear in the top right-hand corner of the preview panel which, when clicked, will once again begin updating the preview:

Figure 17-11

The size buttons (B) can be used to zoom in and out of the canvas.

Preview Pinning

When building an app in Xcode it is likely that it will consist of several SwiftUI View files in addition to the default ContentView.swift file. When a SwiftUI View file is selected from the project navigator, both the code editor and preview canvas will change to reflect the currently selected file. Sometimes you may want the user interface layout for one SwiftUI file to appear in the preview canvas while editing the code in a different file. This can be particularly useful if the layout from one file is dependent on or embedded in another view. The pin button (labeled C in Figure 17-10 above) pins the current preview to the canvas so that it remains visible on the canvas after navigating to a different view. The view to which you have navigated will appear beneath the pinned view in the canvas and can be scrolled into view.

The Preview Toolbar

The preview toolbar (marked D in Figure 17-10 above and shown below) provides a range of options for changing the preview panel:

Figure 17-12

By default, the preview displays a static representation of the user interface. To test the user interface in a running version of the app, simply click on the Live Preview button (A). Xcode will then build the app and run it within the preview canvas where you can interact with it as you would in a simulator or on a physical device. When in Live Preview mode, the button changes to a stop button which can be used to exit live mode.

The current version of the app may also be previewed on an attached physical device by clicking on the Preview on Device button (B). As with the preview canvas, the running app on the device will update dynamically as changes are made to the code in the editor. Click the button marked C to rotate the preview between portrait and landscape modes.

The Inspect Preview button (D) displays the panel shown in Figure 17-13 below allowing properties of the canvas to be changed such as the device type, color scheme (light or dark mode) and dynamic text size.

Figure 17-13

The Duplicate Preview button (E) allows multiple preview canvases to be displayed simultaneously (a topic that will be covered later in this chapter).

Modifying the Design

Working with SwiftUI primarily involves adding additional views, customizing those views using modifiers, adding logic and interacting with state and other data instance bindings. All of these tasks can be performed exclusively by modifying the structure in the code editor. The font used to display the “Hello, world!” Text view, for example, can be changed by adding the appropriate modifier in the editor:

Text("Hello, world!")
    .padding()
    .font(.largeTitle)

An alternative to this is to make changes to the SwiftUI views by dragging and dropping items from the Library panel. The Library panel is displayed by clicking on the toolbar button highlighted in Figure 17-14:

Figure 17-14

When displayed, the Library panel will appear as shown in Figure 17-15:

Figure 17-15

When launched in this way, the Library panel is transient and will disappear either after a selection has been made, or a click is performed outside of the panel. To keep the panel displayed, hold down the Option key when clicking on the Library button.

When first opened, the panel displays a list of views available for inclusion in the user interface design. The list can be browsed, or the search bar used to narrow the list to specific views. The toolbar (highlighted in the above figure) can be used to switch to other categories such as modifiers, commonly used code snippets, images and color resources.

An item within the library can be applied to the user interface design in a number of ways. To apply a font modifier to the “Hello, world!” Text view, one option is to select the view in either the code or preview canvas, locate the font modifier in the Library panel, and double-click on it. Xcode will then automatically apply the font modifier.

Another option is to locate the Library item and then drag and drop it onto the desired location either in the code editor or the preview canvas. In Figure 17-16 below, for example, the font modifier is being dragged to the Text view within the editor:

Figure 17-16

The same result can be achieved by dragging an item from the library onto the preview canvas. In the case of Figure 17-17, a Button view is being added to the layout beneath the existing Text view:

Figure 17-17

In this example, the side along which the view will be placed if released highlights and the preview canvas displays a notification that the Button and existing Text view will automatically be placed in a Vertical Stack container view (stacks will be covered later in the chapter entitled SwiftUI Stacks and Frames).

Once a view or modifier has been added to the SwiftUI view file it is highly likely that some customization will be necessary, such as specifying the color for a foreground modifier. One option is, of course, to simply make the changes within the editor, for example:

Text("Hello, world!")
    .padding()
    .font(.largeTitle)
    .foregroundColor(.red)

Another option is to select the view in either the editor or preview panel and then make the necessary changes within the Attributes inspector panel:

Figure 17-18

The Attributes inspector will provide the option to make changes to any modifiers already applied to the selected view.

Before moving on to the next topic, it is also worth noting that the Attributes inspector provides yet another way to add modifiers to a view via the Add Modifier menu located at the bottom of the panel. When clicked, this menu will display a long list of modifiers available for the current view type. To apply a modifier, simply select it from the menu. An entry for the new modifier will subsequently appear in the inspector where it can be configured with the required properties.

Editor Context Menu

Holding down the Command key while clicking on an item in the code editor will display the menu shown in Figure 17-19:

Figure 17-19

This menu provides a list of options that will vary depending on the type of item selected. Options typically include a shortcut to a popup version of the Attributes inspector for the current view, together with options to embed the current view in a stack or list container. This menu is also useful for extracting part of a view into its own self-contained subview. Creating subviews is strongly encouraged to promote reuse, improve performance and unclutter complex design structures.

Previewing on Multiple Device Configurations

Every newly created SwiftUI View file includes an additional declaration at the bottom of the file that resembles the following:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

This structure, which conforms to the PreviewProvider protocol, returns an instance of the primary view within the file. This instructs Xcode to display the preview for that view within the preview canvas (without this declaration, nothing will appear in the canvas).

By default, the preview canvas shows the user interface on a single device based on the current selection in the run target menu to the right of the run and stop buttons in the Xcode toolbar (as highlighted in Figure 1721 below). To preview on other device models, one option is to simply change the run target and wait for the preview canvas to change.

A better option, however, is to modify the previews structure to specify a different device. In the following example, the canvas previews the user interface on an iPhone SE:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewDevice("iPhone SE (2nd generation)")
            .previewDisplayName("iPhone SE")
    }
}

In fact, it is possible using this technique to preview multiple device types simultaneously by placing them into a Group view as follows:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        
        Group {
            ContentView()
                .previewDevice("iPhone 11")
                .previewDisplayName("iPhone 11")
            ContentView()
                .previewDevice("iPhone SE (2nd generation)")
                .previewDisplayName("iPhone SE")
        }
    }
}

When multiple devices are previewed, they appear in a scrollable list within the preview canvas as shown in Figure 17-20:

Figure 17-20

The environment modifier may also be used to preview the layout in other configurations, for example, to preview in dark mode:

ContentView()
    .preferredColorScheme(.dark)
    .previewDevice("iPhone SE (2nd generation)")

This preview structure is also useful for passing sample data into the enclosing view for testing purposes within the preview canvas, a technique that will be used in later chapters. For example:

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(sampleData: mySampleData)                 
    }
}

An alternative to manually editing the PreviewProvider declaration is to simply duplicate the current preview instance using the Duplicate Preview button marked D in Figure 17-12 above. Once the new preview appears, it will have its own preview toolbar within which the Preview Inspect button (C) may be used to configure the properties of the preview. All of these changes will automatically be reflected in the PreviewProvider declaration within the view file.

Running the App on a Simulator

Although much can be achieved using the preview canvas, there is no substitute for running the app on physical devices and simulators during testing.

Within the main Xcode project window, the menu marked C in Figure 17-21 is used to choose a target simulator. This menu will include simulators that have been configured and any physical devices connected to the development system:

Figure 17-21

When a project is first created, it may initially be configured to target macOS instead of iOS as shown in Figure 17-22:

Figure 17-22

To switch to iOS, click on the area marked by the arrow above and select the iOS option from the resulting menu together with a device or simulator as illustrated below:

Figure 17-23

Clicking on the Run toolbar button (marked B in Figure 17-21 above) will compile the code and run the app on the selected target. The small panel in the center of the Xcode toolbar (D) will report the progress of the build process together with any problems or errors that cause the build process to fail. Once the app is built, the simulator will start and the app will run. Clicking on the stop button (A) will terminate the running app.

The simulator includes a number of options not available in the Live Preview for testing different aspects of the app. The Hardware and Debug menus, for example, include options for rotating the simulator through portrait and landscape orientations, testing Face ID authentication and simulating geographical location changes for navigation and map-based apps.

Running the App on a Physical iOS Device

Although the Simulator environment provides a useful way to test an app on a variety of different iOS device models, it is important to also test on a physical iOS device.

If you have entered your Apple ID in the Xcode preferences screen as outlined in the “Joining the Apple Developer Program” chapter and selected a development team for the project, it is possible to run the app on a physical device simply by connecting it to the development Mac system with a USB cable and selecting it as the run target within Xcode.

With a device connected to the development system, and an application ready for testing, refer to the device menu located in the Xcode toolbar. There is a reasonable chance that this will have defaulted to one of the iOS Simulator configurations. Switch to the physical device by selecting this menu and changing it to the device name listed under the iOS Devices section as shown in Figure 17-24:

Figure 17-24

With the target device selected, make sure the device is unlocked and click on the run button at which point Xcode will install and launch the app on the device.

As will be discussed later in this chapter, a physical device may also be configured for network testing, whereby apps are installed and tested on the device via a network connection without the need to have the device connected by a USB cable.

Managing Devices and Simulators

Currently connected iOS devices and the simulators configured for use with Xcode can be viewed and managed using the Xcode Devices window which is accessed via the Window -> Devices and Simulators menu option. Figure 17-25, for example, shows a typical Device screen on a system where an iPhone has been detected:

Figure 17-25

A wide range of simulator configurations are set up within Xcode by default and can be viewed by selecting the Simulators button at the top of the left-hand panel. Other simulator configurations can be added by clicking on the + button located in the bottom left-hand corner of the window. Once selected, a dialog will appear allowing the simulator to be configured in terms of device model, iOS version and name.

Enabling Network Testing

In addition to testing an app on a physical device connected to the development system via a USB cable, Xcode also supports testing via a network connection. This option is enabled on a per device basis within the Devices and Simulators dialog introduced in the previous section. With the device connected via the USB cable, display this dialog, select the device from the list and enable the Connect via network option as highlighted in Figure 17-26:

Figure 17-26

Once the setting has been enabled, the device may continue to be used as the run target for the app even when the USB cable is disconnected. The only requirement being that both the device and development computer be connected to the same Wi-Fi network. Assuming this requirement has been met, clicking on the run button with the device selected in the run menu will install and launch the app over the network connection.

Dealing with Build Errors

If for any reason a build fails, the status window in the Xcode toolbar will report that an error has been detected by displaying “Build” together with the number of errors detected and any warnings. In addition, the left-hand panel of the Xcode window will update with a list of the errors. Selecting an error from this list will take you to the location in the code where corrective action needs to be taken.

Monitoring Application Performance

Another useful feature of Xcode is the ability to monitor the performance of an application while it is running, either on a device or simulator or within the Live Preview canvas. This information is accessed by displaying the Debug Navigator.

When Xcode is launched, the project navigator is displayed in the left-hand panel by default. Along the top of this panel is a bar with a range of other options. The seventh option from the left displays the debug navigator when selected as illustrated in Figure 17-27. When displayed, this panel shows a number of real-time statistics relating to the performance of the currently running application such as memory, CPU usage, disk access, energy efficiency, network activity and iCloud storage access.

Figure 17-27

When one of these categories is selected, the main panel (Figure 17-28) updates to provide additional information about that particular aspect of the application’s performance:

Figure 17-28

Yet more information can be obtained by clicking on the Profile in Instruments button in the top right-hand corner of the panel.

Exploring the User Interface Layout Hierarchy

Xcode also provides an option to break the user interface layout out into a rotatable 3D view that shows how the view hierarchy for a user interface is constructed. This can be particularly useful for identifying situations where one view instance is obscured by another appearing on top of it or a layout is not appearing as intended. This is also useful for learning how SwiftUI works behind the scenes to construct a SwiftUI layout, if only to appreciate how much work SwiftUI is saving us from having to do.

To access the view hierarchy in this mode, the app needs to be running on a device or simulator. Once the app is running, click on the Debug View Hierarchy button indicated in Figure 17-29:

Figure 17-29

Once activated, a 3D “exploded” view of the layout will appear. Clicking and dragging within the view will rotate the hierarchy allowing the layers of views that make up the user interface to be inspected:

Figure 17-30

Moving the slider in the bottom left-hand corner of the panel will adjust the spacing between the different views in the hierarchy. The two markers in the right-hand slider (Figure 17-31) may also be used to narrow the range of views visible in the rendering. This can be useful, for example, to focus on a subset of views located in the middle of the hierarchy tree:

Figure 17-31

While the hierarchy is being debugged, the left-hand panel will display the entire view hierarchy tree for the layout as shown in Figure 17-32 below:

Figure 17-32

Selecting an object in the hierarchy tree will highlight the corresponding item in the 3D rendering and vice versa. The far right-hand panel will also display the attributes of the selected object. Figure 17-33, for example, shows the inspector panel while a Text view is selected within the view hierarchy.

Figure 17-33

Summary

When creating a new project, Xcode provides the option to use either UIKit Storyboards or SwiftUI as the basis of the user interface of the app. When in SwiftUI mode, most of the work involved in developing an app takes place in the code editor and the preview canvas. New views can be added to the user interface layout and configured either by typing into the code editor or dragging and dropping components from the Library either onto the editor or the preview canvas.

The preview canvas will usually update in real-time to reflect code changes as they are typed into the code editor, though will frequently pause updates in response to larger changes. When in the paused state, clicking the Resume button will restart updates. The Attribute inspector allows the properties of a selected view to be changed and new modifiers added. Holding down the Command key while clicking on a view in the editor or canvas displays the context menu containing a range of options such as embedding the view in a container or extracting the selection to a subview.

The preview structure at the end of the SwiftUI View file allows previewing to be performed on multiple device models simultaneously and with different environment settings.

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.

Introduction to SwiftUI Essentials

The goal of this book is to teach the skills necessary to build iOS 15 applications using SwiftUI, Xcode 13, and the Swift 5.5 programming language.

Beginning with the basics, this book provides an outline of the steps necessary to set up an iOS development environment together with an introduction to the use of Swift Playgrounds to learn and experiment with Swift.

The book also includes in-depth chapters introducing the Swift 5.5 programming language including data types, control flow, functions, object-oriented programming, property wrappers, structured concurrency, and error handling.

An introduction to the key concepts of SwiftUI and project architecture is followed by a guided tour of Xcode in SwiftUI development mode. The book also covers the creation of custom SwiftUI views and explains how these views are combined to create user interface layouts including the use of stacks, frames, and forms.

Other topics covered include data handling using state properties in addition to observable, state, and environment objects, as are the key user interface design concepts such as modifiers, lists, tabbed views, context menus, user interface navigation, and outline groups.

The book also includes chapters covering graphics drawing, user interface animation, view transitions and gesture handling, WidgetKit, document-based apps, Core Data, CloudKit, and SiriKit integration.

Chapters are also provided explaining how to integrate SwiftUI views into existing UIKit-based projects and explains the integration of UIKit code into SwiftUI.

Finally, the book explains how to package up a completed app and upload it to the App Store for publication.

Along the way, the topics covered in the book are put into practice through detailed tutorials, the source code for which is also available for download.

The aim of this book, therefore, is to teach you the skills necessary to build your own apps for iOS 15 using SwiftUI. Assuming you are ready to download the iOS 15 SDK and Xcode 13 and have an Apple Mac system you are ready to get started.

For Swift Programmers

This book has been designed to address the needs of both existing Swift programmers and those who are new to both Swift and iOS app development. If you are familiar with the Swift 5.5 programming language, you can probably skip the Swift-specific chapters. If you are not yet familiar with the SwiftUI-specific language features of Swift, however, we recommend that you at least read the sections covering implicit returns from single expressions, opaque return types, and property wrappers. These features are central to the implementation and understanding of SwiftUI.

For Non-Swift Programmers

If you are new to programming in Swift then the entire book is appropriate for you. Just start at the beginning and keep going.

Source Code Download

The source code and Xcode project files for the examples contained in this book are available for download at:

https://www.ebookfrenzy.com/web/swiftui-ios15/

Errata

While we make every effort to ensure the accuracy of the content of this book, it is inevitable that a book covering a subject area of this size and complexity may include some errors and oversights. Any known issues with the book will be outlined, together with solutions at the following URL:

https://www.ebookfrenzy.com/errata/swiftui-ios15.html

In the event that you find an error not listed in the errata, please let us know by emailing our technical support team at [email protected].

SwiftUI Essentials – iOS 15 Edition

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