© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
A. NekrasovSwift Recipes for iOS Developershttps://doi.org/10.1007/978-1-4842-8098-0_8

8. SwiftUI

Alexander Nekrasov1  
(1)
Moscow, Russia
 

SwiftUI is a modern way of building user interfaces. It appeared in iOS 13, macOS 10.15, tvOS 13, and watchOS 6. Nowadays, most apps don’t need to support iOS 12, so it’s safe to use SwiftUI in your project.

On the other hand, there’s huge code base written in UIKit. Hundreds of popular libraries appeared before SwiftUI or support old versions of iOS. That’s why in this chapter, we’ll talk not only about SwiftUI but also about mixing two frameworks.

What is SwiftUI? Is it a wrapper around UIKit or an independent platform? It’s both. Some SwiftUI controls are built on top of UIKit; others are completely new.

Basic Overview of SwiftUI

SwiftUI uses the conception of building blocks. Each block is a View. Blocks can represent the whole screen, or part of it. Blocks can contain other blocks, and this structure can be deep. A similar architecture is used in other platforms like Flutter or React Native.

Basic App

SwiftUI apps contain two initial controls, App and View, shown in Recipes 8-1 and 8-2, respectively.
import SwiftUI
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
Recipe 8-1

SwiftUI App

import SwiftUI
struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Recipe 8-2

SwiftUI View

These two structs are the only pieces of code you need to run an app. It will have only one text label in the middle, but it will run.

Building Blocks

When you create a new SwiftUI project, you see two controls :
  • Text – The closest control from UIKit is UILabel. Of course, it has SwiftUI specifics.

  • ContentView – This View is the layout of the screen. It has size of phone screen because it’s referred in your App struct.

From a SwiftUI point of view, there’s no difference between Text and ContentView. For example, this code is totally valid:
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            Text("I'm the only element on the screen :)")
        }
    }
}

At the same time, ContentView can be used inside another View.

The most common controls are as follows:
  • Text – A block of text similar to UILabel

  • Image – A control displaying an image, similar to UIImageView

  • HStack and VStack – Controls displaying a row or column of elements, one after another, similar to UIStackView

  • List – A scrollable list of items, similar to UITableView or UICollectionView

  • ZStack – A frame, showing child controls on top of each other, like UIView in UIKit

  • Spacer – Used in stacks to make a dynamic gap between controls

  • Button – A tappable element, allowing the user to control the app. Like UIButton

The preceding list is not a full list, but these controls are the most common .

Modifying Controls

You can make small modifications to controls without overriding them or creating new ones. For example, you may want to change the font, color, padding, background, and other properties.

In SwiftUI, it’s done by calling methods following controls constructors. For example:
Text("Hello, world!")
            .font(.system(size: 14))
            .foregroundColor(.blue)

A modifier can be applied not only to components, but to a group of controls. For example, if you apply a foreground color to HStack, all the inner controls will get this color automatically, unless it’s overridden.

Compositions

When you create a new control, you usually build it from existing blocks. This is a composition . SwiftUI has three main controls for this purpose:
  • HStack – It shows controls horizontally one after another.

  • VStack – It shows controls vertically one after another.

  • ZStack – It shows controls on top of each other.

Recipe 8-3 is an example of such composition. It’s a custom control for a guest list shown in Figure 8-1. You have a list of contacts with photos and names. On the right side, there’s a switch. When it’s on, the contact is invited; otherwise, they are not in our guest list.

A set of two images has the first one with a screenshot of codes written to generate a guest list in the Swift user interface on the left and the preview of the guest list in the iPhone schema on the right.

Figure 8-1

Guest list written in SwiftUI. Code and preview

Photos will be loaded from the Web using the URL provided in pictureURL. SwiftUI has a native AsyncImage control, but it’s available only since iOS 15, so we’ll use KFImage.

KFImage is a part of the Kingfisher library. You can add it using Swift Package Manager: https://github.com/onevcat/Kingfisher .
import SwiftUI
import Kingfisher
struct ContactGuestView: View {
    let name: String
    let pictureURL: URL?
    @State private var isInvited = false
    var body: some View {
        HStack(alignment: .center,
               spacing: 8.0) {
            KFImage(pictureURL)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: 32, height: 32)
                .clipShape(Circle())
            Toggle(name, isOn: $isInvited)
                .onChange(of: isInvited) { newValue in
                    print(newValue)
                }
                .font(.system(size: 16))
                .foregroundColor(.primary)
        }
       .padding(.leading, 16)
       .padding(.trailing, 16)
       .frame(height: 32, alignment: .center)
    }
}
struct ContactGuestView_Previews: PreviewProvider {
    static var previews: some View {
        ContactGuestView(
            name: "John Doe",
            pictureURL: URL(string: "https://picsum.photos/32/32")
        )
    }
}
Recipe 8-3

