All articles
iOS18 min read

Building a Smooth Sliding Sidebar Menu a.k.a Drawer in SwiftUI (No Third-Party Libraries)

Level up your iOS skills by building a professional sidebar menu from scratch with complete control

K
Karan Pal
Author
Image generated by AI
Image generated by AI

🤔 The Problem with Generic Navigation Solutions

I once lost half a day hacking around a drawer library’s weird edge-swipe bug. It was one of those “this should’ve taken 5 minutes” moments that ended in “why am I even using this library?”

And that’s the thing — most third-party navigation libraries get you 80% of the way there. But that last 20%? Feels like wrestling with someone else’s code and hoping it doesn’t break everything else in your app.

The truth is: SwiftUI makes custom navigation elegant. With a few state variables, clean animations, and some gesture handling, you can build a sidebar that’s fast, smooth, and entirely under your control.

What You’ll Build

A sleek sidebar that:

No black boxes. No “wait, where does this transition come from?” Just pure SwiftUI — clean, declarative, and easy to understand.

Let’s get started 🚀

🏗️ Understanding the SwiftUI Approach

Before we dive into the code, let’s talk mindset. SwiftUI isn’t UIKit. We don’t push and pop views like it’s a to-do list — we declare what the UI should look like based on state, and SwiftUI handles the how.

✨ The Magic of State-Driven Design

In UIKit, a sliding sidebar usually means view controller containment, frame math gymnastics, and imperatively managed animations. With SwiftUI, we flip the script.

All we need to say is:

“When _isOpen_ is true, show the sidebar.” And SwiftUI just... does it.

No more juggling animation states or delegating logic across three files. Just clean, predictable UI that reacts to state like it’s supposed to.

🧱 Our Architecture Strategy

We’re going with a simple, powerful setup:

SwiftUI’s gesture system plays beautifully with animations. If a user drags halfway and lets go, we’ll check the gesture velocity and animate to the right end state. No janky half-open menus.

🎯 Gesture + Animation Harmony

In UIKit, you’d be tracking touch locations, syncing them with timers, and praying Auto Layout doesn’t explode.

In SwiftUI? ou just combine DragGesture with a few animation modifiers, and boom — the sidebar follows your finger in real time, then snaps into place with a clean, responsive finish.

This whole system runs on state management — so if you’re already comfortable with @State, @Binding, and friends, you’re in great shape. If you need a refresher, this complete guide to SwiftUI state management (https://medium.com/swift-pal/mastering-swiftui-state-management-state-vs-binding-vs-observedobject-vs-stateobject-2025-e1f751aa038d) will get you up to speed.

Now let’s start building 💪

**Mastering SwiftUI State Management in 2025: @State, @Binding, @ObservedObject, @StateObject — Plus…** _Confused between @State, @Binding, @ObservedObject, @StateObject, @Observable and @Bindable in SwiftUI? You’re not…_medium.comhttps://medium.com/swift-pal/mastering-swiftui-state-management-state-vs-binding-vs-observedobject-vs-stateobject-2025-e1f751aa038d

🔧 Building the Core Structure

Alright, time to get our hands dirty 🛠️ We’ll start by laying down the foundation: a container view that holds everything — the sidebar, the main content, and the state that controls their interaction.

Think of it as the stage where all the sliding, swiping, and snapping happens.

🧱 Setting Up the Basic Structure

Let’s create our main sidebar container. This view will handle:

Here’s what the basic structure looks like:

import SwiftU

struct SlidingSidebarView<Content: View, Sidebar: View>: View {
    @State private var isOpen = false
    @State private var dragOffset: CGFloat = 0
    
    let content: Content
    let sidebar: Sidebar
    let sidebarWidth: CGFloat
    
    init(sidebarWidth: CGFloat = 280, @ViewBuilder content: () -> Content, @ViewBuilder sidebar: () -> Sidebar) {
        self.sidebarWidth = sidebarWidth
        self.content = content()
        self.sidebar = sidebar()
    }
    
    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                // Main content area
                content
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .offset(x: isOpen ? sidebarWidth + dragOffset : dragOffset)
                
                // Sidebar
                sidebar
                    .frame(width: sidebarWidth)
                    .offset(x: isOpen ? dragOffset : -sidebarWidth + dragOffset)
            }
        }
    }
}

