Home A SwiftUI Tour
Post
Cancel

A SwiftUI Tour

SwiftUI is a new way to build your UI in easy and faster way. with less code and apple cross platform views.

Why to use

-SwiftUI redraw your view when you change the state of your view so it will always have the latest data.
-It will help you to build reusable views that can be used across any apple platform (watch, tv, phone).
-Easy way to animate the views.
-Faster development thanks to the Xcode live preview, as now you don’t need to run the app each time you change the layout.

Let’s start

Create a SwiftUI project and then navigate to ContentView file, if you look at the ContentView file content, you will find that by default each swift file will have two Struct one will contain your views and layout and the second struct confirm to PreviewProvider Xcode search for any struct confirming to PreviewProvider protocol and generate a previews for it. So you can have multiple previews with deferent settings.

In the top right of the canvas click resume to display the preview. After that try to change the Hello world text and see what will happen.

Customize your views

There is two ways to change the design, by code and the inspector. It’s a new thing so the inspector will help you to know what attributes are available.

Customize using inspector

To show the inspector command-click on the view, then select Show SwiftUI Inspector. Then change the attributes as you want. As you can see, when you change any attributes the code also will change.

Customize using code

Below the text try to change the font using .font(.title) and the color using .foregroundColor(.green).

1
2
3
4
5
6
7
struct ContentView: View {
  var body: some View {
    Text("Hello, World!")
        .font(.title)
        .foregroundColor(.green)
  }
}

The methods we used above, called modifiers, modifiers wrap the view to change it’s properties and each modifier returns a new view.

Stacks

In SwiftUI you can group your view using three type of stacks. HStack, which group your views horizontally.

1
2
3
4
5
6
7
8
9
10
struct ContentView: View {
  var body: some View {
    HStack {
      Text("Hello, World!")
          .font(.title)
          .foregroundColor(.green)
      Text("Placeholder")
    }
  }
}

VStack, which group your views vertically.

1
2
3
4
5
6
7
8
9
10
struct ContentView: View {
  var body: some View {
    VStack {
      Text("Hello, World!")
        .font(.title)
        .foregroundColor(.green)
      Text("Placeholder")
    }
  }
}

ZStack, which group your views above each others, back to front.

1
2
3
4
5
6
7
8
9
10
struct ContentView: View {
  var body: some View {
    ZStack {
      Text("Hello, World!")
        .font(.title)
        .foregroundColor(.green)
      Text("Placeholder")
    }
  }
}

The Stack view has two initializer properties, alignment and spacing.

1
2
3
4
5
6
7
8
9
10
struct ContentView: View {
  var body: some View {
    VStack(alignment: .trailing, spacing: 10) {
      Text("Hello, World!")
        .font(.title)
        .foregroundColor(.green)
      Text("Placeholder")
    }
  }
}

If you want to set priority for the views you can use .layoutPriority()

1
2
3
4
5
6
7
8
9
10
11
struct ContentView: View {
  var body: some View {
    VStack(alignment: .trailing, spacing: 10) {
      Text("Long text Long text Long text Long text Long text")
        .font(.title)
        .foregroundColor(.green)
      Text("Placeholder").layoutPriority(1)
    }
    .lineLimit(1)
  }
}

Padding and Spacer

Padding add a space to the left and right of the HStack and space to the top and bottom of the VStack. While Spacer is a flexible space expands along the magor axis of it’s stack view.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ContentView: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 10) {
      Text("Hello, World!")
        .font(.title)
        .foregroundColor(.green)
      HStack {
        Text("Placeholder")
          .font(.subheadline)
        Text("Placeholder")
          .font(.subheadline)
      Spacer()
      }
    }
    .padding()
  }
}

Custom View

To make a custom view that can be reused in defrent screens, click on File, New, then file. in the user interface section select SwiftUI View and name it then click on Create.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct WarningView: View {
  let message: String
  
  init(message: String) {
    self.message = message
  }
  
  var body: some View {
    ZStack {
      Color.yellow.edgesIgnoringSafeArea(.all)
      Text(message)
        .foregroundColor(.red)
        .font(.title)
        .fontWeight(.bold)
    }
  }
}

I created a warning view to show warning text to the user. To use it:

1
2
3
4
5
6
7
8
9
10
11
struct ContentView: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 10) {
      Text("Hello, World!")
        .font(.title)
        .foregroundColor(.green)
      WarningView(message: "Warning")
    }
    .padding()
  }
}

UIKit

UIViewRepresentable

To use any UIKit view you need to wrap it with UIViewRepresentable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct MapView: UIViewRepresentable {
  func makeUIView(context: Context) -> MKMapView {
    MKMapView(frame: .zero)
  }
  
  func updateUIView(_ uiView: MKMapView, context: Context) {
    let coordinate = CLLocationCoordinate2D(
    latitude: 31.963158,
    longitude: 35.930359)
    let camera = MKMapCamera(lookingAtCenter: coordinate,
                             fromDistance: 400000,
                             pitch: 0,
                             heading: 0)
    uiView.setCamera(camera, animated: true)
  }
}

Then you can use it like this:

1
2
3
4
5
struct ContentView: View {
  var body: some View {
    MapView()
  }
}

UIKit view will not show in static mode to see the map switch to live view by clicking on Live Preview button.

UIViewControllerRepresentable

To use a UIKit view controller you need to create a new struct that confirms to UIViewControllerRepresentable.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
struct ContentView: View {
  var body: some View {
    PageViewController()
  }
}
struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}
struct PageViewController: UIViewControllerRepresentable {
  func makeCoordinator() -> Coordinator {
    Coordinator()
  }
  
  func makeUIViewController(context: Context) -> AppViewController {
    let viewController = AppViewController()
    viewController.delegate = context.coordinator
    return viewController
  }
  func updateUIViewController(_ pageViewController: AppViewController, context: Context) {
  }
  class Coordinator: MyDelegate {
    func printIt() {
      print("Hi!")
    }
  }
}
class AppViewController: UIViewController {
  var button: UIButton!
  weak var delegate: MyDelegate?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    addButton()
  }
  private func addButton() {
    button = UIButton()
    button.setTitle("Hi", for: .normal)
    button.setTitleColor(.red, for: .normal)
    button.frame = view.bounds
    button.addTarget(self, action: #selector(pressed), for: .touchUpInside)
    view.addSubview(button)
  }
  @objc
  private func pressed() {
    delegate?.printIt()
  }
}
protocol MyDelegate: AnyObject {
  func printIt()
}

As in the example, you can use Coordinator to implement any delegate the view controller require.

Image

To make an image you need to use Image struct.

1
2
3
4
5
6
7
8
9
10
11
struct ProfileImageView: View {
  var body: some View {
    Image("image")
      .resizable() // To make an image resizable
      .aspectRatio(contentMode: .fill) // to change the content mode
      .frame(width: 200.0, height: 200.0) // to change the frame
      .clipShape(Rectangle()) // clip to a shape, you can also use a `Circle()`.
      .overlay(Rectangle().stroke(Color.yellow, lineWidth: 2)) // add an overlay.
      .shadow(radius: 10) // add shadow
  }
}

Button

Creating a button is very easy in SwiftUI, you will have to pass to things the action and the view builder. the action will be called when the user click on the button, and the view builder should has how the button will look like.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ContentView: View {
  var body: some View {
    Button(action: {
      print("Wow")
    }) {
      Text("Press here")
        .font(.largeTitle)
        .fontWeight(.bold)
        .foregroundColor(.red)
        .background(Color.black)
    }
  }
}

To see the print message in the debug area right click on the Live preview and click on Debug preview. To make a rounded corner button you need to use padding and cornerRadius modifiers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView: View {
    var body: some View {
      Button(action: {
        print("Wow")
    }) {
      Text("Press here")
        .font(.largeTitle)
        .fontWeight(.bold)
        .foregroundColor(.red)
        .padding()
        .background(Color.black)
        .cornerRadius(40)
    }
  }
}

Notice that i added the padding before the background color, it’s important because if you add the padding after the background you will have a different result. To make a button with gradient you need to set a LinearGradient as a background.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentView: View {
  Button(action: {
    print("Wow")
  }) {
    Text("Press here")
      .font(.largeTitle)
      .fontWeight(.bold)
      .foregroundColor(.white)
      .padding()
      .background(LinearGradient(gradient: Gradient(colors: [.purple, .pink]), startPoint: .leading, endPoint: .trailing))
      .cornerRadius(40)
    }
  }
}

List

First we need to create a row.

1
2
3
4
5
6
7
8
9
struct ItemRowView: View {
  let name: String
  var body: some View {
    HStack {
      Text(name)
      Spacer()
    }.padding()
  }
}

Then you can add this row to a list.

1
2
3
4
5
6
7
8
struct ContentView: View {
  var body: some View {
    List {
      ItemRowView(name: "Abed")
      ItemRowView(name: "Zaina")
    }
  }
}

What if you want to make it dynamic? it’s also easy.

1
2
3
4
5
6
7
8
struct ContentView: View {
  let names = ["Abed", "Moski", "Hafs", "Rajiv"]
  var body: some View {
    List(names, id: \.self) { item in
      ItemRowView(name: item)
    }
  }
}

We provide the id to allow swift you to identify our view inside the list so it can control them and animate them.

To give the abilty of navigation to your view you need to wrap it with NavigationView, to add a title to the navigation view you can use navigationBarTitle(_:) modifier in the item inside the navigation view.