Custom Component for Guest List

Here’s an interesting part in this line: @State private var isInvited = false

It’s a state of your component, or a part of it. When toggle (in UIKit, it’s called Switch) state is changed, the variable is automatically changed too. You can access your state and take the necessary action.

How to handle state change? In an ideal case, we should have a view model that handles all the changes and stores the result in a repository. For this, we use method .onChange. Full signature looks this way:
@inlinable public func onChange<V>(of value: V, perform action: @escaping (_ newValue: V) -> Void) -> some View where V : Equatable

In a provided closure, we change the value in the internal storage or send a request to the back end. In this example, we just print it.

Note

onChange should be called right after the Toggle declaration, before styling. Styling methods return simple View, not Toggle, which won’t recognize the onChange method.

To wrap the SwiftUI basics up, Recipe 8-4 shows how to use it.
import SwiftUI
struct ContactGuest {
    var id: String
    var name: String
    var pictureURL: URL?
}
struct GuestListView: View {
    let contacts: [ContactGuest]
    var body: some View {
        ScrollView(.vertical, showsIndicators: false) {
            LazyVStack {
                ForEach(contacts, id: .id) { contact in
                    ContactGuestView(
                        name: contact.name,
                        pictureURL: contact.pictureURL
                    )
                }
            }
        }
    }
}
struct GuestListView_Previews: PreviewProvider {
    static var previews: some View {
        GuestListView(
            contacts: [
                ContactGuest(
                    id: "1",
                    name: "John Stone",
                    pictureURL: URL(string: "https://picsum.photos/id/1/32/32")
                ),
                ContactGuest(
                    id: "2",
                    name: "Ponnappa Priya",
                    pictureURL: URL(string: "https://picsum.photos/id/2/32/32")
                ),
                ContactGuest(
                    id: "3",
                    name: "Mia Wong",
                    pictureURL: URL(string: "https://picsum.photos/id/3/32/32")
                ),
                ContactGuest(
                    id: "4",
                    name: "Peter Stanbridge",
                    pictureURL: URL(string: "https://picsum.photos/id/4/32/32")
                )
            ]
        )
    }
}
Recipe 8-4

Guest List

LazyVStack is a variation of VStack that doesn’t render invisible components. LazyVStack inside ScrollView is like UITableView. SwiftUI has its own List, but when you have a static set of items, it may be faster to use this composition.

Inserting UIKit Components

SwiftUI is a relatively new framework. Most of the available libraries, including the popular ones, still don’t support it. Fortunately, there’s an easy way to insert standard UIKit UIView (or any subclass) to a SwiftUI component tree.

Creating a SwiftUI Component from a UIKit Component

Let’s say you have a complicated custom UIView subclass MyCustomOldView that you’re not ready to port to SwiftUI. You need to make a wrapper around it, which conforms to the UIViewRepresentable protocol.

The second step is to add properties. You can add them by adding variables to your struct.

Steps 3 and 4 are implementing two functions declared in the UIViewRepresentable protocol:
func makeUIView(context: Self.Context) -> Self.UIViewType
This function creates an instance of your MyCustomOldView and returns it. If there are any constant settings of that component, you can set them up in this function as well. It may be helpful if you’re not using your own custom class, but standard UIKit class or class from an external library, and you need some adjustments.
func updateUIView(Self.UIViewType, context: Self.Context)

This function needs to update the state of your UIKit component. You shouldn’t (re)create your UIView in this function; only apply all the changeable properties. Don’t return anything.

Example with Custom Label

In Recipe 8-5, we’ll wrap a UILabel subclass into a SwiftUI structure. Many apps have a predefined list of components with integrated styles and formatting. When the app starts working with SwiftUI, there are two options:
  • To add all these components in SwiftUI using Text

  • To wrap existing components into SwiftUI ones

The second way has an advantage if part of your app migrated to SwiftUI but another part didn’t.
import UIKit
import SwiftUI
class HeaderLabel: UILabel {
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    private func commonInit() {
        textAlignment = .center
        font = .systemFont(ofSize: 20, weight: .bold)
        textColor = .darkText
        numberOfLines = 1
    }
}
struct HeaderText: UIViewRepresentable {
    var text: String
    func makeUIView(context: Context) -> HeaderLabel {
        HeaderLabel()
    }
    func updateUIView(_ uiView: HeaderLabel, context: Context) {
        uiView.text = text
    }
}
Recipe 8-5