🧩 Breaking Down the Structure

Notice how we’re using generics (Content: View, Sidebar: View) to keep this component super flexible. You can pass in any SwiftUI views — no weird base classes or rigid layouts required. It’s your design, your way.

The GeometryReader gives us access to the full screen size, which is perfect for responsive behavior. And by using a ZStack with .leading alignment, we make sure the sidebar starts right where it should — tucked off-screen to the left.

🪄 The Offset Magic

This is where SwiftUI really shows off. Instead of hard-coding frames and juggling layout math, we let .offset() handle the view positioning for us:

That dragOffset state? It’s our secret weapon. It lets the sidebar follow the user’s finger and makes the gesture feel natural while preserving clean logic for open vs closed state.

🚀 Using Our Sidebar

Here’s how clean it looks in action:

import SwiftUI

struct ContentView: View {
    var body: some View {
        SlidingSidebarView {
            // Main content
            VStack {
                Text("Main Content Area")
                Button("Toggle Sidebar") {
                    // We'll add this functionality next
                }
            }
        } sidebar: {
            // Sidebar content
            VStack(alignment: .leading, spacing: 20) {
                Text("Sidebar Menu")
                    .font(.headline)
                Button("Home") { }
                Button("Settings") { }
                Button("Profile") { }
            }
            .padding()
            .background(Color.gray.opacity(0.1))
        }
    }
}

Looking pretty clean already 🎉 But right now, it’s just a static layout — no movement, no magic.

Next up: we’ll breathe life into it with smooth animations and responsive gestures that’ll make your users smile (and maybe even swipe just for fun).

✨ Implementing Smooth Animations

Now for the fun part — making our sidebar glide like butter 🧈

SwiftUI’s animation system is one of its superpowers. It turns simple state changes into delightful transitions without needing to micromanage frames or timing.

🪄 Adding Animation Magic

Let’s enhance the sidebar with smooth animations and interactive controls. We’ll wire up a toggle function, apply animation modifiers, and give the main content view full control over opening and closing the menu:

import SwiftUI

struct SlidingSidebarView<Content: View, Sidebar: View>: View {
    @State private var isOpen = false
    @State private var dragOffset: CGFloat = 0
    
    let content: (Binding<Bool>) -> Content  // Pass binding to content
    let sidebar: Sidebar
    let sidebarWidth: CGFloat
    
    // Animation configuration
    private let animationDuration: Double = 0.3
    private let animationCurve = Animation.easeInOut(duration: 0.3)
    
    init(sidebarWidth: CGFloat = 280, @ViewBuilder content: @escaping (Binding<Bool>) -> Content, @ViewBuilder sidebar: () -> Sidebar) {
        self.sidebarWidth = sidebarWidth
        self.content = content
        self.sidebar = sidebar()
    }
    
    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                // Main content area
                content($isOpen)  // Pass the binding
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .offset(x: isOpen ? sidebarWidth + dragOffset : dragOffset)
                    .animation(animationCurve, value: isOpen)
                
                // Sidebar
                sidebar
                    .frame(width: sidebarWidth)
                    .offset(x: isOpen ? dragOffset : -sidebarWidth + dragOffset)
                    .animation(animationCurve, value: isOpen)
            }
        }
    }
}

🧈 The Animation Secret Sauce

Here’s what gives the sidebar that silky-smooth feel:

🧪 Usage Example

Now your content view can toggle the sidebar with ease — no hacks, no global state:

struct ContentView: View {
    var body: some View {
        SlidingSidebarView { isOpen in
            VStack {
                Text("Main Content Area")
                    .font(.title)
                
                Button("Toggle Sidebar") {
                    isOpen.wrappedValue.toggle()
                }
                .buttonStyle(.borderedProminent)
            }
        } sidebar: {
            VStack(alignment: .leading, spacing: 20) {
                Text("Sidebar Menu")
                    .font(.headline)
                Button("Home") { }
                Button("Settings") { }
                Button("Profile") { }
            }
            .padding()
            .background(Color.secondary.opacity(0.1))
        }
    }
}

🎯 Why This Animation Approach Works

The magic here is that we’re not manually animating anything. No timers, no CADisplayLink, no frame math. Instead, we’re telling SwiftUI:

“Hey, when _isOpen_ changes, animate the views from their current position to their new state.”

SwiftUI takes care of the rest — calculating all the in-between values for a perfectly smooth transition.

If you’re new to SwiftUI animations, this is a great example of its declarative power in action. Want to go deeper into how this works under the hood? Check out this beginner-friendly guide on animation fundamentals: https://medium.com/swift-pal/swiftui-animations-for-beginners-learn-with-simple-examples-2025-edition-76eb224eb633

Next up, we’ll add the pan gestures that’ll make users feel like they’re directly manipulating the sidebar with their finger. 🤚

**SwiftUI Animations for Beginners: Learn with Simple Examples (2025 Edition)** _Animations in SwiftUI feel like magic 🪄 — until your view just snaps instead of fades. In this beginner-friendly…_medium.comhttps://medium.com/swift-pal/swiftui-animations-for-beginners-learn-with-simple-examples-2025-edition-76eb224eb633

🎨 Making the Sidebar Look Beautiful

Before we jump into gestures and interactivity, let’s show our sidebar a little love. Function is great — but form? That’s what makes it feel native, polished, and intentional.

📱 Designing for iOS Standards

Apple’s Human Interface Guidelines are all about clarity, consistency, and thoughtful use of space. A good sidebar should:

We’re not just tossing buttons in a box — we’re designing something that feels right.

🧑‍🎨 Our Enhanced Sidebar Design

Time to give our basic sidebar a proper style upgrade. We’ll add some padding, spacing, icons, and accessibility-friendly system colors to make it shine:

struct ContentView: View {
    var body: some View {
        SlidingSidebarView { isOpen in
            VStack {
                Text("Main Content Area")
                    .font(.title)
                
                Button("Toggle Sidebar") {
                    isOpen.wrappedValue.toggle()
                }
                .buttonStyle(.borderedProminent)
            }
        } sidebar: {
            VStack(alignment: .leading, spacing: 0) {
                // Header Section
                VStack(alignment: .leading, spacing: 8) {
                    Image(systemName: "person.circle.fill")
                        .font(.system(size: 50))
                        .foregroundColor(.blue)
                    
                    Text("John Doe")
                        .font(.title2)
                        .fontWeight(.semibold)
                    
                    Text("john.doe@example.com")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
                .padding(.top, 60)
                .padding(.horizontal, 20)
                .padding(.bottom, 30)
                
                // Menu Items
                VStack(spacing: 0) {
                    SidebarMenuItem(icon: "house.fill", title: "Home")
                    SidebarMenuItem(icon: "gear", title: "Settings")
                    SidebarMenuItem(icon: "person.fill", title: "Profile")
                    SidebarMenuItem(icon: "heart.fill", title: "Favorites")
                    SidebarMenuItem(icon: "bell.fill", title: "Notifications")
                }
                
                Spacer()
                
                // Bottom Section
                VStack(spacing: 0) {
                    Divider()
                        .padding(.horizontal, 20)
                    
                    SidebarMenuItem(icon: "questionmark.circle", title: "Help & Support")
                    SidebarMenuItem(icon: "rectangle.portrait.and.arrow.right", title: "Sign Out")
                        .foregroundColor(.red)
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
            .background(Color(.systemBackground))
            .overlay(
                // Right border for depth
                Rectangle()
                    .frame(width: 0.5)
                    .foregroundColor(Color(.separator))
                    .frame(maxWidth: .infinity, alignment: .trailing)
            )
        }
    }
}

// Custom Menu Item Component
struct SidebarMenuItem: View {
    let icon: String
    let title: String
    
    var body: some View {
        HStack(spacing: 16) {
            Image(systemName: icon)
                .font(.system(size: 20))
                .foregroundColor(.blue)
                .frame(width: 24, height: 24)
            
            Text(title)
                .font(.body)
                .fontWeight(.medium)
            
            Spacer()
        }
        .padding(.horizontal, 20)
        .padding(.vertical, 16)
        .contentShape(Rectangle())
        .onTapGesture {
            // Handle menu item tap
            print("Tapped \(title)")
        }
    }
}

🎯 Design Decisions That Matter

Every detail in this sidebar layout is doing real work — nothing’s here by accident:

✨ Pro Polish Tips

🧩 Customization Options

Want to match your app’s branding? Just swap .foregroundColor(.blue) with your primary brand color, or drop in custom backgrounds for different sections. The structure is flexible enough to plug into any design system without friction.

Now we’ve got a sidebar that doesn’t just function like iOS — it feels like iOS. Let’s take it one step further and make it feel amazing to use with smooth gesture interactions 🚀

🤚 Adding Gesture Recognition

Here’s where our sidebar goes from “just another menu” to “okay… this feels legit.”

Pan gestures are what separate a basic drawer from a professional navigation experience. Users expect to drag, swipe, and tap — not just click buttons.

✨ The Magic of Direct Manipulation

Think about how good it feels to swipe open Control Center or pull down Notification Center. That physical connection between your finger and the UI creates a kind of trust — like the app is listening in real time.

We’re bringing that same level of responsiveness to our sidebar.

🧠 Complete Gesture System: Pan + Tap

We’re not stopping at just pan gestures. A modern sidebar should also close when the user taps outside of it — no extra buttons required.

Let’s add both:

Here’s how to make it feel intuitive, fluid, and just right:

struct SlidingSidebarView<Content: View, Sidebar: View>: View {
    @State private var isOpen = false
    @State private var dragOffset: CGFloat = 0

    let content: (Binding<Bool>) -> Content
    let sidebar: Sidebar
    let sidebarWidth: CGFloat

    // Animation configuration
    private let animationCurve = Animation.easeInOut(duration: 0.3)

    // Gesture configuration
    private let minimumSwipeDistance: CGFloat = 50
    private let velocityThreshold: CGFloat = 300

    init(sidebarWidth: CGFloat = 280, @ViewBuilder content: @escaping (Binding<Bool>) -> Content, @ViewBuilder sidebar: () -> Sidebar) {
        self.sidebarWidth = sidebarWidth
        self.content = content
        self.sidebar = sidebar()
    }

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                // Main content area
                content($isOpen)
                    .frame(
                        width: geometry.size.width,
                        height: geometry.size.height
                    )
                // NEW: Makes entire content area tappable, not just content elements
                    .contentShape(Rectangle())
                    .offset(x: isOpen ? sidebarWidth + dragOffset : dragOffset)
                // UPDATED: Simplified animation logic - always animate isOpen changes
                    .animation(animationCurve, value: isOpen)
                // NEW: Tap gesture on content only - closes sidebar when open
                    .onTapGesture {
                        if isOpen {
                            withAnimation(animationCurve) {
                                isOpen = false
                            }
                        }
                    }

                // Sidebar
                sidebar
                    .frame(width: sidebarWidth)
                    .offset(x: isOpen ? dragOffset : -sidebarWidth + dragOffset)
                    .animation(animationCurve, value: isOpen)
            }
            .gesture(
                DragGesture()
                    .onChanged { value in
                        let translation = value.translation.width

                        if isOpen {
                            // When open, allow dragging left to close
                            dragOffset = max(-sidebarWidth, min(0, translation))
                        } else {
                            // When closed, allow dragging right to open
                            dragOffset = max(0, min(sidebarWidth, translation))
                        }
                    }
                    .onEnded { value in
                        let translationWidth = value.translation.width
                        let velocityWidth = value.velocity.width

                        let shouldToggle = shouldCompleteGesture(
                            translation: translationWidth,
                            velocity: velocityWidth
                        )

                        withAnimation(animationCurve) {
                            dragOffset = 0
                            if shouldToggle {
                                isOpen.toggle()
                            }
                        }
                    }
            )
        }
    }

    // Smart gesture completion logic
    private func shouldCompleteGesture(translation: CGFloat, velocity: CGFloat) -> Bool {
        if isOpen {
            // When open, detect close gestures (leftward)
            let fastSwipeLeft = velocity < -velocityThreshold
            let draggedFarLeft = translation < -minimumSwipeDistance
            return fastSwipeLeft || draggedFarLeft
        } else {
            // When closed, detect open gestures (rightward)
            let fastSwipeRight = velocity > velocityThreshold
            let draggedFarRight = translation > minimumSwipeDistance
            return fastSwipeRight || draggedFarRight
        }
    }
}

