SwiftUI - Building the UI

SwiftUI enables us to build the user interface in a declarative way. The framework is entirely designed around structs.

— View hierarchy

struct ContentView: View {
    var body: some View {
        Text("Building the UI")
    }
}

Structs inherits from a View protocol A type that represents a SwiftUI view and uses a computed body property that must return some View.

The some keyword has been introduced with Swift 5 and stands for an opaque type which must be inferred by the compiler within the body (if not an error will be raised). The body cannot return zero views nor multiple views but one specific view.

Views are automatically constructed using @ViewBuilder which, under the hood, takes the function body, through an init(content: () -> Content) initializer and returns a view which may also returns a view itself and so on.

This how a view hierarchy is created in SwiftUI.

struct ContentView: View {
     var body: some View {
         NavigationView {
             VStack(alignment: .leading) {
                 Text("Building the UI")
                 List {
                     CategoryRow()
                 }
          }
      }
}

Designing your view hierarchy is straightforward as the top level view must be positioned first and each subview will be nested within a container view.

In our ContentView, the navigation view contains a vertical stack view which itself contains a text and a list with only one row.

While this view hierarchy remains light, it can quickly become massive when building more complex UI. Custom views address this issue.

— Custom views

While SwiftUI enables us to work directly with VStack, ZStack, Text, Picker or List.. it may sometimes be useful to create our custom views which will encapsulate these built-in components in order to create a specific view.

Creating a custom loader view is useful to warn the user some data is being fetched from a web-service.

Because the view hierarchy is designed around a Content type which must conform to the View protocol, we may end up with the following implementation to nest some of our content within our loader view.

struct LoaderView<Content>: View where Content: View {
    var isShowing: Bool
    var content: () -> Content
    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .center) {
                self.content()
                    .disabled(self.isShowing)
                    .blur(radius: self.isShowing ? 8 : 0)
                ActivityIndicator(
                    isAnimating: self.isShowing, 
                    style: .large
                )
                    .frame(
                         width: geometry.size.width / 4, 
                         height: geometry.size.height / 7.5
                    )
                    .background(Color.gray.opacity(0.5)) 
                    .foregroundColor(Color.primary)
                    .cornerRadius(20)
                    .opacity(self.isShowing ? 1 : 0)
            }
        }
    }
}

The custom view can then be placed at a specific position within the view hierarchy.

struct ContentView: View {
    var body: some View {
        LoaderView(isShowing: true) {
            NavigationView {
                VStack(alignment: .leading) {
                    Text("Building the UI")
                List {
                    CategoryRow()
                } 
            }
        }
     }
}

Using computed properties within the ContentView may improve readability avoiding all the nested code layers.

struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme: ColorScheme
    var body: some View {
        self.preferencesButton
    }
    var preferencesButton: some View {
        Button(action: {
            self.navigator.presenting = .preferences
        }) {
        Image(systemName: "selection.pin.in.out")
            .accentColor(
                colorScheme == .light ? Color.black: Color.white
            )
        }
    }
}

Because all of the view hierarchy is rendered through the body property, the struct might quickly end up with a large body which may create unexpected behaviors (confusion too) when building your UI.

Custom views are a convenient way to provide more clarity and legibility (and better maintenance) to your project.

- Modifiers

Modifiers in SwiftUI enable us to customize the UI.

struct HeaderView: View {
    var headlines: Headlines
    var body: some View {
        Group {
            HStack {
                Text("Building the UI")
                    .font(.system(size: 30))
                    .fontWeight(.semibold)
 
                Image(systemName: "star.fill")
                    .imageScale(.medium)
                    .foregroundColor(.yellow)
            }.frame(height: 50)
         }
     }
}

The SwiftUI declarative approach provides a clear understanding on how views will be rendered. Because these modifiers return some View, it might lead to some undesired UI effects therefore the position of these modifiers is extremely important.

Adding the following modifiers after .fontWeight(.semibold) will result in an unwanted extra layer with yellow background since we’re applying the background color modifier to the layer returned from the .frame(width: 200, height: 200, alignment: .center) modifier.

.background(Color.red)
.frame(width: 200, height: 200, alignment: .center)
.background(Color.yellow)