Wrapping UIKit Components into SwiftUI

Insert it in any SwiftUI View:
struct ContentView: View {
    var body: some View {
        HeaderText(text: "Hello, world!")
    }
}

If your properties can be changed dynamically, you can add the @Binding keyword .

Applying Styles with ViewModifier

We already applied several styles with modifiers. We used font, foregroundColor, frame, and others.

When you create your own component, you don’t have to provide all the adjustable properties in a constructor. It may be reasonable if you have two to three of them. But when you create a more complicated View, there can be dozens of them, and it’s very uncomfortable to use them all in a constructor.

For this purpose, you can add extensions changing your custom View and view modifiers changing the properties of View more globally.

Creating a ViewModifier

To create your own modifier, you need to add a struct conforming to the ViewModifier protocol .

In this struct, you need to implement one function:
func body(content: Content) -> some View
In this function, you need to set up your View and return the result.
struct ModifierName: ViewModifier {
    func body(content: Content) -> some View {
        content
            ... // Do modifications here
    }
}

Chaining Modifiers in SwiftUI

To make your modifier look like the standard SwiftUI modifiers, you need to create an extension. Depending on your modifier, you can make it globally available by extending View, or make it more targeted to your own view or some tree of components (subclasses of Text, for example).
extension View {
    func applyMyStyle() -> some View {
        modifier(ModifierName())
    }
}

HeaderText without UIKit

Recipe 8-6 shows how we can make a modifier for Text to make a header with style matching the one from Recipe 8-5 .
import SwiftUI
struct Header: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.system(size: 20, weight: .bold))
            .foregroundColor(Color(UIColor.darkText))
            .fixedSize(horizontal: false, vertical: true)
            .lineLimit(1)
            .frame(alignment: .center)
    }
}
extension Text {
    func styleAsHeader() -> some View {
        modifier(Header())
    }
}
Recipe 8-6

HeaderText Using ViewModifier

Use it this way:
struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .styleAsHeader()
    }
}
If there are properties specific to your View that can’t be applied to Context, apply them in an extension directly, before calling the modifier method. For example:
func changeMyView() -> some View {
    doSomethingSpecific().modifier(SomeModifier())
}

Using ViewModifiers, you can create your own style library without subclassing Views.

Note

If you apply a modifier on a container (e.g., HStack or VStack), it applies to all of its children. That’s why it’s usually better to use pure modifiers without calling methods of the View subclass and to extend View globally.

Creating Custom Views

One of the most useful features of SwiftUI is creating custom reusable components . Creating small adjustable building blocks gives endless possibilities in UI creation.

There are three main ways of creating custom views:
  • Composition of existing SwiftUI components adjusted for your needs

  • Wrapping UIKit components

  • Using Canvas for drawing your own views using lines, circles, and other primitives

Custom views can have changeable data. There are two key property wrappers:
  • @State

  • @Binding

We already discussed composition and wrapping UIKit components earlier in this chapter. Let’s review Canvas drawing and property wrappers in details.

Drawing on a Canvas

Canvas is a SwiftUI control allowing to get access directly to CoreGraphics’ CGContext. CGContext has a number of methods for drawing, such as follows:
  • func addRect(CGRect) – Draws a rectangle

  • func addEllipse(in: CGRect) – Draws an ellipse

  • func addArc(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) – Draws an arc (part of an ellipse)

  • And others

If we draw a filled circle with dashed border and spin it around its center, it can look like a loading indicator (see Figure 8-2). Let’s review an example in Recipe 8-7.

A set of two images has the first one with a screenshot of codes written to generate a custom spinner in the swift user interface on the left and the preview of the custom spinner as a blue dot in the iPhone schema on the right.

Figure 8-2

Custom spinner written in SwiftUI

