Skip to content

Instantly share code, notes, and snippets.

@metasidd
Last active June 16, 2024 15:33
Show Gist options
  • Save metasidd/a39198e70632c40e3d4fb444025dcc74 to your computer and use it in GitHub Desktop.
Save metasidd/a39198e70632c40e3d4fb444025dcc74 to your computer and use it in GitHub Desktop.
Pill Buttons - Mail App iOS 18 - SwiftUI
//
// PillButtons.swift
//
// Created by Siddhant Mehta on 6/11/24
// Twitter: @metasidd
//
// Feel free to reuse, or remix.
import SwiftUI
struct Pill: Identifiable {
let id: Int
let text: String
let systemIcon: String
let color: Color
}
struct PillButtonRowView: View {
private let pills: [Pill] = [
Pill(id: 1, text: "Priority", systemIcon: "person", color: Color.blue),
Pill(id: 2, text: "Shopping", systemIcon: "cart", color: Color.green),
Pill(id: 3, text: "Social", systemIcon: "text.bubble", color: Color.indigo),
Pill(id: 4, text: "Promotions", systemIcon: "horn", color: Color.red)
]
@State private var selectedPillID: Int = 1
var body: some View {
HStack(spacing: 8) {
ForEach(pills) { pill in
PillButton(pill: pill, isSelected: selectedPillID == pill.id) {
withAnimation(.bouncy) {
self.selectedPillID = pill.id
}
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
}
}
struct PillButton: View {
let pill: Pill
let isSelected: Bool
let onSelect: () -> Void
var body: some View {
Button(action: {
onSelect()
}) {
HStack {
iconView
textView
}
.frame(maxWidth: isSelected ? .infinity : nil, alignment: .center)
.font(.body)
.foregroundStyle(pillForeground)
.padding(.horizontal, 16)
.padding(.vertical, 16)
.background(pillBackground)
.clipShape(
RoundedRectangle(cornerRadius: 20, style: .continuous)
)
}
.buttonStyle(PlainButtonStyle()) // To remove the default button style
}
// MARK: - Properties
private var pillIcon: String {
isSelected ? "\(pill.systemIcon).fill" : pill.systemIcon
}
private var pillBackground: Color {
isSelected ? pill.color : Color(UIColor.secondarySystemBackground)
}
private var pillForeground: Color {
isSelected ? Color(UIColor.white) : Color(UIColor.label)
}
// MARK: - Subviews
@ViewBuilder
private var iconView: some View {
// Intentionally layering two icons to remove any ghosting effect, or lagging views
ZStack {
Image(systemName: "\(pill.systemIcon).fill")
.opacity(isSelected ? 1 : 0)
Image(systemName: pill.systemIcon)
.opacity(isSelected ? 0 : 1)
}
}
@ViewBuilder
private var textView: some View {
if isSelected {
Text(pill.text)
.offset(x: isSelected ? 0 : 100)
.frame(maxWidth: isSelected ? nil : 0)
.transition(.opacity)
}
}
}
#Preview {
PillButtonRowView()
}
@Codelaby
Copy link

Great job
Small variation:

using UUID
selected with environment .selected(bool) @Environment(\.isSelected) private var isSelected: Bool
symbolVariant
colorized from .borderled and tinted variant for pastelized style

private struct isSelectedKey: EnvironmentKey {
    static let defaultValue: Bool = false
}
 
extension EnvironmentValues {
    var isSelected: Bool {
        get { self[isSelectedKey.self] }
        set { self[isSelectedKey.self] = newValue }
    }
}
 
extension View {
    func isSelected(_ selected: Bool) -> some View {
        environment(\.isSelected, selected)
    }
}

struct Pill: Identifiable {
    let id: UUID = UUID()
    let text: LocalizedStringKey
    let systemIcon: String
    let color: Color
}

struct PillButton: View {
    
    @Environment(\.isSelected) private var isSelected: Bool

    let pill: Pill
    let onSelect: () -> Void
    
    var body: some View {
        
        Button(action: {
            onSelect()

        }, label: {
            HStack {
                iconView
                textView
            }
            .font(.body)
            .frame(maxWidth: isSelected ? .infinity : nil, alignment: .center)

            .padding(.vertical, 8)
        })
        .background(pillBackground, in: .rect(cornerRadius: 20))
        //.tint(pillBackground)
        .clipShape(.rect(cornerRadius: 20))
        .buttonStyle(BorderedButtonStyle())
        .foregroundStyle(pillForeground)
        .allowsHitTesting(!isSelected)

    }
    
    // MARK: - Properties
    
    private var pillBackground: Color {
        isSelected ? Color.accentColor : .clear
        //isSelected ? Color.accentColor : .gray
    }
    
    private var pillForeground: Color {
        isSelected ? Color.white : .primary
        //isSelected ? Color.accentColor : .primary
    }
    
    // MARK: - Subviews
    @ViewBuilder
    private var iconView: some View {
        Image(systemName: pill.systemIcon)
            .symbolVariant(isSelected ? .fill : .none)
    }
    
    @ViewBuilder
    private var textView: some View {
        if isSelected {
            Text(pill.text)
                .offset(x: isSelected ? 0 : 100)
                .frame(maxWidth: isSelected ? nil : 0)
                .transition(.opacity)
                .lineLimit(1)
        }
    }
}

Usage

#Preview {
    struct PreviewWrapper: View {
        
        private let pills: [Pill] = [
            Pill(text: "Priority", systemIcon: "person", color: Color.blue),
            Pill(text: "Shopping", systemIcon: "cart", color: Color.green),
            Pill(text: "Social", systemIcon: "text.bubble", color: Color.indigo),
            Pill(text: "Promotions", systemIcon: "horn", color: Color.red)
        ]
        
        // Inicializar selectedPillID con el id del primer elemento de la lista
        @State private var selectedPillID: UUID
        
        init() {
            // Establecer el valor inicial de selectedPillID al id del primer elemento de pills
            _selectedPillID = State(initialValue: pills.first?.id ?? UUID())
        }
        
        var body: some View {
            VStack {
                
                Text("Pill Button style Email IOS 18").font(.largeTitle.bold()).multilineTextAlignment(.center)
                
                Spacer()
                HStack(spacing: 8) {
                    ForEach(pills) { pill in
                        PillButton(pill: pill) {
                            withAnimation(.bouncy) {
                                self.selectedPillID = pill.id
                            }
                            print("click:", pill.text)
                        }
                        .accentColor(pill.color)
                        .isSelected(selectedPillID == pill.id)
                    }
                }
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(8)
             
                Spacer()
                
                Text("Adaption code by: @metasidd")
            }
        }
    }
    
    return PreviewWrapper()
}

Pastelized variant

        //.background(pillBackground, in: .rect(cornerRadius: 20))
        .tint(pillBackground)

    private var pillBackground: Color {
        //isSelected ? Color.accentColor : .clear
        isSelected ? Color.accentColor : .gray
    }
    
    private var pillForeground: Color {
        //isSelected ? Color.white : .primary
        isSelected ? Color.accentColor : .primary
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment