Tap Gesture and List in SwiftUI

In this post, we’ll explore how to utilize tap gestures within a list. Our objective is:

Therefore, we have a list of options (or whatever you prefer) that we can enable or disable.

We will define an Option type:

struct Option: Identifiable {
    let id = UUID()
    var name: String
    var enabled: Bool
}

In the view, we declare a list that iterates over a list of options, retrieving both the index and the element for each iteration. For each individual element, we display the name. If the enable option is set to true, we also display an image of a checkmark. When the user taps on a row, the status of the property is toggled.

struct ContentView: View {
    @State var options = [Option(name: "Option1", enabled: false),
                   Option(name: "Option2", enabled: false),
                   Option(name: "Option3", enabled: false)]
    
    var body: some View {
        List(Array(options.enumerated()), id: \.offset) { index, option in
            HStack() {
                Text(option.name)
                Spacer()
                if option.enabled {
                    Image(systemName: "chevron.down")
                        .foregroundColor(.gray)
                }
            }.contentShape(Rectangle())
            .onTapGesture {
                options[index].enabled = !options[index].enabled
            }
        }
    }
}

Note that we add a contentShape to cover the entire width of the list, and it’s on this shape that the onTap gesture is applied. The options are declared as a @State because we need to change the ‘enabled’ value, and these changes must be propagated to the UI.

Note: English is not my native language, sorry for any errors. I use ChatGPT only to generate the banner of the post, the content is human.

Drag gesture with SwiftUI

In this post, we’ll learn how to use a drag gesture to check the scrolling direction of a list. The goal is to hide the tab bar and change the appearance of a floating button based on whether the list is scrolling up or down, similar to the behavior of the Gmail app. (Check the code from previous post floating-button-in-swiftui).

First step: Define the two different styles for the Floating Button:

struct FullFB: View {
    var body: some View {
        Label("write", systemImage: "pencil")
        .font(.title.weight(.semibold))
        .padding()
        .background(.pink)
        .foregroundStyle(.white)
        .clipShape(RoundedRectangle(cornerSize: CGSize(width: 40, height: 40)))
        .shadow(radius: 4, x: 0, y: 4)
    }
}

struct SmallFB: View {
    var body: some View {
        Image(systemName: "pencil")
        .font(.title.weight(.semibold))
        .padding()
        .background(.pink)
        .foregroundStyle(.white)
        .clipShape(Circle())
        .shadow(radius: 4, x: 0, y: 4)
    }
}

The code should be self-explanatory: the first style is a rounded rectangle button featuring both text and an icon, while the second style is a circular button with only an icon.

Now take a look to the list:

List(initList(), id:\.self) { mail in
                        HStack {
                            Text(mail)
                            Spacer()
                        }
                    }
                    .gesture(
                        DragGesture().onChanged { value in
                            if value.translation.height > 0{
                                tabbarVisibility = .visible
                            } else {
                                tabbarVisibility = .hidden
                            }
                        }
                    ).toolbar(tabbarVisibility, for: .tabBar)

We display a list of text, but we also attach a gesture to the list. When the DragGesture changes its value, we check the translation. If it’s greater than zero, the list is scrolling up, and we set the tab bar visibility to visible. In any other case, it’s set to hidden.

The complete code is:

struct ContentView: View {
    @State var tabbarVisibility: Visibility = .visible
    var body: some View {
        TabView {
            NavigationStack {
                ZStack {
                    List(initList(), id:\.self) { mail in
                        HStack {
                            Text(mail)
                            Spacer()
                        }
                    }
                    .gesture(
                        DragGesture().onChanged { value in
                            if value.translation.height > 0{
                                tabbarVisibility = .visible
                            } else {
                                tabbarVisibility = .hidden
                            }
                        }
                    ).toolbar(tabbarVisibility, for: .tabBar)
                    .padding()
                    
                    VStack {
                        Spacer()
                        HStack {
                            Spacer()
                            NavigationLink { EmptyView()} label: {
                                if tabbarVisibility == .visible {
                                    FullFB()
                                } else {
                                    SmallFB()
                                }
                                                            
                            }.padding()
                        }
                    }.padding()
                }
            }.tabItem {
                Label("Emails", systemImage: "envelope")
            }
            Text("Second tab")
            .tabItem {
                Label("Video", systemImage: "video")
            }
        }
        
    }
    func initList() -> [String] {
        var emails = [String]()
        for i in 1000000...1000100 {
            emails.append("Email " + String(i))
        }
        return emails
    }
}

The tabbarvisibility is also used to determine which type of floating button to display.

Towards coding and beyond… If you’d like to stay updated with the latest content from my blog, consider subscribing to my weekly newsletter.

Note: English is not my native language, sorry for any errors. I use ChatGPT only to generate the banner of the post, the content is human.

Floating button in SwiftUI

The Apple User Interface Guidelines don’t mention floating buttons, and there is no native component for them. However, some applications, such as Twitter and the Gmail client, do use them.

In this post, we’ll explore how to create a floating button, taking the Gmail app client as our example.

We aim to create this:

Therefore, we need to create a TabView. Inside the first tab, we’ll use a ZStack that contains both a list and a button.

struct ContentView: View {
    var body: some View {
        TabView {
            NavigationStack {
                ZStack {
                    List(initList(), id:\.self) { mail in
                        Text(mail)
                    }
                    VStack {
                        Spacer()
                        HStack {
                            Spacer()
                            NavigationLink { EmptyView()} label:{
                                Image(systemName: "pencil")
                                    .font(.title.weight(.semibold))
                                    .padding()
                                    .background(.pink)
                                    .foregroundStyle(.white)
                                    .clipShape(Circle())
                                    .shadow(radius: 4, x: 0, y: 4)
                            }.padding()          
                        }
                    }.padding()
                }
            }.tabItem {
                Label("Emails", systemImage: "envelope")
            }
            Text("Second tab")
                .tabItem {
                    Label("Video", systemImage: "video")
                }
        }
    }
    func initList() -> [String] {
        var emails = [String]()
        for i in 1000000...1000100 {
            emails.append("Email " + String(i))
        }
        return emails
    }
}

The list is initialized with the initList utility function. Within the ZStack, we display the list of emails and a navigation link represented by a button. The button is placed within a VStack accompanied by a Spacer, pushing it to the bottom. Additionally, it is nested inside an HStack with another Spacer, positioning it on the right side.

Tapping on the button we move on an EmptyView.

Feel free to experiment with the code—it’s the best way to learn and discover new possibilities. Dive in and have fun!

If you’d like to stay updated with the latest content from my blog, consider subscribing to my weekly newsletter.

AppStorage in SwiftUI (Form episode II°)

In this post, we’ll explore how to use AppStorage. Commonly, UserDefaults is employed to save small amounts of data, such as user preferences in an app. However, it comes with certain limitations:

  • The data is saved in a single .plist file, indicating that having a large file is not the optimal solution. On Apple TV, there is a fixed limit of 1 MB, but it’s recommended to keep the file size smaller, particularly if these default values are loaded at app startup for speed considerations.
  • The data is stored in plaintext, so it’s NOT suitable for sensitive information.
  • The permitted types are: Data, String, Date, Bool, Int, Double, Float, Array, Dictionary, and URL.

What is AppStorage? It’s a wrapper for UserDefaults that allows us to save and load data from UserDefaults without having to directly call it.

Take a look at a short example:

struct ContentView: View {
    @AppStorage("enabled") private var enabled = false
    @AppStorage("nickname") private var nickname = ""
    
    var body: some View {
        VStack {
            Form {
                TextField("Nickname", text: $nickname)
                Toggle(isOn: $enabled) {
                    Text("Enable")
                }
            }
        }
        .padding()
    }
}

So, we declare the variable with @AppStorage and use it similarly to how we use @State variables. The primary difference is that, in this case, the changes are automatically saved (and loaded).

Simple and powerful. That’all.

Form in Swiftui (Episode I°)

Many times in an app, we need to display a set number of static information items. For example, consider a user profile. While our initial instinct might be to use a list, it’s not always the best choice. Lists are typically designed for displaying an undefined number of elements. In cases where we have a fixed number of information pieces to show, it’s more appropriate to use a Form to achieve a structured layout like this:

The code is:

struct ContentView: View {
    @State var second: String = ""
    @State var enabled: Bool = true
    
    var body: some View {
        VStack {
            Form {
                HStack {
                    Spacer()
                    Image(systemName: "person.circle")
                        .resizable()
                        .frame(width: 100, height: 100)
                    Spacer()
                }
                Text("Name")
                TextField("Last Name", text: $second)
                Text("Address")
                Toggle(isOn: $enabled) {
                    Text("Enabled")
                }
                DatePicker(selection:.constant(Date()), displayedComponents: [.date], label: { Text("Birth date") })
            }
        }
    }
}

In a Form, we have the flexibility to incorporate various types of objects. For example, you can include an image (within an HStack), a read-only text, a text field, a toggle switch, and a date picker. You can even add buttons and more.

That’s all for this episode. In the next one, we’ll learn how to save the information displayed in the form.