The world of SwiftUI is constantly evolving, with each update pushing the boundaries of app development. With iOS 18, the enhancements are both exciting and significant, set to transform how developers engage with SwiftUI.

This guide aims to explore every new feature and improvement in this version, offering a comprehensive overview of the changes.

The Floating Tab Bar

The Tab view in SwiftUI has been greatly enhanced with the addition of a floating tab bar. This new feature can seamlessly transition into a sidebar, providing users with an intuitive way to access the full functionality of an app.

On iPad, users can now tap a sidebar button on the tab bar to transform the tab bar into sidebar. For developers, it’s just a line of code if you want to support this feature. All you need is to set the tab view style to .sidebarAdaptable:

struct ContentView: View {
    @State var customization = TabViewCustomization()
    
    var body: some View {
        TabView {
            Tab("Home", systemImage: "house.fill") {
                
            }
            Tab("Bookmark", systemImage: "bookmark.circle.fill") {
                
            }
            Tab("Videos", systemImage: "video.circle.fill") {
                
            }
            Tab("Profile", systemImage: "person.crop.circle") {
                
            }
            Tab("Settings", systemImage: "gear") {
                
            }
        }
        .tint(.yellow)
        .tabViewStyle(.sidebarAdaptable)
        .tabViewCustomization($customization)
    }
}

Once the option is set, users can effortlessly switch between a sidebar and a tab bar, enhancing navigation flexibility. Additionally, the new tab bar offers extensive customization. By attaching the .tabViewCustomization modifier to the Tab view, users can tailor the menu items of the tab bar.

Sheet Presentation Sizing

Sheet presentation sizing is now consistent and streamlined across platforms. By using the .presentationSizing modifier, you can easily create sheets with ideal dimensions using presets such as .form or .page, or even specify custom sizes.

struct PresentationSizingDemo: View {
    
    @State private var showSheet = false
    
    var body: some View {
        Button {
            showSheet.toggle()
        } label: {
            Text("Show sheet")
        }
        .sheet(isPresented: $showSheet) {
            Text("This is a quick demo of presentation sizing.")
                .presentationSizing(.form)
        }
    }
}

On iPad, the .form preset displays a smaller sheet compared to .page. However, there is no size difference on iPhone.

Color Mesh Gradients

SwiftUI now offers extensive support for colorful mesh gradients. The new MeshGradient feature allows you to create two-dimensional gradients using a grid of positioned colors. By combining control points and colors, you can design a wide variety of gradient effects.

Below shows a gradient created using MeshGradient:

struct MeshGradientView: View {
    var body: some View {
        MeshGradient(width: 3, height: 3, points: [
            .init(0, 0), .init(0.5, 0), .init(1, 0),
            .init(0, 0.5), .init(0.5, 0.5), .init(1, 0.5),
            .init(0, 1), .init(0.5, 1), .init(1, 1)
        ], colors: [
            .red, .purple, .indigo,
            .orange, .white, .blue,
            .yellow, .green, .mint
        ])
        .ignoresSafeArea()
    }
}
Zoom Transition

SwiftUI now has the built-in support of zoom transition. You can use the .matchedTransitionSource modifier to easily render the zoom transition.

If you’re familiar with using matchedGeometryEffect, you’ll find matchedTransitionSource quite similar. Below is sample code we wrote to create the zoom transition shown above:

struct ZoomTransitionDemo: View {
    let samplePhotos = (1...20).map { Photo(name: "coffee-\($0)") }
    
    @Namespace() var namespace
    
    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVGrid(columns: [ GridItem(.adaptive(minimum: 150)) ]) {
                    
                    ForEach(samplePhotos) { photo in
                        NavigationLink {
                            Image(photo.name)
                                .resizable()
                                .navigationTransition(.zoom(sourceID: photo.id, in: namespace))
                        } label: {
                            Image(photo.name)
                                .resizable()
                                .scaledToFill()
                                .frame(minWidth: 0, maxWidth: .infinity)
                                .frame(height: 150)
                                .cornerRadius(30.0)
                        }
                        .matchedTransitionSource(id: photo.id, in: namespace)
                        
                    }
                }
            }
        }
        .padding()
    }
}

The matchedTransitionSource modifier is applied to a NavigationLink with a specific photo ID, designating the view as the source of the navigation transition. For the destination view, which is also an Image view, the navigationTransition modifier is used to render the zoom transition.

More Animations for SF Symbols 6

iOS 17 introduced a fantastic collection of expressive animations for SF Symbols. Developers can leverage these animations using the new symbolEffect modifier. iOS 18 pushes the SF Symbols to version 6 with an even wider variety of animated symbols for developers to utilize in their apps.

Here is a sample code snippet for the new rotate animation:

struct ContentView: View {
    @State private var animate = false
    
    var body: some View {
        Image(systemName: "ellipsis.message")
            .font(.system(size: 300))
            .symbolRenderingMode(.palette)
            .foregroundStyle(.purple, .gray)
            .symbolEffect(.rotate, value: animate)
            .onTapGesture {
                animate.toggle()
            }
    }
}

On top of the rotate animation, SF Symbols 6 also provides two other types of animation including .wiggle and .breathe.

Enhancements of SwiftUI Charts

The SwiftUI Charts framework now supports vectorized and function plots. For example, let’s say you want to plot a graph for the following function:

y = x^2

You can use LinePlot to plot the graph like this:

Chart {
    LinePlot(x: "x", y: "y") { x in
        return pow(x, 2)
    }
    .foregroundStyle(.green)
    .lineStyle(.init(lineWidth: 10))
}
.chartXScale(domain: -4...4)
.chartYScale(domain: -4...4)
.chartXAxis {
    AxisMarks(values: .automatic(desiredCount: 10))
}
.chartYAxis {
    AxisMarks(values: .automatic(desiredCount: 10))
}
.chartPlotStyle { plotArea in
    plotArea
        .background(.yellow.opacity(0.02))
}

You can simply provide the function to a LinePlot to graph a function.

More Control of Scroll Views

The new version of SwiftUI delivers a powerful set of new APIs that give developers fine-grained control over their scroll views. The introduction of the onScrollGeometryChange modifier allows you to keep track with the state of scroll views. This new capability enables you to efficiently react to changes in the scroll view’s content offsets, content size, and other scroll-related properties.

Here’s a sample code snippet that demonstrates how you can use this modifier to display a ‘Scroll to Top’ button after the user has scrolled down a list:

struct ScrollViewDemo: View {
    
    let samplePhotos = (1...20).map { Photo(name: "coffee-\($0)") }
    
    @State private var showScrollToTop = false
    
    var body: some View {
        ScrollView {
            VStack {
                ForEach(samplePhotos) { photo in
                    Image(photo.name)
                        .resizable()
                        .scaledToFill()
                        .frame(height: 200)
                        .clipShape(RoundedRectangle(cornerRadius: 15))
                }
            }
        }
        .padding(.horizontal)
        .overlay(alignment: .bottom) {
            if showScrollToTop {
                Button("Scroll to top") {
                    
                }
                .controlSize(.extraLarge)
                .buttonStyle(.borderedProminent)
                .tint(.green)
            }
        }
        .onScrollGeometryChange(for: Bool.self) { geometry in
            geometry.contentOffset.y < geometry.contentInsets.bottom + 200
            
        } action: { oldValue, newValue in
            withAnimation {
                showScrollToTop = !newValue
            }
        }

    }
}

The geometry of a scroll view changes frequently while scrolling. We can leverage the onScrollGeometryChange modifier to capture the update and display the ‘Scroll to top’ button accordingly.

SwiftUI also introduces the onScrollVisibilityChange modifier for views within a scroll view. This modifier allows you to detect when a particular view becomes visible and perform specific actions in response.

Suppose we have a Rectangle view at the end of a scroll view and we want to trigger a color change animation only when this view comes into view. We can use the onScrollVisibilityChange modifier to detect when the view becomes visible and when it goes off-screen.

Rectangle()
    .fill(color)
    .frame(height: 100)
    .onScrollVisibilityChange(threshold: 0.9) { visible in
        withAnimation(.linear(duration: 5)) {
            color = visible ? .green : .blue
        }
    }
Widgets in Control Center

You now have the ability to design custom resizable controls, like buttons and toggles, which can be placed in the Control Center or on the lock screen. Controls are a new kind of Widget that that are easy to build with App Intents.

To create a control widget in Control Center, you adopt the ControlWidget protocol and provide the implementation.

struct StartPartyControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        StaticControlConfiguration(
            kind: "com.apple.karaoke_start_party"
        ) {
            ControlWidgetButton(action: StartPartyIntent()) {
                Label("Start the Party!", systemImage: "music.mic")
                Text(PartyManager.shared.nextParty.name)
            }
        }
    }
}

We will further look into control widgets in a separate tutorial.

A new Mix Modifier for Color

You can now blend two different colors to create your desired hue by using the new mix modifier.

VStack {
    Color.purple.mix(with: .green, by: 0.3)
        .frame(height: 100)
    
    Color.purple.mix(with: .green, by: 0.5)
        .frame(height: 100)
    
    Color.purple.mix(with: .green, by: 0.8)
        .frame(height: 100)
}

Simply provide the mix modifier with the color to blend and the blend ratio. SwiftUI will then generate the new color based on these parameters.

Visual Effects for Text

You can now extend SwiftUI Text views with custom rendering effects by adopting the TextRenderer.

struct CustomTextRenderer: TextRenderer {
    func draw(layout: Text.Layout, in context: inout GraphicsContext) {
        for line in layout {
            for (index, slice) in runs.enumerated() {
                context.opacity = (index % 2 == 0) ? 0.4 : 1.0
                context.translateBy(x: 0, y: index % 2 != 0 ? -15 : 15)
                
                context.draw(slice)
            }
        }
    }
}

struct TextAnimationDemo: View {
    var body: some View {
        Text("What's New in SwiftUI")
            .font(.system(size: 100))
            .textRenderer(CustomTextRenderer())
    }
}

By implementing the draw method, you can customize the visual effect of each character.

Conclusion

The iOS 18 update introduces a host of significant enhancements to SwiftUI. This tutorial offers a concise introduction to some of the new features. For more complex features, we will be creating detailed, standalone tutorials to thoroughly explore their applications and benefits. Be sure to stay tuned for these upcoming in-depth guides.

If you have any questions or feedback, feel free to reach out to me on or