🧠 Understanding SwiftUI’s Gesture Harmony

SwiftUI makes combining gestures feel almost magical — and that’s what gives this sidebar its buttery responsiveness.

🎯 Gesture Priority Perfection

No need to manually wrangle gesture conflicts — SwiftUI handles it for us. Tap and drag gestures can coexist in peace. SwiftUI’s gesture system smartly decides which one takes precedence based on movement and timing.

Go ahead — try it. The sidebar should feel like it’s physically attached to your finger, and a single tap on the main content dismisses it instantly 🎉

🔧 Making It Reusable

Our sidebar’s working beautifully — but right now, it’s tailored to a specific layout and design. Time to level up and turn it into a flexible, drop-in component that can adapt to any design system without losing that smooth, responsive feel.

🧩 The Challenge of Flexibility

It’s easy to build something that works for one screen. But reusing that same component across multiple views or apps? That’s where most implementations fall apart.

The key to great SwiftUI components is balance:

⚙️ Component Configuration Strategy

Instead of hardcoding things like width, background, or shadow — we’ll expose the most common customization points:

All with sensible defaults, so it just works out of the box — but stays flexible for power users.

💪 Enhanced Reusable Implementation

Let’s rebuild our sidebar as a fully reusable, highly configurable SwiftUI component — ready to drop into any app, design system, or UI style:

// NEW: Configuration struct for sidebar appearance and behavior
struct SidebarConfiguration {
    let width: CGFloat
    let animationDuration: Double
    let minimumSwipeDistance: CGFloat
    let velocityThreshold: CGFloat
    
    static let `default` = SidebarConfiguration(
        width: 280,
        animationDuration: 0.3,
        minimumSwipeDistance: 50,
        velocityThreshold: 300
    )
}

struct SlidingSidebar<Content: View, Sidebar: View>: View {
    // NEW: External binding support for parent view control
    @Binding var isOpen: Bool
    @State private var dragOffset: CGFloat = 0
    
    let content: Content
    let sidebar: Sidebar
    let configuration: SidebarConfiguration
    
    // NEW: Configurable initialization with multiple convenience initializers
    init(
        isOpen: Binding<Bool>,
        configuration: SidebarConfiguration = .default,
        @ViewBuilder content: () -> Content,
        @ViewBuilder sidebar: () -> Sidebar
    ) {
        self._isOpen = isOpen
        self.configuration = configuration
        self.content = content()
        self.sidebar = sidebar()
    }
    
    // NEW: Simple initializer for basic use cases
    init(
        isOpen: Binding<Bool>,
        @ViewBuilder content: () -> Content,
        @ViewBuilder sidebar: () -> Sidebar
    ) {
        self.init(isOpen: isOpen, configuration: .default, content: content, sidebar: sidebar)
    }
    
    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                // Main content area
                content
                    .frame(
                        width: geometry.size.width,
                        height: geometry.size.height
                    )
                    .contentShape(Rectangle())
                    .offset(x: isOpen ? configuration.width + dragOffset : dragOffset)
                    .animation(animationCurve, value: isOpen)
                    .onTapGesture {
                        if isOpen {
                            withAnimation(animationCurve) {
                                isOpen = false
                            }
                        }
                    }
                
                // Sidebar
                sidebar
                    .frame(width: configuration.width)
                    .offset(x: isOpen ? dragOffset : -configuration.width + dragOffset)
                    .animation(animationCurve, value: isOpen)
            }
            .gesture(
                DragGesture()
                    .onChanged { value in
                        let translation = value.translation.width
                        
                        if isOpen {
                            dragOffset = max(-configuration.width, min(0, translation))
                        } else {
                            dragOffset = max(0, min(configuration.width, translation))
                        }
                    }
                    .onEnded { value in
                        let translationWidth = value.translation.width
                        let velocityWidth = value.velocity.width
                        
                        let shouldToggle = shouldCompleteGesture(
                            translation: translationWidth,
                            velocity: velocityWidth
                        )
                        
                        withAnimation(animationCurve) {
                            dragOffset = 0
                            if shouldToggle {
                                isOpen.toggle()
                            }
                        }
                    }
            )
        }
    }
    
    // UPDATED: Uses configuration values instead of hardcoded constants
    private var animationCurve: Animation {
        .easeInOut(duration: configuration.animationDuration)
    }
    
    private func shouldCompleteGesture(translation: CGFloat, velocity: CGFloat) -> Bool {
        if isOpen {
            let fastSwipeLeft = velocity < -configuration.velocityThreshold
            let draggedFarLeft = translation < -configuration.minimumSwipeDistance
            return fastSwipeLeft || draggedFarLeft
        } else {
            let fastSwipeRight = velocity > configuration.velocityThreshold
            let draggedFarRight = translation > configuration.minimumSwipeDistance
            return fastSwipeRight || draggedFarRight
        }
    }
}

