Skip to main content

Creating a liquid glass custom component

·410 words·2 mins
Author
Léo Marcotte

Context
#

The code
#

GlassMenuItem.swift
#

import SwiftUI

public struct GlassMenuItem: Identifiable {

    // MARK: Subtypes

    public enum LeadingItem {
        case symbol(String)
        case image(String)
    }

    // MARK: Properties

    let title: String
    let leadingItem: LeadingItem
    let action: () -> Void

    // MARK: Computed properties

    public var id: String {
        title
    }

    // MARK: Init

    public init(
        leadingItem: LeadingItem,
        title: String,
        action: @escaping () -> Void
    ) {
        self.leadingItem = leadingItem
        self.title = title
        self.action = action
    }
}

struct GlassMenuItemView: View {

    // MARK: - Properties

    private let namespace: Namespace.ID
    let item: GlassMenuItem
    @Binding private var isExpanded: Bool

    // MARK: - Init

    public init(
        namespace: Namespace.ID,
        item: GlassMenuItem,
        isExpanded: Binding<Bool>
    ) {
        self.namespace = namespace
        self.item = item
        self._isExpanded = isExpanded
    }

    // MARK: - Body

    public var body: some View {
        HStack(spacing: 4) {
            Spacer()
            if isExpanded {
                Text(item.title)
                    .font(.system(size: 14, weight: .bold, design: .rounded))
                    .padding()
                    .glassEffect()
                    .glassEffectID(item.id, in: namespace)
            }
            Group {
                switch item.leadingItem {
                    case .symbol(let name):
                    Image(systemName: name)
                        .frame(width: 60.0, height: 60.0)
                        .font(.system(size: 28, weight: .bold))
                case .image(let name):
                    Image(name)
                        .frame(width: 60.0, height: 60.0)
                        .font(.system(size: 28, weight: .bold))
                }
            }
            .glassEffect()
            .glassEffectID(item.id, in: namespace)
            .contentShape(Circle())
        }
        .bold()
        .foregroundStyle(.primary)
        .contentShape(Rectangle())
        .onTapGesture {
            withAnimation {
                isExpanded.toggle()
            }
            item.action()
        }
    }
}

GlassExpandableMenu.swift
#

import SwiftUI

public struct GlassExpandableMenu: View {

    // MARK: - Namespace

    @Namespace private var namespace

    // MARK: - Properties

    @State private var isExpanded: Bool = false

    private let items: [GlassMenuItem]

    // MARK: - Init

    public init(items: [GlassMenuItem]) {
        self.items = items
    }

    // MARK: - Body

    public var body: some View {
        VStack {
            Spacer()
            HStack {
                Spacer()
                GlassEffectContainer(spacing: 24) {
                    VStack(spacing: 24) {
                        if isExpanded {
                            ForEach(items) { item in
                                GlassMenuItemView(
                                    namespace: namespace,
                                    item: item,
                                    isExpanded: $isExpanded
                                )
                            }
                        }

                        HStack {
                            Spacer()

                            Image(systemName: "plus")
                                .foregroundStyle(isExpanded ? .white : .primary)
                                .font(.system(size: 28, weight: .bold))
                                .frame(width: 60, height: 60)
                                .rotationEffect(.degrees(isExpanded ? 45 : 0))
                                .glassEffect(.regular.tint(isExpanded ? Color.red.opacity(0.2) : Color.systemBackground).interactive())
                                .glassEffectID("plus", in: namespace)
                                .contentShape(Circle())
                                .onTapGesture {
                                    withAnimation {
                                        isExpanded.toggle()
                                    }
                                }
                        }
                    }
                    .ds_padding(.trailing, .twentyFour)
                    .ds_padding(.bottom, .thirtyTwo)
                }
            }
        }
    }
}

Demo
#

Usage
#

#Preview {
    GlassExpandableMenu(
        items: [
            .init(
                leadingItem: .symbol("1.square"),
                title: "One",
                action: {
                    print("1")
                }
            ),
            .init(
                leadingItem: .symbol("2.square"),
                title: "Two",
                action: {
                    print("2")
                }
            ),
            .init(
                leadingItem: .symbol("3.square"),
                title: "Three",
                action: {
                    print("3")
                }
            ),
            .init(
                leadingItem: .symbol("4.square"),
                title: "Four",
                action: {
                    print("4")
                }
            ),
            .init(
                leadingItem: .symbol("5.square"),
                title: "Five",
                action: {
                    print("5")
                }
            ),
            .init(
                leadingItem: .symbol("6.square"),
                title: "Six",
                action: {
                    print("6")
                }
            ),
        ]
    )
}

Result
#

image