import SwiftUI
let loaderTimer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
struct LoaderView: View {
    let circleColor: UIColor
    let spinnerColor: UIColor
    @State private var phase: CGFloat = 0
    @State private var length: CGFloat = Double.pi*12
    var body: some View {
        Canvas { context, size in
            context.withCGContext { cgContext in
                let rect = CGRect(origin: .zero, size: size).insetBy(dx: 4, dy: 4)
                let path = CGPath(ellipseIn: rect, transform: nil)
                cgContext.addPath(path)
                cgContext.setStrokeColor(spinnerColor.cgColor)
                cgContext.setFillColor(circleColor.cgColor)
                cgContext.setLineWidth(4)
                cgContext.setAlpha(0.5)
                cgContext.setLineDash(phase: 0, lengths: [length])
                cgContext.drawPath(using: .eoFillStroke)
            }
        }
        .frame(width: 32, height: 32)
        .transformEffect(
            CGAffineTransform(translationX: 16, y: 16)
                .rotated(by: phase / 8)
                .translatedBy(x: -16, y: -16)
        )
        .onReceive(loaderTimer) { _ in
            phase += 1
            let sinPhase = sin(phase / 20)
            length = Double.pi * (CGFloat(12) + abs(sinPhase) * CGFloat(11))
        }
    }
}
struct LoaderView_Previews: PreviewProvider {
    static var previews: some View {
        ZStack {
            Color.black
                .edgesIgnoringSafeArea(.all)
            LoaderView(
                circleColor: .blue,
                spinnerColor: .white
            )
        }
    }
}
Recipe 8-7

Using Canvas in SwiftUI

You can see many magic numbers here. Try to play with them to see how the result changes.

Component State

We already used the @State keyword in several recipes. What is @State and how does it change our View?

@State is a property wrapper , which means it generates some code behind the scenes. It creates a storage and moves our variable out of struct as it’s a value type. It’s recommended to make @State variables private and initialize them right after the declaration, without a constructor.

@State variables are used to keep the View state. Depending on component logic, it can be a text string, a Boolean value, a number, or a structure. @State variables can be directly used in layout, and even more, the layout can change them in response to the user’s actions.

@State variables shouldn’t be shared between objects. They’re not observable, and their changes can’t be handled by willSet or didSet. To run some code when the @State variable is changed, you can use three ways:
  • Use UI control callbacks, like onChange or onEditingChanged.

  • Use bindings.

  • Use observable objects (e.g., your view model can conform to ObservableObject).

To see the @State wrapper in action, please review Recipes 8-3 and 8-7.

Data Binding

Using @Binding instead of @State allows you to have a more powerful connection between variables and UI elements using it and share state between objects .

If you use a view model and declare some variable there, you can set up a connection with UI elements passing the binding itself, not the value.

The advantages of this approach are as follows:
  • When you change Toggle or type text, it will change the value directly in your view model. It’s much easier to use it later.

  • If a variable changes several UI components, they’ll be automatically updated when the variable is changed.

For example, if some text field should appear only when Toggle is on, @State won’t solve the problem, but @Binding will (Recipe 8-8).
import SwiftUI
class LoginViewModel: ObservableObject {
    @Published var login: String = ""
    @Published var password: String = ""
    @Published var rememberMe: Bool = false
}
struct LoginView: View {
    @Binding var login: String
    @Binding var password: String
    @Binding var rememberMe: Bool
    var body: some View {
        VStack {
            TextField("Login", text: $login)
            TextField("Password", text: $password)
            Toggle(isOn: $rememberMe) {
                Text("Remember me")
            }
        }
    }
}
struct LoginScreenView: View {
    @ObservedObject var viewModel = LoginViewModel()
    private var loginAvailable: Bool {
        !viewModel.login.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
        !viewModel.password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }
    var body: some View {
        VStack {
            LoginView(
                login: $viewModel.login,
                password: $viewModel.password,
                rememberMe: $viewModel.rememberMe
            )
            Button("Login") {
                print("Your login: (viewModel.login)")
                print("Your password: (viewModel.password)")
                if viewModel.rememberMe {
                    print("We will remember you")
                } else {
                    print("You will be automatically logged out")
                }
            }.disabled(!loginAvailable)
        }
    }
}
struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginScreenView()
    }
}
Recipe 8-8

Using @Binding

In this recipe, we use both bindings and observable objects. By using a publishable view model and bindings, we can control the Login button (it becomes inactive when one of the fields is empty) with minimum code. We can also access the entered values from the Login button callback without referencing the text fields themselves.

These features show the power of SwiftUI and its advantages compared to UIKit (Figure 8-3).

A set of two images. A screenshot of data binding codes in the swift user interface is on the left and a preview of the login page creation in the iPhone outline is on the right.

Figure 8-3

Data binding in SwiftUI

Summary

SwiftUI is a future of iOS UI development. The purpose of this chapter is not to give a comprehensive SwiftUI course, it’s too big topic for one chapter. But we talked about several interesting concepts showing power of SwiftUI, such as building user interface from building blocks, using UIKit components in SwiftUI, using ViewModifier, Canvas and Binding.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.188.62