Skip to content

15 - iOS Swift UI

Swift UI

Better apps. Less code.

  • Build user interfaces for any Apple device using just one set of tools and APIs.
  • Automatic support for Dynamic Type, Dark Mode, localization, and accessibility.
  • Live preview
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import SwiftUI

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

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

MVC

In Xcode 15 (targeting iOS 17) we can generate preview with #Preview macro.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import SwiftUI

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

#Preview {
    ContentView()
}

Preview macro supports multiple previews (and titles for them). So you can set up different previews for your design.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import SwiftUI

struct ContentView: View {
    var greeting:String

    var body: some View {
        Text(greeting)
            .padding()
    }
}

#Preview("Preview simple") {
    ContentView(greeting: "View 1")
}

#Preview("Complex view") {
    ContentView(greeting: "View 2")
}

some (swift 5.1)

  • Some denotes opaque type (reverse of generics)
  • Opaque types preserve type identity, and protocol types don’t.
  • With a generic type, the caller of the function determines the concrete type of the placeholder (“outside”).
  • With opaque types, the implementation determines the concrete type (“inside”).
  • An opaque types always refers to one specific, concrete type – we just don’t know which one.
  • A protocol type can refer to many types, as long as they conform to the protocol.

Stacks

  • Body property of ContentView only describes a single view. To build up more complex views – embed views in stacks.
  • Stack combine and embed multiple views - group views together horizontally, vertically, or back-to-front.
  • Cmd-click on element to open structured editing popover.
  • Embed element in stack

MVC

UI Elements

  • Access library of possible elements from “plus” icon
  • Drag elements onto code or live preview
  • Controls, Layout, Other, Paints

MVC

Combining views 1

Add new file, User Interface section, "SwiftUI View". SubView.swift

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import SwiftUI

struct SubView: View {
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 50.0)
                .frame(width: nil, height: 38.0)
                .foregroundColor(.red)
            Text("Hello, from other view!")
                .foregroundColor(Color.yellow)
        }
    }
}

#Preview {
    SubView()
}

MVC

Combining views 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Text("Headline")
                    .font(.title)
            }
            HStack {
                Text("SwiftUI")
                    .font(.subheadline)
                Spacer()
                Text("Apple")
                    .font(.subheadline)
            }
            SubView()
        }
        .padding()

    }
}

#Preview {
    ContentView()
}

MVC

State in view

  • Define local state variables with @State
  • Modifying these causes view to invalidate and redraw needed parts
  • Live preview
  • Play icon
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import SwiftUI

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

    var body: some View {
        VStack {
            Text("\(counter)")
                .fontWeight(.bold)
                .font(.largeTitle)
                .padding()

            Button(action: {
                counter += 1
            }) {
                Text("INCREMENT")
                    .fontWeight(.bold)
                    .font(.title)
                    .padding()
                    .background(Color.purple)
                    .cornerRadius(40)
                    .foregroundColor(.white)
                    .padding(10)
                    .overlay(
                        RoundedRectangle(cornerRadius: 40)
                            .stroke(Color.purple, lineWidth: 5)
                    )
            }
        }
    }
}

#Preview {
    ContentView()
}

MVC

passing data to subView 1

In subview

  • var somename: SomeType
    • Constructor parameter, one-way param
  • @Binding var somename: SomeType
    • Two-way binding
    • Use .constant to create constant value for preview
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import SwiftUI

struct ImageLabelView: View {
    var imageName: String
    @Binding var email: String

    var body: some View {
        HStack {
            Image(systemName: imageName)
                .foregroundColor(.blue)
            Text(email)
        }
    }
}

#Preview {
    ImageLabelView(imageName: "envelope.fill", email: .constant("akaver@example.com"))
}

passing data to subView 2

  • To pass state variable to binding property prefix it with $
1
2
3
4
5
6
7
8
import Foundation

struct User {
    var firstName: String
    var lastName: String
    var title: String
    var email: String
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import SwiftUI

struct CardView: View {
    @State var user: User

    var body: some View {
        ZStack {
            Rectangle()
                .frame(width: 300, height: 100)
                .cornerRadius(20)
                .shadow(radius: 10)
                .foregroundColor(Color.orange)
            VStack(alignment: .leading){
                Text("\(user.firstName) \(user.lastName)")
                    .font(.title)
                Text(user.title)
                    .italic()
                Spacer()
                ImageLabelView(imageName: "envelope.fill", email: $user.email)
            }.padding()
                .frame(width: 300, height: 100)
        }
    }
}


#Preview {
    CardView(user: User(firstName: "First", lastName: "Last", title: "Title", email: "example@example.com"))
}

State and Data flow

Framework automatically performs most of the work traditionally done by view controllers – UI and state is synced

MVC

  • Embed view into NavigationView and create NavigationLinks
  • NavigationLink specifies destination view and content
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct ContentView: View {
    var users: [User] = [
        User(firstName: "Mikk", lastName: "Raba", title: "TA", email: "mikk.raba@taltech.ee"),
        User(firstName: "Kerman", lastName: "Saapar", title: "TA", email: "kesaap@taltech.ee"),
        User(firstName: "Andres", lastName: "Käver", title: "Blabers a lot", email: "andres.kaver@taltech.ee"),
    ]

    var body: some View {
        NavigationView {
            ScrollView {
                VStack {
                    ForEach(users){user in
                        NavigationLink(destination: InfoView()) {
                            CardView(user: user)
                        }
                    }
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

StateObject

  • Use State and Binding for value types
  • Use StateObject and ObservedObject for reference types

ForEach

  • ForEach in SwiftUI is a view struct – can be returned directly from view body
  • Provide array of items and how to identify items uniquely
1
2
3
4
5
        VStack(alignment: .leading) {
                    ForEach((1...10).reversed(), id: \.self) {
                        Text("\($0)...")
                    }
                }
  • In structs use Identifiable protocol

Identifiable

Use Identifiable protocol to help SwiftUI uniquely detect elements

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import Foundation

struct User: Identifiable { 
    let id = UUID()
    var firstName: String
    var lastName: String
    var title: String
    var email: String
}

extension User {
    static let users = [
        User(firstName: "Andres", lastName: "Käver", title: "Teacher", email: "akaver@akaver.com"),
        User(firstName: "Kerman", lastName: "Saapar", title: "TA", email: "kerman@example.com"),
        User(firstName: "Mikk", lastName: "Raba", title: "TA", email: "mikk@example.com"),
    ]
}

animation, gesture

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import SwiftUI

struct InfoView: View {
    @State var isScaled = false

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 50.0)
                .frame(width: 300.0, height: 200.0)
            Text("Hello, World!")
                .foregroundColor(Color.white)
        }
        .scaleEffect(isScaled ? 1.2 : 1.0)
        .animation(.easeInOut(duration: 1.0), value: isScaled)
        .gesture(TapGesture()
            .onEnded {
                isScaled.toggle()
            })
    }
}


#Preview {
    InfoView()
}

Size classes

  • Use @Environment
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import SwiftUI

struct InfoView: View {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    @Environment(\.verticalSizeClass) var verticalSizeClass

    @State var isScaled = false

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 50.0)
                .frame(width: 300.0, height: 200.0)
            Text("Hello, \(horizontalSizeClass == .compact ? "hc" : "hr") \(verticalSizeClass == .compact ? "vc" : "vr")")
                .foregroundColor(Color.white)
        }
        .scaleEffect(isScaled ? 1.2 : 1.0)
        .animation(.easeInOut(duration: 1.0), value: isScaled)
        .gesture(TapGesture()
            .onEnded {
                isScaled.toggle()
            })
    }
}


#Preview {
    InfoView()
}

System icons

System icons can be explored using this:
https://developer.apple.com/sf-symbols/