Clean Usage Examples

Now your sidebar component is incredibly flexible:

// Basic usage with default configuration
struct ContentView: View {
    @State private var sidebarOpen = false
    
    var body: some View {
        SlidingSidebar(isOpen: $sidebarOpen) {
            VStack {
                Text("Main Content")
                Button("Toggle Sidebar") {
                    sidebarOpen.toggle()
                }
            }
        } sidebar: {
            MySidebarMenuView()
        }
    }
}

// Custom configuration for different design needs
struct CustomContentView: View {
    @State private var sidebarOpen = false
    
    let customConfig = SidebarConfiguration(
        width: 320,
        animationDuration: 0.4,
        minimumSwipeDistance: 80,
        velocityThreshold: 500
    )
    
    var body: some View {
        SlidingSidebar(isOpen: $sidebarOpen, configuration: customConfig) {
            MyMainContentView()
        } sidebar: {
            MyCustomSidebarView()
        }
    }
}

✅ Why This Approach Works

This pattern strikes the sweet spot between ease of use and deep customization — just like Apple’s own SwiftUI views.

It’s the SwiftUI way: simple by default, powerful when needed.

Want to go deeper into reusable view architecture and best practices? Check out our complete guide to building scalable SwiftUI components: https://medium.com/swift-pal/how-to-build-reusable-swiftui-components-a-complete-2025-guide-a792c2d4b9ce

**How to Build Reusable SwiftUI Components: A Complete 2025 Guide** _Master the art of DRY code in SwiftUI. Build once, use everywhere — complete with state management, styling best…_medium.comhttps://medium.com/swift-pal/how-to-build-reusable-swiftui-components-a-complete-2025-guide-a792c2d4b9ce

🎉 Wrapping Up

Congratulations! You’ve just built a professional-grade sliding sidebar menu — no hacks, no black boxes, no weird library quirks. Just clean SwiftUI code you fully understand and control.

More importantly, you now own this component. You can tweak it, theme it, and scale it to whatever your app needs — without fear of breaking someone else’s abstraction.

✅ What You’ve Accomplished

From a simple container view, you’ve created a sidebar that:

💡 The Power of Understanding

You didn’t just drop in a dependency — you built it. You now understand the underlying mechanics: gesture coordination, state-driven layout, and animation timing. This kind of insight makes you faster, more confident, and way more effective as a SwiftUI developer.

✨ Advanced Animation Techniques

Want to level up even further? Check out this in-depth guide to SwiftUI animation techniques — covering spring physics, matchedGeometryEffect, timeline-driven animations, and more: https://medium.com/swift-pal/advanced-animations-in-swiftui-matchedgeometryeffect-timelineview-phaseanimator-beyond-2025-da8876b7b0b9

**Advanced Animations in SwiftUI: matchedGeometryEffect, TimelineView, PhaseAnimator & Beyond (2025…** _In this deep dive, we’re exploring SwiftUI’s most advanced animation tools — from layout-matching transitions and…_medium.comhttps://medium.com/swift-pal/advanced-animations-in-swiftui-matchedgeometryeffect-timelineview-phaseanimator-beyond-2025-da8876b7b0b9

🧠 Real-World Considerations

In a production setting, you might want to push this component even further. Here are a few enhancements worth exploring:

🚀 Your Next Steps

Take this sidebar and make it yours. Try:

The structure you’ve built is rock solid — and now it’s ready for whatever enhancements your app (or your designer 😅) throws at it.

Remember: building SwiftUI components from scratch like this sharpens your skills more than any library ever could. Every gesture, state variable, and animation teaches you something deeper about how SwiftUI really works.

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading