Getting started with SwiftUI in iOS iOS 14.05.2021

Introduction

SwiftUI, introduced during Apple's 2019 Worldwide Developer Conference (WWDC) 2019, provides an innovative and simple way to build user interfaces across all Apple platforms.

SwiftUI does not use UIKit concepts such as Auto Layout. It has a completely new layout system designed to make it easy to write applications that work across Apple platforms.

SwiftUI uses three basic layout components – VStack, HStack, and ZStack. VStack is a view that arranges its children in a vertical line, HStack arranges its children in a horizontal line, and ZStack arranges its children by aligning them with the vertical and horizontal axes.

struct ContentView: View {
    var body: some View {
        VStack {
            Text("5").padding(.all, 5).background(Color.green).mask(Circle())
            Text("Item 2")
            Text("Item 3")
        }.background(Color.blue)
    }
}

or

struct ContentView: View {
    var body: some View {
        HStack{
            Text("Item 1")
            Text("Item 2")
            Divider().background(Color.black)
            Spacer()
            Text("Item 3")
        }.background(Color.red)
    }
}

Items such as background(Color.black), .padding() are called ViewModifiers, or just modifiers. Modifiers can be applied to views or other modifiers, producing a different version of the original value.

Text

The Text is a view that displays one or more lines of read-only text. Text has a number of standard modifiers to format text.

struct ContentView: View {
    @State var password = ""
    @State var someText = ""

    var body: some View {
        VStack{
            Text("Hello World").fontWeight(.medium)
            SecureField("Enter a password", text: $password).padding()
            Text("Password entered: \(password)").italic()
            TextField("Enter some text", text: $someText).padding()
            Text("\(someText)").font(.largeTitle).underline()

            Text("Use kerning to change space between lines of text").kerning(7)

            Text("This is a multiline text implemented in swiftUI. The trailing modifier was added to the text. This text also implements multiple modifiers")
                .background(Color.yellow)
                .multilineTextAlignment(.trailing)
                .lineSpacing(10)
        }
    }
}

Unlike regular text views, TextFields and SecureFields require state variables to store the value entered by the user. State variables are declared using the keyword @State. Values entered by the user are stored using the process of binding, where the state variable is bound to the SecureField or TextField input parameter. The $ symbol is used to bind a state variable to the field. Using the $ symbol ensures that the state variable's value is changed to correspond to the value entered by the user.

Binding also notifies other views of state changes and causes the views to be redrawn on state change.

struct ContentView: View {
    @State var name: String = ""

    var body: some View {
        VStack{
            TextField("Type your name...", text: $name)
                .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
                .background(Color.white)
                .overlay(
                    RoundedRectangle(cornerRadius: 8)
                        .stroke(lineWidth: 2)
                        .foregroundColor(.blue)
                )
                .shadow(color: Color.gray.opacity(0.4),
                        radius: 3, x: 1, y: 2)
        }
    }
}

TextField has two pairs of initializers, with each pair having a localized and non - localized version for the title parameter. These parameters are two closures that can be used to perform additional processing before and after the user input:

  • onEditingChanged. Called when the edit obtains focus (when the Boolean parameter is true) or loses focus (when the parameter is false).
  • onCommit. Called when the user performs a commit action, such as pressing the return key. This is useful when you want to move the focus to the next field automatically.

Read more how to customize TextField.

Image

In this part, we will learn how to add an image to a view, use an already existing UIImage, put an image in a frame, and use modifiers to present beautiful images.

Add an Image view to VStack

Image("img1")

Add a .resizable() modifier to the image and allow SwiftUI to adjust the image such that it fits the screen space available:

Image("img1").resizable()

The .resizable() modifier causes the full image to fit on the screen, but the proportions are distorted. That can be fixed by adding the .aspectRatio(contentMode: .fit) modifier:

Image("img1").resizable().aspectRatio(contentMode: .fit)

Add another image to VStack

Image("img2")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .frame(width:300, height:200)
    .clipShape(Circle())
    .overlay(Circle().stroke(Color.blue, lineWidth: 6))
    .shadow(radius: 10)

Button

Button in SwiftUI is a control that performs an action when triggered.

Button(action: {
    print("Button clicked")
}) {
    Image(systemName: "square")
}

Buttons can have more complex formatted labels.

Button(action: {
    print("Multiple Labels")
}, label: {
    VStack{
        Text("Title").font(.title)
        Text("Sub title").font(.subheadline)
    }
})

ScrollView

SwiftUI scroll views are used to easily create scrolling containers. They automatically size themselves to the area where they are placed. Scroll views are vertical by default and can be made to scroll horizontally or vertically by passing in the .horizontal() or .vertical() modifiers as the first parameter to the scroll view.

We will add two scroll views to a VStack component: one horizontal and one vertical. Each scroll view will contain SF symbols for the letters A–E:

struct ContentView: View {
    let imageNames = [
        "a.circle.fill",
        "b.circle.fill",
        "c.circle.fill",
        "d.circle.fill",
        "e.circle.fill"
    ]
    var body: some View {
        VStack{
            ScrollView {
                    ForEach(self.imageNames, id: \.self){ name in
                        Image(systemName: name)
                            .font(.largeTitle)
                            .foregroundColor(Color.white)
                            .frame(width: 50, height: 50)
                            .background(Color.green)
                    }
            }
            .frame(width:50, height:200)

            ScrollView(.horizontal, showsIndicators: false) {
                HStack{
                    ForEach(self.imageNames, id: \.self){ name in
                        Image(systemName: name)
                            .font(.largeTitle)
                            .foregroundColor(Color.white)
                            .frame(width: 50, height: 50)
                            .background(Color.green)
                    }
                }
            }
        }
    }
}

List

List is a container that presents rows of data arranged in a single column. It is a UIKit's UITableView equivalent in SwiftUI.

struct ContentView: View {
    var body: some View {
        List {
            Text("Item 1")
            Text("Item 2")
            Text("Item 3")                        
            Text("Item 4")
        }
    }
}

The difference between List and ScrollView is that scroll views are used for smaller datasets. List do not load the whole dataset in memory at once and thus are more optimized at handling large data.

Let's create simple example with a separate view as a "cell".

struct MovieRow: View {
    var movie: MovieInfo
    var body: some View {
        HStack {
            Text(movie.title)    
            Text(movie.year)
        }
        .padding()
    }
}

struct MovieInfo: Identifiable {
    var id = UUID()
    var title: String
    var year: Int
}

struct ContentView: View {
    let items: [MovieInfo] = [
        MovieInfo(title: "The Shawshank Redemption", year: 1994),
        MovieInfo(title: "The Godfather", year: 1972)
    ]
    var body: some View {
        List {
            ForEach(self.items){ item in
                MovieRow(movie: item)
            }
        }
    }
}

Adding the id property to our struct allows us to later use it within a ForEach loop without providing id: .\self parameter.

SwiftUI's sections are used to separate items into groups. Following is a example of sections inside list view.

struct ContentView: View {
    var body: some View {
        NavigationView{
            List {
                Section(header: Text("Section 1")){
                    Text("Item 1-1")
                }
                Section(header: Text("Section 2")){
                    Text("Item 2-1")
                    Text("Item 2-2")
                }
            }
        .listStyle(GroupedListStyle())
            .navigationBarTitle("Items", displayMode: .inline)
        }
    }
}

Form

Forms may contain required and optional fields. Some fields may have additional requirements that need to be met. For example, the password fields may be required to contain at least eight characters. When developing an app, we may want to enable or disable the submit button based on the form requirements, thus allowing the users to submit a form only when it meets all our requirements.

struct ContentView: View {
    @State private var username = ""
    @State private var password = ""

    var body: some View {
        VStack{
            Text("My App")
                .fontWeight(.heavy)
                .foregroundColor(.blue)
                .font(.largeTitle)
                .padding(.bottom, 30)
            Image(systemName: "person.circle")
                .font(.system(size: 150))
                .foregroundColor(.gray)
                .padding(.bottom,40)
            Group {
                TextField("Username", text: $username)
                SecureField("Password", text: $password)
            }
            .padding()
            .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.black, lineWidth: 1))

            Button(action: {
                print("Submit clicked")
            }) {
                Text("Submit")
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(Color.white)
            .clipShape(Capsule())
            .disabled(username.isEmpty || password.isEmpty)
        }.padding()
    }
}

The .disable() modifier can be used to disable a button and prevent users from submitting a form until certain conditions have been met.”

Alerts and ActionSheet

A common way of letting the user know that something important happened is to present an alert with a message and an OK button.

Displaying alerts is a three-step process. First, we create an @State variable that triggers the displaying or hiding of the variable; then, we add an .alert() modifier to the view we are modifying; and finally, we add the Alert view inside the .alert() modifier.

struct ContentView: View {
    @State private var showSubmitAlert = false

    var body: some View {
        Button(action: {
            self.showSubmitAlert=true
        }) {
            Text("Submit")
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .clipShape(Capsule())
        }
        .alert(isPresented: $showSubmitAlert ) {
            Alert(title: Text("Confirm Action"),
            message: Text("Are you sure you want to submit the form"),
            dismissButton: .default(Text("OK")))
        }
    }
}

In some cases, we may want to present a Yes or No choice to the user.

struct ContentView: View {
    @State private var changeText = false
    @State private var displayText = "Tap to Change Text"

    var body: some View {
        Text(displayText)
        .onTapGesture {
            self.changeText = true
        }
        .alert(isPresented: $changeText) {
            Alert(title: Text("Changing Text"),
                message: Text("Do you want to change the displayed text"),
                primaryButton: .cancel(), //.cancel(Text("No"))
                secondaryButton: .default(Text("OK")) {
                    self.displayText = (self.displayText == "Stay Foolish") ? "Stay Hungry" : "Stay Foolish"
            })
        }
    }
}

ActionSheet provides the user with additional choices related to an action they are currently taking, whereas the Alert view informs the user if something unexpected happens or they are about to perform an irreversible action.

@State private var showSheet = false

struct ContentView: View {
    @State private var showSheet = false

    var body: some View {
        Text("Present Sheet")
            .onTapGesture {
                self.showSheet = true
            }
            .actionSheet(isPresented: $showSheet){
                ActionSheet(
                    title: Text("ActionSheet"),
                    message: Text("Description").font(.largeTitle),
                    buttons: [
                        .default(Text("Dismiss Sheet")),
                        .default(Text("Save")),
                        .destructive(Text("Cancel")),
                        .default(Text("Print to console")) {
                            print("Print button clicked")
                        }
                    ])
            }
    }
}

Navigation

SwiftUI navigation organizes around two styles: flat and hierarchical. In SwiftUI, you implement a flat hierarchy using a TabView. A flat navigational structure works best when the user needs to move between different views that divide content into categories.

Hierarchical navigation provides the user with fewer options at the top, and a deeper structure underneath. In SwiftUI, you implement hierarchical navigation using a NavigationView. Compared to a flat layout, a hierarchical layout has fewer lop level views, but each contains a deeper view stack beneath.

Here is the first screen with list

struct Screen1: View {
    @State private var animals = ["Cats", "Dogs", "Goats"]

    var body: some View {
        NavigationView{
            List{
                ForEach(animals, id: \.self){ animal in
                    Text(animal)
                }.onDelete(perform: removeAnimal)
            }
        }.navigationBarTitle(Text("Animals"), displayMode: .inline)
    }

    func removeAnimal(at offsets: IndexSet){
        animals.remove(atOffsets: offsets)
    }
} 

Here is the second screen with text

struct Screen2: View {
    var body: some View {
        NavigationView{
            Text("Screen 2").fontWeight(.medium)
        }.navigationBarTitle(Text("Text"), displayMode: .inline)
    }
} 

Open the ContentView.swift file and create a NavigationView to navigate between the SwiftUI views we added to the project:

struct ContentView: View {
    var body: some View {        
        NavigationView {
            VStack{
                NavigationLink(destination: Screen1()) {
                    Text("Screen 1")
                }

                NavigationLink(destination: Screen2()) {
                    Text("Screen 2").padding()
                }
            }.navigationBarTitle(Text("Main View"), displayMode: .inline)
        }
    }
}

A NavigationLink has to be placed in a NavigationView prior to being used. It takes two parameters – destination and label. The destination parameter represents the view that would be displayed when the label is clicked, while the label parameter represents the text to be displayed within NavigationLink.

Pickers

Here you will learn how to implement the pickers, namely, Picker, Toggle, Slider, Stepper, and DatePickers. Pickers are typically used to prompt the user to select from a set of mutually exclusive values; Toggles are used to switch between on/off states; and Sliders are used to select a value from a bounded linear range of values. Like Sliders, Steppers also provide the user interface for selecting from a range of values. However, Steppers use a + and – sign to allow the users to increment the desired value by a certain amount. Finally, DatePickers are used to select dates.

struct ContentView: View {    
    @State var choice = 0
    @State var showText = false
    @State var transitModes = ["Bike", "Car", "Bus"]
    @State var sliderVal: Float = 0
    @State var stepVal = 0
    @State var gameTime = Date()

    var body: some View {
        Form{
            Section(header:Text("Picker") {
                Picker(selection: $choice, label:Text("Transit Mode")) {
                    ForEach( 0 ..< transitModes.count) { index in
                        Text("\(self.transitModes[index])")
                    }
                }.pickerStyle(SegmentedPickerStyle())
                Text("Current choice: \(transitModes[choice])")
            }

            Section{
                Toggle(isOn: $showText){
                    Text("Show Text")
                }
                if showText {
                    Text("The Text toggle is on")
                }
            }

            Section{
                Slider(value: $sliderVal, in: 0...10, step: 0.001)
                Text("Slider current value \(sliderVal, specifier: "%.1f")")
            }

            Section {
                Stepper("Stepper", value: $stepVal, in: 0...5)
                Text("Stepper current value \(stepVal)")
            }

            Section {
                DatePicker("Please select a date", selection: $gameTime)
            }


            Section {
                DatePicker("Please select a date", selection: $gameTime, in: Date()...)
            }
        }
    }
}

Form views group controls used for data entry and Section creates hierarchical view content. Enclosing Section views, our Form creates the gray area between each picker.

How to apply groups of styles using ViewModifiers

SwiftUI comes with built-in modifiers such as background() and fontWeight(), among others. SwiftUI also gives programmers the ability to create their own custom modifiers to do something specific. Custom modifiers allow the programmer to combine multiple existing modifiers into a single modifier.

In the ContentView.swift file, create a struct that conforms to the ViewModifier protocol, accepts parameter of the Color type, and applies styles to the body:

struct BackgroundStyle: ViewModifier {
    var bgColor: Color
    func body(content: Content) -> some View{
        content
            .frame(width: UIScreen.main.bounds.width * 0.3)
            .foregroundColor(Color.black)
            .padding()
            .background(bgColor)
            .cornerRadius(CGFloat(20))
    }
}

Add the custom style to the text using the modifier() modifier:

Text("Perfect").modifier(BackgroundStyle(bgColor: .blue))

To apply styles without using the modifier() modifier, create an extension to the View protocol:

extension View {
    func backgroundStyle(color: Color) -> some View{
        self.modifier(BackgroundStyle(bgColor: color))
    }
}

Remove the modifier on the Text view and add your custom style using the backgroundStyle() modifier you just created:

Text("Perfect").backgroundStyle(color: Color.red)

Separating presentation from content with ViewBuilder

ViewBuilder is a custom parameter attribute that constructs views from closures. ViewBuilder can be used to create custom views that can be used across the application with minimal or no code duplication. We will create a SwiftUI view, BlueCircle.swift, for declaring the ViewBuilder and implement the custom ViewBuilder. The ContentView.swift file will be used to implement the custom view.

Add a new file to the project:

struct BlueCircle<Content: View>: View {
    let content: Content
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    var body: some View {
        HStack {
            content
            Spacer()
            Circle()
                .fill(Color.blue)
                .frame(width:20, height:30)
        }.padding()
    }
}

Open the ContentView.swift file and implement BlueCircle ViewModifier:

struct ContentView: View {
    var body: some View {
        VStack {
            BlueCircle {
                Text("some text here")
                Rectangle()
                    .fill(Color.red)
                    .frame(width: 40, height: 40)
            }
            BlueCircle {
                Text("Another example")
            }
        }
    }
}

ViewBuilder is a struct that implements the View protocol and therefore should have a body variable within it.

Animation

Let's look at two ways to accomplish animation:

  • Implicit animations
  • Explicit animations

Implicit animations are by far the easiest to add into SwiftUI as all you need to do is add an implicit animation to a View. This means all the aspects of that view will be animated regardless of any visible changes that have been made to their state. Animations will be performed as part of a wider change (such as a higher level binding change).

Let's make the button disappear and reappear when we click it. We'll achieve this by changing the opacity of the Text view (the alpha channel) using @State to an opacity variable.

struct ContentView: View {
    @State var opacity = 0.0
    var body: some View {
        Button("Tap to Animate") {
            self.opacity = (self.opacity == 1.0) ? 0 : 1.0
        }
        Text("Learn SwiftUI")
            .opacity(opacity)
            .animation(.default)
            //.animation(.easeIn(duration: 1))
    }
}

Next, let's take a look at explicit animations in SwiftUI as a way for us to prepare a specific animation prior to it taking place.

struct ContentView: View {
    @State var opacity = 0.0
    var body: some View {
        Button("Tap to Animate") {
            withAnimation {
                self.opacity = (self.opacity == 1.0) ? 0 : 1.0
            }
        }
        Text("Learn SwiftUI")
            .opacity(opacity)
    }
}

Drawing

SwiftUI contains a few basic shapes such as rectangles, circles, and so on that can be used to create more complex shapes by combining them.

SwiftUI has five different basic shapes:

  • Rectangle
  • RoundedRectangle
  • Capsule
  • Circle
  • Ellipse
struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            Rectangle()
                .stroke(Color.orange, lineWidth: 15)
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.red)
            Capsule(style: .continuous)
                .fill(Color.green)
                .frame(height: 100)
            Capsule(style: .circular)
                .fill(Color.yellow)
                .frame(height: 100)
            Circle()
                .strokeBorder(Color.blue, lineWidth: 15)
            Ellipse()
                .fill(Color.purple)

        }
        .padding([.horizontal], 20)
    }
}

Image with border

struct ContentView: View {
    var body: some View {
        Image(systemName: "a.circle.fill")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .clipShape(Circle())
            .shadow(radius: 10)
            .padding(.horizontal, 20)
            .overlay(
                Circle().strokeBorder(Color.red, style: StrokeStyle(lineWidth: 1))
            )
    }
}

Gradient

SwiftUI has several ways of rendering gradients. A gradient can be used to fill a shape, or even fill a border.

SwiftUI has three different types of gradients:

  • Linear gradients
  • Radial gradients
  • Angular gradients
extension Text {
    func bigLight() -> some View {
        self
            .font(.system(size: 80))
            .fontWeight(.thin)
            .foregroundColor(.white)
    }
}

struct LinearGradientView: View {
    var body: some View {
        ZStack {
            LinearGradient(
                gradient: Gradient(colors: [.orange, .green, .blue, .black]),
                startPoint: .topLeading,
                endPoint: .bottomTrailing)
            Text("Linear Gradient")
                .bigLight()
        }
    }
}

struct RadialGradientView: View {
    var body: some View {
        ZStack {
            RadialGradient(gradient: Gradient(colors: [.orange, .green, .blue, .black]),
                           center: .center,
                           startRadius: 20,
                           endRadius: 500)
            Text("Radial Gradient")
                .bigLight()
        }
    }
}

struct AngularGradientView: View {
    var body: some View {
        ZStack {
            AngularGradient(gradient: Gradient(
                                colors: [.orange, .green, .blue, .black,
                                         .black, .blue, .green, .orange]),
                            center: .center)
            Text("Angular Gradient")
                .bigLight()
        }
    }
}

struct ContentView: View {
    @State private var selectedGradient = 0

    var body: some View {
        ZStack(alignment: .top) {
            Group {
                if selectedGradient == 0 {
                    LinearGradientView()
                } else if selectedGradient == 1 {
                    RadialGradientView()
                } else {
                    AngularGradientView()
                }
            }.edgesIgnoringSafeArea(.all)

            Picker(selection: self.$selectedGradient,
                   label: Text("Select Gradient")) {
                Text("Linear").tag(0)
                Text("Radial").tag(1)
                Text("Angular").tag(2)
            }
            .pickerStyle(SegmentedPickerStyle())
            .padding(.horizontal, 32)
        }
    }
}