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

Although SwiftUI is described as being data driven, it is still necessary to handle the events that are generated when a user interacts with the views in the user interface. Some views, such as the Button view, are provided solely for the purpose of user interaction. In fact, the Button view can be used to turn a variety of different views into a "clickable" button. It is possible, for example, to designate an entire stack of views as a single button. A Button view needs to be declared with the action method to be called when a click is detected together with the view to act as the button content.

Button(action: {
    // Code to perform action here
}) {
    Text("Click Me")
}

Button(action: {
    print("Button clicked")
}) {
    Image(systemName: "square.and.arrow.down")
}

Frames

By default, a view will be sized automatically based on its content and the requirements of any layout in which it may be embedded. Although much can be achieved using the stack layouts to control the size and positioning of a view, sometimes a view is required to be a specific size or to fit within a range of size dimensions. To address this need, SwiftUI includes the flexible frame modifier.

Text("Hello World")
    .font(.largeTitle)
    .border(Color.black)
    .frame(width: 100, height: 100, alignment: .center)

Frames can also be implemented so that they are sized relative to the size of the container within which the corresponding view is embedded. This is achieved by wrapping the view in a GeometryReader and using the reader to identify the container dimensions. These dimensions can then be used to calculate the frame size. The following example uses a frame to set the dimensions of two Text views relative to the size of the containing VStack:

GeometryReader { geometry in
    VStack {
        Text("Hello World, how are you?")
            .font(.largeTitle)
            .frame(width: geometry.size.width / 2, 
                height: (geometry.size.height / 4) * 3)
        Text("Goodbye World")
            .font(.largeTitle)
            .frame(width: geometry.size.width / 3,
                height: geometry.size.height / 4)
    }
}

The topmost Text view is configured to occupy half the width and three quarters of the height of the VStack while the lower Text view occupies one third of the width and one quarter of the height.

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

The SwiftUI List control provides similar functionality to the UIKit TableView class in that it presents information in a vertical list of rows with each row containing one or more views contained within a cell. Consider, for example, the following List implementation:

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

Lists are similar to scroll views in that they are used to view a collection of items. Lists are used for larger datasets, whereas scroll views are used for smaller datasets; the reason being that list views do not load the whole dataset in memory at once and thus are more efficient at handling large data.

We'll create a struct to hold weather information and an array of weather data. The data will then be used to create a view that provides weather information from several cities.

A custom list row is used when working with several lines of code within a list view row. Implementing custom lists improves modularity and readability, and allows code reuse.

Proceed as follows:

struct WeatherRow: View {
    var weather: WeatherInfo
    var body: some View {
        HStack {
            Image(systemName: weather.image)
                .frame(width: 50, alignment: .leading)
            Text("\(weather.temp)°F")
                .frame(width: 80, alignment: .leading)
            Text(weather.city)
        }
        .font(.system(size: 25))
        .padding()
    }
}

struct WeatherInfo: Identifiable {
    var id = UUID()
    var image: String
    var temp: Int
    var city: String
}

struct ContentView: View {
    let weatherData: [WeatherInfo] = [
    WeatherInfo(image: "snow", temp: 5, city:"New York"),
    WeatherInfo(image: "cloud", temp:5, city:"Houston"),
    WeatherInfo(image: "sun.max", temp: 80, city:"San Francisco")
    ]
    var body: some View {
        List {
            ForEach(self.weatherData){ weather in
                WeatherRow(weather: weather)
            }
        }
    }
}

The id = UUID() property creates a unique identifier for each variable we create from the struct. Adding the id property to our struct allows us to later use it within a ForEach loop without providing id: .\self parameter.

Lists are usually used to add, edit, remove, or display content from an existing dataset. To implement the add functionality, we will enclose the List view in NavigationView, and add a button to navigationBarItems that triggers the add function we will create.

struct ContentView: View {
    @State var numbers = [1,2,3,4]
    var body: some View {
        NavigationView{
            List{
                ForEach(self.numbers, id:\.self){ number in
                    Text("\(number)")
                }
            }.navigationBarTitle("Number List", displayMode: .inline)
            .navigationBarItems(trailing: Button("Add", action: addItemToRow))
        }
    }
    private func addItemToRow() {
        self.numbers.append(Int.random(in: 0 ..< 100))
    }
}

Wrapping the list in a navigation view allows you to add a title and navigation items to the view.

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("North America")){
                    Text("USA")
                }
                Section(header: Text("Africa")){
                    Text("Nigeria")
                    Text("Ghana")
                }
                Section(header: Text("Europe")){
                    Text("Spain")
                    Text("France")
                    Text("UK")
                }
            }
        .listStyle(GroupedListStyle())
            .navigationBarTitle("Continents and Countries", displayMode: .inline)
        }
    }
}

To make items in a list navigable, the first step is to embed the entire list within a NavigationView. Once the list is embedded, the individual rows must be wrapped in a NavigationLink control which is, in turn, configured with the destination view to which the user is to be taken when the row is tapped.”

struct ToDoItem : Identifiable {
    var id = UUID()
    var task: String
    var imageName: String
}

struct ContentView: View {
    @State private var toggleStatus = true

    var listData: [ToDoItem] = [
        ToDoItem(task: "Take out trash", imageName: "trash.circle.fill"),
        ToDoItem(task: "Pick up the kids", imageName: "person.2.fill"),
        ToDoItem(task: "Wash the car", imageName: "car.fill")
    ]

    var body: some View {
        NavigationView {
            List {
                Section(header: Text("Settings")) {
                    Toggle(isOn: $toggleStatus) {
                        Text("Allow Notifications")
                    }
                }

                Section(header: Text("To Do Tasks")) {
                    ForEach (listData) { item in
                        NavigationLink(destination: Text(item.task)) {
                            HStack {
                                Image(systemName: item.imageName)
                                Text(item.task)
                            }
                        }
                    }
                }
            }
            .navigationBarTitle(Text("To Do List"))
        }
    }
}

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)
        }
    }
}