
Daily SwiftUI
Xcode 26 - iOS 18.0
Custom Tab Bar with Glass Effect
Difficulty: Intermediate | Topics: Custom UI, UIKit Bridge, Glass Morphism
Standard tab bars often limit design creativity. This experiment demonstrates how to build a highly customized, floating tab bar that uses a "Glassmorphism" visual style.
What you'll learn:
- Creating custom tab bars with glass/blur effects
- Bridging UIKit and SwiftUI with
UIViewRepresentable - Implementing smooth icon transitions with
symbolVariant - Managing state with
@Bindingacross view hierarchies
The Main View Implementation
The implementation below uses a ZStack to overlay the custom tab interface at the bottom of the screen. It manages the active tab state manually, allowing for smooth animations and custom SFSymbol transitions (switching from line icons to filled icons upon selection).
// // ContentView.swift // swiftui-custom-tabbar // // Created by Georgius Dewantama on 21/11/25. // import SwiftUI // MARK: - Tab Items enum CustomTab: String, CaseIterable { case home = "Home" case notiification = "Notifications" case settings = "Settings" var symbol: String { switch self { case .home: return "house" case .notiification: return "bell" case .settings: return "gearshape" } } var actionSymbol: String { switch self { case .home: return "plus" case .notiification: return "tray.full.fill" case .settings: return "cloud.moon.fill" } } var index: Int { Self.allCases.firstIndex(of: self) ?? 0 } } struct ContentView: View { @State var activeTab: CustomTab = .home var body: some View { TabView(selection: $activeTab) { Tab.init(value: .home) { ScrollView(.vertical) { VStack(spacing: 10) { ForEach(1...50, id: \.self) { _ in RoundedRectangle(cornerRadius: 20) .fill(.gray.gradient) .frame(height: 50) } } .padding(15) } .safeAreaBar(edge: .bottom, spacing: 0, content: { Text(".") .blendMode(.destinationOver) .frame(height: 55) }) .toolbarVisibility(.hidden, for: .tabBar) } Tab.init(value: .notiification) { Text("NOTIFICATIONS") .toolbarVisibility(.hidden, for: .tabBar) } Tab.init(value: .settings) { Text("SETTINGS") .toolbarVisibility(.hidden, for: .tabBar) } } .safeAreaInset(edge: .bottom) { CustomTabBarView() .padding(.horizontal, 20) } } @ViewBuilder func CustomTabBarView() -> some View { GlassEffectContainer(spacing: 10) { HStack(spacing: 10) { GeometryReader { CustomTabBar(size: $0.size, activeTab: $activeTab) { tab in VStack(spacing: 3) { Image(systemName: tab.symbol) .font(.title3) Text(tab.rawValue) .font(.system(size: 10)) .fontWeight(.medium) } .symbolVariant(.fill) .frame(maxWidth: .infinity) } .glassEffect(.regular.interactive(), in: .capsule) } ZStack { ForEach(CustomTab.allCases, id: \.rawValue) { tab in Image(systemName: tab.actionSymbol) .font(.system(size: 22, weight: .medium)) .blurFade(activeTab == tab) } } .frame(width: 55, height: 55) .glassEffect(.regular.interactive(), in: .capsule) .animation(.smooth(duration: 0.55, extraBounce: 0), value: activeTab) } } .frame(height: 55) } } // MARK: - Blur Fade IN / OUT extension View { @ViewBuilder func blurFade(_ status: Bool) -> some View { self .compositingGroup() .blur(radius: status ? 0 : 10) .opacity(status ? 1 : 0) } } #Preview { ContentView() }
Bridging UIKit and SwiftUI
The component below, CustomTabBar, is a UIViewRepresentable wrapper. While SwiftUI is powerful, sometimes UIKit components (like UISegmentedControl) offer specific behaviors we want to leverage.
This code bridges the two worlds: it takes purely SwiftUI views defined in the closure, converts them into images using ImageRenderer, and sets them as segments in a standard UIKit control. This allows for precise touch handling while keeping the design declaratively in SwiftUI.
Key Technique: Converting SwiftUI views to images using ImageRenderer allows us to use them in UIKit controls seamlessly!
// // CustomTabBar.swift // swiftui-custom-tabbar // // Created by Georgius Dewantama on 24/11/25. // import SwiftUI struct CustomTabBar<TabItemView: View>: UIViewRepresentable { var size: CGSize var activeTint: Color = .blue var barTint: Color = .gray.opacity(0.15) @Binding var activeTab: CustomTab @ViewBuilder var tabItemView: (CustomTab) -> TabItemView func makeCoordinator() -> Coordinator { Coordinator(parent: self) } func makeUIView(context: Context) -> UISegmentedControl { let items = CustomTab.allCases.map(\.rawValue) let control = UISegmentedControl(items: items) control.selectedSegmentIndex = activeTab.index /// Convert Tab Item into an image for (index, tab) in CustomTab.allCases.enumerated() { let renderer = ImageRenderer(content: tabItemView(tab)) renderer.scale = 2 let image = renderer.uiImage control.setImage(image, forSegmentAt: index) } DispatchQueue.main.async { for subview in control.subviews { if subview is UIImageView && subview != control.subviews.last { subview.alpha = 0 } } } control.selectedSegmentTintColor = UIColor(barTint) control.setTitleTextAttributes([ .foregroundColor: UIColor(activeTint) ], for: .selected) control.addTarget(context.coordinator, action: #selector(context.coordinator.tabSelected(_:)), for: .valueChanged) return control } func updateUIView(_ uiView: UISegmentedControl, context: Context) { } func sizeThatFits(_ proposal: ProposedViewSize, uiView: UISegmentedControl, context: Context) -> CGSize? { return size } class Coordinator: NSObject { var parent: CustomTabBar init(parent: CustomTabBar) { self.parent = parent } @objc func tabSelected(_ control: UISegmentedControl) { parent.activeTab = CustomTab.allCases[control.selectedSegmentIndex] } } } #Preview { ContentView() }
Key Takeaways:
- Glass morphism creates modern, iOS-native aesthetics
UIViewRepresentablelets you leverage UIKit when needed- Converting SwiftUI views to images opens up hybrid possibilities
SwiftUI 5.0 - iOS 17
SwiftData: The Modern Core Data
Difficulty: Beginner to Intermediate | Topics: Persistence, CRUD Operations, Macros
SwiftData is Apple's modern approach to data persistence, replacing Core Data's complexity with Swift-native simplicity. This section demonstrates a complete CRUD (Create, Read, Update, Delete) implementation.
What you'll learn:
- Setting up SwiftData models with
@Model - Querying data with
@Queryand#Predicate - Performing CRUD operations with
ModelContext - Filtering and sorting data declaratively
App Setup
First, we must configure the entry point of the application. The .modelContainer(for: Person.self) modifier initializes the database schema and ensures the model context is available to the entire window group.
// // learn_swiftui5_swift_dataApp.swift // learn-swiftui5-swift-data // // Created by georgius on 12/10/25. // import SwiftUI import SwiftData @main struct learn_swiftui5_swift_dataApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Person.self) } }
CRUD in Action
In the ContentView, we interact with the data through three main operations:
1. Reading - The @Query macro fetches data automatically. We use #Predicate to filter results (separating "Favorites" from "Normal") and SortDescriptor to order them by date.
2. Writing - The @Environment(\.modelContext) gives us access to the database context to insert new items or delete existing ones.
3. Updating - Changes to @Model properties (like isLiked) are tracked automatically, requiring only a ctx.save() call to persist.
// // ContentView.swift // learn-swiftui5-swift-data // // Created by georgius on 12/10/25. // import SwiftUI import SwiftData struct ContentView: View { /// create Model Context @Environment(\.modelContext) private var ctx /// Fetch the data @Query(FetchDescriptor<Person>( predicate: #Predicate { $0.isLiked == true }, sortBy: [SortDescriptor(\.dateAdded, order: .reverse)] ), animation: .snappy) private var favourites: [Person] @Query(FetchDescriptor<Person>( predicate: #Predicate { $0.isLiked == false }, sortBy: [SortDescriptor(\.dateAdded, order: .reverse)] ), animation: .snappy) private var normal: [Person] var body: some View { NavigationStack { List { DisclosureGroup("Favourites (\(favourites.count))") { ForEach(favourites) { person in HStack { Text(person.name) Spacer() Button { person.isLiked.toggle() try? ctx.save() } label: { Image(systemName: "suit.heart.fill") .tint(person.isLiked ? .red : .gray) } } .swipeActions { Button { ctx.delete(person) try? ctx.save() } label: { Image(systemName: "trash.fill") } .tint(.red) } } } DisclosureGroup("Normal (\(normal.count))") { ForEach(normal) { person in HStack { Text(person.name) Spacer() Button { person.isLiked.toggle() try? ctx.save() } label: { Image(systemName: "suit.heart.fill") .tint(person.isLiked ? .red : .gray) } } .swipeActions { Button { ctx.delete(person) try? ctx.save() } label: { Image(systemName: "trash.fill") } .tint(.red) } } } } .contentMargins(24, for: .scrollContent) .navigationTitle("Swift Data") .toolbar(content: { ToolbarItem(placement: .topBarTrailing, content: { Button("Add Item") { /// create an object let person = Person(name: "Hello User \(Date().formatted(date: .numeric, time: .omitted))") /// do crud with context model ctx.insert(person) do { try ctx.save() } catch { print(error.localizedDescription) } } }) }) } } } #Preview { ContentView() } @Model class Person { var name: String var isLiked: Bool var dateAdded: Date init(name: String, isLiked: Bool = false, dateAdded: Date = .init()) { self.name = name self.isLiked = isLiked self.dateAdded = dateAdded } }
Key Takeaways:
- SwiftData eliminates boilerplate with Swift macros
@Queryautomatically updates your UI when data changes- Predicates use Swift syntax instead of string-based queries
- No need for manual change tracking—SwiftData handles it!
Keyframes: Cinematic Animations in SwiftUI
Difficulty: Intermediate to Advanced | Topics: Advanced Animations, Keyframe API, Multi-property Animation
Want to create animations that feel like they came from a Pixar film? The Keyframe API is your answer!
What you'll learn:
- Creating complex, multi-stage animations
- Synchronizing multiple property changes
- Using different easing curves (Cubic, Spring, Linear)
- Building realistic reflection effects
The Car Jump Animation
This example utilizes the Keyframe API to create a complex, multi-stage animation of a car. Unlike standard linear animations, Keyframes allow you to define specific values at specific times for different properties using "Tracks".
In the code below, we use .keyframeAnimator to manipulate four properties simultaneously:
1. Scale - Squashes and stretches the car to simulate suspension compression and rebound.
2. Offset - Moves the car up and down, creating the jumping motion with realistic bouncy physics.
3. Rotation - Tilts the car forward and backward during the jump, adding dynamic realism.
4. Opacity - Fades the shadow (reflection) in and out as the car leaves the ground and returns.
Pro Tip: Notice how we use SpringKeyframe for natural physics and CubicKeyframe for precise control!
// // ContentView.swift // learn-swiftui5-started // // Created by georgius on 11/10/25. // import SwiftUI struct ContentView: View { @State private var startKeyFrameAnimation: Bool = false var body: some View { VStack { Spacer() Image(systemName: "car") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 200, height: 200) .keyframeAnimator( initialValue: Keyframe(), trigger: startKeyFrameAnimation) { view, frame in view .scaleEffect(frame.scale) .offset(y: frame.offsetY) .rotationEffect(frame.rotation, anchor: .bottom) .background { view .blur(radius: 3) .rotation3DEffect(.init(degrees: 180), axis: (x: 1.0, y: 0.0, z: 0.0)) .mask({ LinearGradient(colors: [ .white.opacity(frame.reflectionOpacity), .white.opacity(frame.reflectionOpacity - 0.3), .white.opacity(frame.reflectionOpacity - 0.45) ], startPoint: .top, endPoint: .bottom) }) .offset(y: 195) } } keyframes: { frame in KeyframeTrack(\.offsetY) { CubicKeyframe(10, duration: 0.15) SpringKeyframe(-100, duration: 0.3, spring: .bouncy) CubicKeyframe(-100, duration: 0.45) SpringKeyframe(0, duration: 0.3, spring: .bouncy) } KeyframeTrack(\.scale) { CubicKeyframe(0.9, duration: 0.15) CubicKeyframe(1.2, duration: 0.3) CubicKeyframe(1.2, duration: 0.3) CubicKeyframe(1, duration: 0.3) } KeyframeTrack(\.rotation) { CubicKeyframe(.zero, duration: 0.15) CubicKeyframe(.zero, duration: 0.3) CubicKeyframe(.init(degrees: -10), duration: 0.1) CubicKeyframe(.init(degrees: 10), duration: 0.1) CubicKeyframe(.init(degrees: -10), duration: 0.1) CubicKeyframe(.init(degrees: 0), duration: 0.15) } KeyframeTrack(\.reflectionOpacity) { CubicKeyframe(0.5, duration: 0.15) CubicKeyframe(0.3, duration: 0.75) CubicKeyframe(0.5, duration: 0.3) } } Spacer() Button("Key Frame Animation") { startKeyFrameAnimation.toggle() } .fontWeight(.bold) } .padding() } } struct Keyframe { var scale: CGFloat = 1 var offsetY: CGFloat = 0 var rotation: Angle = .zero var reflectionOpacity: CGFloat = 0.5 } #Preview { ContentView() }
Key Takeaways:
KeyframeTracklets you animate different properties independently- Combine
SpringKeyframeandCubicKeyframefor varied effects - The
triggerparameter controls when the animation plays - Reflections and shadows make animations feel grounded and real