Now to make a Row in a list navigate you to another screen you need to wrap it with NavigationLink, the navigation link take a destination parameter which is the View you need to navigate to.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ContentView: View {
  let names = ["Abed", "Moski", "Hafs", "Rajiv"]
  var body: some View {
    NavigationView {
      List(names, id: \.self) { item in
        NavigationLink(destination: DetailsView()) {
          ItemRowView(name: item)
        }
      }
      .navigationBarTitle(Text("Users"))
    }
  }
}

Pass data to different screen

To pass data to different screen that screen need to have a initializer that take the value you need to pass.

1
2
3
4
5
6
7
8
9
struct DetailsView: View {
  let name: String
  var body: some View {
    HStack {
      Text(name)
      Spacer()
    }.padding()
  }
}

And you can pass it like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ContentView: View {
  let names = ["Abed", "Moski", "Hafs", "Rajiv"]
  var body: some View {
    NavigationView {
      List(names, id: \.self) { item in
        NavigationLink(destination: DetailsView(name: item)) { //*
          ItemRowView(name: item)
        }
      }
      .navigationBarTitle(Text("Users"))
    }
  }
}

Group

It’s a view to group another views.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ItemRowView: View {
  let name: String
    var body: some View {
      HStack {
        Group {
          Text("Hi!")
          Text("Hi!")
        } 
        Group {
          Text("Hi!")
          Text("Hi!")
        }
     }
  }
}

Why to use group? one of the reasons that you can add up to 10 views in one stack but not more than that, but with groups you can add multiple groups and each group has multiple items.

State value

It’s a value that can be change, and changing it effects the view content or layout, you can add @State attribute to add state to a view.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView: View {
  @State private var isLoved = true
  var body: some View {
    VStack {
      Toggle(isOn: $isLoved) {
        Text("Love you?")
      }
      Image(systemName: isLoved ? "heart.fill" : "heart")
        .resizable()
        .frame(width: 30, height: 30)
        .foregroundColor(isLoved ? .red : .black)
    }
    .padding()
  }
}

It’s recommended to keep the @State properties as private as it’s belong to the view and will not be shared with another view even if it’s shared it will be a copy.

Object Bindning

Use it for complex properties, like the one you will share to multipile views or when you care that the value should be a refrance value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class User: ObservableObject {
  @Published var name = "abedalkareem"
  @Published var age = 26
  @Published var isRich = false
}
struct ContentView: View {
  @ObservedObject var user = User()
  var body: some View {
    VStack {
      Button(action: {
        self.user.isRich = true
      }) {
        Text("Make him rich")
      }
      Text("\(user.name)")
      Text("\(user.age)")
      Text("Is rich: \(user.isRich ? "yes" : "no")")
    }
    .padding()
  }
}

@Publish is a property wappers that allow us to create observable objects that automatically notify the observers when change happens.

Environment Object

It’s a way to share object to be available everywhere in your app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class User: ObservableObject {
  @Published var name = "abedalkareem"
  @Published var age = 26
  @Published var isRich = false
}
struct ContentView: View {
  @EnvironmentObject var user: User
  var body: some View {
    VStack {
      Button(action: {
        self.user.isRich = true
      }) {
        Text("Make him rich")
      }
      Text("\(user.name)")
      Text("\(user.age)")
      Text("Is rich: \(user.isRich ? "yes" : "no")")
    }
    .padding()
  }
}

Now to set the user value you can do this:

1
2
3
4
5
6
7
8
9
10
11
12
13
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
  let user = User()
  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    let contentView = ContentView().environmentObject(user) // here
    if let windowScene = scene as? UIWindowScene {
      let window = UIWindow(windowScene: windowScene)
      window.rootViewController = UIHostingController(rootView: contentView)
      self.window = window
      window.makeKeyAndVisible()
    }
  }
}

Now any View has a user object will have the same intstance and when the value changed it will change everwhere else.

1
@EnvironmentObject var user: User

Animation

To animate any animatable property of a view (opacity, rotation, color, size), you can use animation(_:) modifier.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ContentView: View {
  @State var isLoved = true
  var body: some View {
    Button(action: {
      self.isLoved.toggle()
    }) {
      Image(systemName: "heart.fill")
        .foregroundColor(isLoved ? .red : .black)
        .imageScale(.large)
        .rotationEffect(.degrees(isLoved ? 0 : 180))
        .scaleEffect(isLoved ? 1.5 : 1)
        .padding()
        .animation(.easeIn)
    }
  }
}

To stop one property animation you can add .animation(nil) after it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ContentView: View {
  @State var isLoved = true
    var body: some View {
      Button(action: {
        self.isLoved.toggle()
      }) {
        Image(systemName: "heart.fill")
          .foregroundColor(isLoved ? .red : .black)
          .imageScale(.large)
          .rotationEffect(.degrees(isLoved ? 0 : 180))
          .animation(nil)
          .scaleEffect(isLoved ? 1.5 : 1)
          .padding()
          .animation(.easeIn)
      }
   }
}

This will stop the animation of the rotation. Another way to animate all the affected views by the value change is to wrap the value changing with withAnimation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ContentView: View {
  @State var isLoved = true
  var body: some View {
    Button(action: {
      withAnimation(.easeInOut(duration: 4)) {
        self.isLoved.toggle()
      }
    }) {
      Image(systemName: "heart.fill")
        .foregroundColor(isLoved ? .red : .black)
        .imageScale(.large)
        .rotationEffect(.degrees(isLoved ? 0 : 180))
        .scaleEffect(isLoved ? 1.5 : 1)
        .padding()
    }
  }
}

You can use .easing_you_choose(duration: your_duration) to change the duration. To add transition you can use transition(_:), in case of hiding and showing a view the default animation is fade in and out but if you add transtion it will take the transition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct ContentView: View {
  @State var hidden = true
  var body: some View {
    VStack {
      Button(action: {
        withAnimation(.easeInOut(duration: 1)) {
          self.hidden = !self.hidden
        }
      }) {
        Image(systemName: "trash.fill")
        .foregroundColor(.yellow)
      }
      if hidden {
        Image(systemName: "heart.fill")
          .foregroundColor(.red)
          .imageScale(.large)
          .padding()
          .transition(.slide)
      }
      Spacer()
    }
  }
}

To add a custom animation for removing and adding you can use asymmetric(insertion:, removal:).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct ContentView: View {
  @State var hidden = true
  var body: some View {
    VStack {
      Button(action: {
        withAnimation(.easeInOut(duration: 1)) {
          self.hidden = !self.hidden
        }
      }) {
        Image(systemName: "trash.fill")
        .foregroundColor(.yellow)
      }
      if hidden {
        Image(systemName: "heart.fill")
          .foregroundColor(.red)
          .imageScale(.large)
          .padding()
          .transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top)))
}
      Spacer()
    }
  }
}

PreviewProvider

The preview provider help you change the layout in the preview, apply system

Preview Layout

It helps you to change the layout of the view.

1
2
3
4
5
6
struct ItemRowView_Previews: PreviewProvider {
  static var previews: some View {
    ItemRowView(name: "Test")
      .previewLayout(.fixed(width: 300, height: 100))
  }
}

Change the device type

1
2
3
4
5
6
struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
      .previewDevice(PreviewDevice(rawValue: "iPhone 8"))
  }
}

To show multiple devices

1
2
3
4
5
6
7
8
9
struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ForEach(["iPhone 8", "iPhone 8 Plus", "iPhone X"], id: \.self) { item in
      ContentView()
        .previewDevice(PreviewDevice(rawValue: item))
        .previewDisplayName(item)
    }
  }
}

Dark mode

To enable dark mode.

1
2
3
4
5
6
7
8
9
10
11
struct ContentView: View {
  var body: some View {
    Text("Say my name")
  }
}
struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
      .environment(\.colorScheme, .dark)
  }
}

WatchOS

Any code you wrote above can be used on watchOS but there is one difference when it comes to platform specific.

WKInterfaceObjectRepresentable

To use any WatchKit object you need to warp it with WKInterfaceObjectRepresentable.

1
2
3
4
5
6
7
struct WatchMapView: WKInterfaceObjectRepresentable {
  func makeWKInterfaceObject(context: WKInterfaceObjectRepresentableContext<WatchMapView>) -> WKInterfaceMap {
    return WKInterfaceMap()
  }
  func updateWKInterfaceObject(_ map: WKInterfaceMap, context: WKInterfaceObjectRepresentableContext<WatchMapView>) {
  }
}

To do any update on the watch kit object you can do it in the updateWKInterfaceObject method.

Notification Interface

To make a custom notification interface you need to use WKUserNotificationHostingController as below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class NotificationController: WKUserNotificationHostingController<NotificationView> {
  var title: String?
  var message: String?
  override var body: NotificationView {
    NotificationView(title: title,
                     message: message)
  }
  override func willActivate() {
    // This method is called when watch view controller is about to be visible to user
    super.willActivate()
  }
  override func didDeactivate() {
    // This method is called when watch view controller is no longer visible
    super.didDeactivate()
  }
  override func didReceive(_ notification: UNNotification) {
    let notificationData = notification.request.content.userInfo as? [String: Any]
    let aps = notificationData?["aps"] as? [String: Any]
    let alert = aps?["alert"] as? [String: Any]
    title = alert?["title"] as? String
    message = alert?["body"] as? String
  }
}

-❤️~.
If you have any questions you can send me a message on Twitter or facebook. Also you can check my Github page or my Apps.

This post is licensed under CC BY 4.0 by the author.