Streaming is available in most browsers,
and in the Developer app.
-
Inspectors in SwiftUI: Discover the details
Meet Inspectors — a structural API that can help bring a new level of detail to your apps. We'll take you through the fundamentals of the API and show you how to adopt it. Learn about the latest updates to sheet presentation customizations and find out how you can combine the two to create perfect presentation experiences.
Chapters
- 0:32 - Inspector
- 3:27 - 🍎🍐🍋🍒
- 9:11 - 🍏
- 9:37 - Presentation customizations
Resources
Related Videos
WWDC23
-
Download
♪ ♪ Nick: Hi, I'm Nick, an engineer on the SwiftUI team. Inspector is an exciting new element in SwiftUI. I'll go over what an inspector is and how to use the API. After that, I'll review modifiers for presentation customizations. Inspector is the name for views that show further detail of selected content. You have probably interacted with one before. Keynote uses an inspector to show formatting details for a selection, in this case, the shape formatter. Here the inspector presents as a trailing sidebar. Another common use case for inspectors is showing content that supplements an app's main content. Shortcuts uses inspector for this purpose. The main interface is the shortcut that the user is editing, and the inspector supplements that with the library of available apps and actions. I will be using this sample app to explore the inspector API. I've been getting to know the animals in and around Apple Park. This app saves the animals I've met, tracks their name, tracks their favorite fruits, and there's this column that I'll get to later labeled "Suspicion Level." And now, meet, SwiftUI's inspector! The inspector shows a read-write view of details about the selected animal. Here I'm adjusting Fabrizio Fish's suspicion level up to "Extremely suspicious." Actually I'll keep him at fishy. Inspector is available to SwiftUI Developers on macOS and iPadOS and iOS too! The inspector API includes programmatic control over column width, allowing you to tune the width of the trailing column. The API includes programmatic control over presented state, allowing hiding and showing of the inspector as needed. Inspector is a higher level abstraction than just a trailing sidebar. In compact size classes, it adapts to a resizable sheet and inspector will automatically overlay in split screen on larger iPads. SwiftUI already has an existing set of structural APIs. Inspector fits in alongside these APIs with characteristics of both navigation components and presentations. Similar to NavigationSplitView and NavigationStack, inspector is used to build the scaffolding of your scene. Similar to Sheet, popover, alert, and confirmation dialogs, inspector is a presentation, dismissing and presenting as needed. It's time to learn how to adopt the new inspector API. Earlier, I showed off the sample app I made for this session. You may have noticed I was tracking each animal's suspicion level. That's because I am trying to solve a high-stakes mystery. Someone is eating all the fruit at Apple Park! I added inspector to my app so that I, Inspector Nick, can gather the details of each animal. I'll show you how easy it was to adopt the inspector API, and maybe I can solve this fruity mystery along the way. Quickly, to Xcode! The game is a-fruit! The first step to adding inspector is using the new modifier, inspector. Like some other presentations, this takes a Bool presented binding, and then the inspector's content in the trailing view builder. For the inspector content, I have a ready made AnimalInspectorForm. I have some custom methods for passing in the animal currently being inspected. And there's my inspector, presenting as a trailing column here on macOS. I have planted the seeds of a wonderful inspector experience. This AnimalForm is using the grouped style. In case you haven't encountered form styles before, they're applied like this. But because inspector contexts use the grouped style by default, I don't need to style it myself. I can even interact with the inspector in the preview's canvas. Ah, this reminds me, while inspectors can collapse by default, they aren't resizable by default. I can change that using the inspector column width modifier. I'll use some sensible defaults. 200 for min, 300 for ideal, and 400 for max. This ideal parameter will be the size of the column at at first launch, but if the user resizes the inspector, the system will persist that size across launches. Lastly, I'll add a toolbar item to toggle the presented state. I'll use a button that toggles the presented property, and for its label, a Label, using the info.circle system image. The toolbar item will appear in the section of the toolbar above the inspector because it's declared within the inspector's view builder. When I scroll, the toolbar behaves exactly like I'd expect, showing a shadow pocket when enough of the content is underneath the toolbar.
I used the inspector modifier on the AnimalTable intentionally, rather than some other spot in the view hierarchy. Like many SwiftUI APIs, the inspector modifier has different behaviors depending on the context of where it is applied. Specifically, the placement decides whether the full height style is used, where there is no separation between toolbar and content, or the under toolbar style is used, where the inspector is nested under the toolbar. Notice the title separator spans the full width of the window in the under toolbar appearance. Similarly, toolbar content will be placed in the main content's toolbar... or in the inspector's toolbar, depending on where the toolbar modifier is used. There are two points to consider when using the inspector API. First, is the inspector going to be placed inside or outside of a navigation structure like a NavigationStack or NavigationSplitView? Second, should the toolbar content be inside or outside of the inspector's view builder? I'll walk through two of these constructions. First up, placing inspector inside a navigation structure, with toolbar content outside the inspector. When an inspector is contained within a NavigationStack, the inspector is underneath the navigation stack's toolbar. Here, the toolbar content is declared outside of the inspector on the main content and is rendered in the navigation stack's toolbar. In compact horizontal size classes, the inspector presents as a sheet and the toolbar item stays in the main content's toolbar. Back to my table of possibilities. For the second construction, I'll look at placing the inspector outside of a navigation structure, with toolbar content inside the inspector view builder. When the inspector modifier is placed outside of a navigation structure, the inspector is given the full height of the trailing column to lay out. If the inspector has toolbar content, that content will be placed in a toolbar section specifically for the inspector. These two toolbar items are positioned with the principal placement and end up centered in the navigation toolbar above the inspector. This time, however, because the toolbar content is inside the inspector's view builder, when the inspector presents as a sheet, the toolbar content is in the sheet.
These principles extend to macOS, except inspector does not present as a sheet on macOS, and so the table becomes simpler. The only axis to worry about is inside or outside a navigation structure. A final, but important note: If you are using an inspector within a NavigationSplitView, the inspector should be placed in the detail column's view builder, or, just like before, it can also be placed entirely outside the navigation structure. Wow, all of this mystery solving is making me hungry! Oh! I'm getting a Nibble Bulletin in the app! The app shows Nibble Bulletins as resizable sheets with some information about where and when the fruit was nibbled. This is a great time for me to talk about presentation customizations SwiftUI released with iOS 16.4. The fruit nibble bulletin is a sheet, not an inspector. Presentation modifiers allow deep customization of sheets and other presentations like popovers. I'll try a few of them out. The presentation background modifier is aptly named. It will set the background of a presentation. Unlike the existing background modifier, the presentation specific modifier will fill the entire presentation and allow underlying content to show through. So, if I use a thinMaterial, I'll see a hint of the List behind the sheet. I'd like to allow interaction with content behind the sheet, in case I want to scroll the list, and look at suspects while reading the Nibble Bulletin. This is as simple as presentationBackgroundInteraction enabled. The dimming view is removed and I can now interact with the background content. The PresentationBackgroundInteraction type can also have an upThrough parameter accepting a presentation detent. As long as the provided argument matches one of the given presentation's detents, SwiftUI will only provide the dimming view at detents greater than the upThrough argument. I want dimming at any detent above a height of 200, so I will first add a height detent of 200 using the presentationDetents modifier. I'll adjust the sheet from its current detent of medium... down to my custom height of 200. When I only enable background interaction up through 200, the dimming view will return for the medium and large detents. There are more customizations available to you to get the perfect look and feel for presentations. Many of these modifiers have effects on other presentations too, not just sheets. And one more thing about presentation modifiers. Those same modifiers compose with Inspector when Inspector is presenting as a sheet. For my inspector from earlier, to disable background content interaction at the medium detent, I can use the same exact code from before... declaring a height detent, and only enabling background interaction up through the same detent. Now the inspector dims at the medium detent and higher. And that's inspectors in SwiftUI. In this video, I introduced the inspector API and the nuances of using it. I picked some of my favorite sheet presentation modifiers to demonstrate, and showed how they can compose with inspectors. So what are you waiting for? Go out and get inspecting. Place the inspector somewhere in your app. Keep it going by customizing your presentations. And as for the nibbler, I guess our efforts to solve this mystery were fruitless. ♪ ♪
-
-
3:35 - Sample models and views
// Copy+Paste the below into an Xcode project to support building and running the session's code snippets import SwiftUI @main struct SwiftUIInspectors: App { var body: some Scene { WindowGroup { ContentView() .environmentObject(AnimalStore()) } } } struct AnimalInspectorForm: View { var animal: Binding<Animal>? @EnvironmentObject private var animalStore: AnimalStore var body: some View { Form { if let animal = animal { SelectedAnimalInspector(animal: animal, animalStore: animalStore) } else { ContentUnavailableView { Image(systemName: "magnifyingglass.circle") } description: { Text("Select a suspect to inspect") } actions: { Text("Fill out details from the interview") } } } #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif } } struct SelectedAnimalInspector: View { @Binding var animal: Animal @ObservedObject var animalStore: AnimalStore var body: some View { Section("Identity") { TextField("Name", text: $animal.name) Picker("Paw Size", selection: $animal.pawSize) { Text("Small").tag(PawSize.small) Text("Medium").tag(PawSize.medium) Text("Large").tag(PawSize.large) } FruitList(selectedFruits: $animal.favoriteFruits, fruits: allFruits) } Section { TextField(text: animalStore(\.alibi, for: animal), prompt: Text("What was \(animal.name) doing at the time of nibbling?"), axis: .vertical) { Text("Alibi") } .lineLimit(4, reservesSpace: true) if let schedule = Binding(animalStore(\.sleepSchedule, for: animal)) { SleepScheduleView(schedule: schedule) } else { Button("Add Sleep Schedule") { animalStore.write(\.sleepSchedule, value: Animal.Storage.newSleepSchedule, for: animal) } } Slider( value: animalStore(\.suspiciousLevel, for: animal), in: 0...1) { Text("Suspicion Level") } minimumValueLabel: { Image(systemName: "questionmark") } maximumValueLabel: { Image(systemName: "exclamationmark.3") } } header: { Text("Interview") } .presentationDetents([.medium, .large]) } } private struct FruitList: View { @Binding var selectedFruits: [Fruit] var fruits: [Fruit] var body: some View { Section("Favorite Fruits") { ForEach(allFruits) { fruit in Toggle(isOn: .init(get: { selectedFruits.contains(fruit) }, set: { newValue in if newValue && !selectedFruits.contains(fruit) { selectedFruits.append(fruit) } else { _ = selectedFruits.firstIndex(of: fruit).map { selectedFruits.remove(at: $0) } } })) { HStack { FruitImage(fruit: fruit, size: .init(width: 40, height: 40), bordered: true) Text(fruit.name).font(.body) } } } } } @ViewBuilder private func selectionBackground(isSelected: Bool) -> some View { if isSelected { RoundedRectangle(cornerRadius: 2).inset(by: -2) .fill(.selection) } } } private struct SleepScheduleView: View { @Binding var schedule: Animal.Storage.SleepSchedule var body: some View { DatePicker(selection: .init(get: { Calendar.current.date(from: schedule.sleepTime) ?? Date() }, set: { newDate in schedule.sleepTime = Calendar.current.dateComponents([.hour, .minute], from: newDate) }), displayedComponents: [.hourAndMinute]) { Text("Sleep at: ") } DatePicker(selection: .init(get: { Calendar.current.date(from: schedule.wakeTime) ?? Date() }, set: { newDate in schedule.wakeTime = Calendar.current.dateComponents([.hour, .minute], from: newDate) }), displayedComponents: [.hourAndMinute]) { Text("Awake at: ") } } } struct AppState { var selection: String? = "Snail" var animals: [Animal] = allAnimals var inspectorPresented: Bool = true var inspectorWidth: CGFloat = 270 var cornerRadius: CGFloat? = nil } extension Binding where Value == AppState { func binding() -> Binding<Animal>? { self.projectedValue.animals.first { $0.wrappedValue.id == self.selection.wrappedValue } } } extension Animal { struct Storage: Codable { var alibi: String = "" var sleepSchedule: SleepSchedule? = nil /// Value between 0 and 1 representing how suspicious the animal is. /// 1 is guilty. var suspiciousLevel: Double = 0.0 struct SleepSchedule: Codable { var sleepTime: DateComponents var wakeTime: DateComponents } static let newSleepSchedule: SleepSchedule = { // Asleep at 10:30, awake at 6:30 .init( sleepTime: DateComponents(hour: 22, minute: 30), wakeTime: DateComponents(hour: 6, minute: 30)) }() } } final class AnimalStore: ObservableObject { var storage: [Animal.ID: Animal.Storage] = [:] /// Getter for properties of an animal stored in self func callAsFunction<Result>(_ keyPath: WritableKeyPath<Animal.Storage, Result>, for animal: Animal) -> Binding<Result> { Binding { [self] in storage[animal.id, default: .init()][keyPath: keyPath] } set: { [self] newValue in self.objectWillChange.send() var animalStore = storage[animal.id, default: .init()] animalStore[keyPath: keyPath] = newValue storage[animal.id] = animalStore } } func write<Value>(_ keyPath: WritableKeyPath<Animal.Storage, Value>, value: Value, for animal: Animal) { objectWillChange.send() var animalStore = storage[animal.id, default: .init()] animalStore[keyPath: keyPath] = value storage[animal.id] = animalStore } func read<Value>(_ keyPath: WritableKeyPath<Animal.Storage, Value>, for animal: Animal) -> Value { storage[animal.id, default: .init()][keyPath: keyPath] } } struct AnimalTable: View { @Binding var state: AppState @EnvironmentObject private var animalStore: AnimalStore @Environment(\.horizontalSizeClass) private var sizeClass: UserInterfaceSizeClass? var fruitWidth: CGFloat { #if os(iOS) 40.0 #else 25.0 #endif } var body: some View { Table(state.animals, selection: $state.selection) { TableColumn("Name") { animal in HStack { Text(animal.emoji).font(.title) .padding(2) .background(.thickMaterial, in: RoundedRectangle(cornerRadius: 3)) Text(animal.name + " " + animal.species).font(.title3) } } TableColumn("Favorite Fruits") { animal in HStack { ForEach(animal.favoriteFruits.prefix(3)) { fruit in FruitImage(fruit: fruit, size: .init(width: fruitWidth, height: fruitWidth), scale: 2.0, bordered: state.selection == animal.id) } } .padding(3.5) } TableColumn("Suspicion Level") { animal in SuspicionTableCell(animal: animal) } } #if os(macOS) .alternatingRowBackgrounds(.disabled) #endif .tableStyle(.inset) } } private struct SuspicionTableCell: View { var animal: Animal @Environment(\.backgroundProminence) private var backgroundProminence @EnvironmentObject private var animalStore: AnimalStore var body: some View { let color = SuspiciousText.model(for: animalStore.read(\.suspiciousLevel, for: animal)).1 HStack { Image( systemName: "cellularbars", variableValue: animalStore.read(\.suspiciousLevel, for: animal) ) .symbolRenderingMode(.hierarchical) SuspiciousText( suspiciousLevel: animalStore.read(\.suspiciousLevel, for: animal), selected: backgroundProminence == .increased) } .foregroundStyle(backgroundProminence == .increased ? AnyShapeStyle(.white) : AnyShapeStyle(color)) } } private struct SuspiciousText: View { var suspiciousLevel: Double var selected: Bool static fileprivate func model(for level: Double) -> (String, Color) { switch level { case 0..<0.2: return ("Unlikely", .green) case 0.2..<0.5: return ("Fishy", .mint) case 0.5..<0.9: return ("Very suspicious", .orange) case 0.9...1: return ("Extremely suspicious!", .red) default: return ("Suspiciously Unsuspicious", .blue) } } var body: some View { let model = Self.model(for: suspiciousLevel) Text(model.0) .font(.callout) } } struct Animal: Identifiable { var name: String var species: String var pawSize: PawSize var favoriteFruits: [Fruit] var emoji: String var id: String { species } } var allAnimals: [Animal] = [ .init(name: "Fabrizio", species: "Fish", pawSize: .small, favoriteFruits: [.arbutusUnedo, .bigBerry, .elstar], emoji: "🐟"), .init(name: "Soloman", species: "Snail", pawSize: .small, favoriteFruits: [.elstar, .flavorKing], emoji: "🐌"), .init(name: "Ding", species: "Dove", pawSize: .small, favoriteFruits: [.quercusTomentella, .pinkPearlApple, .lapins], emoji: "🕊️"), .init(name: "Catie", species: "Crow", pawSize: .small, favoriteFruits: [.pinkPearlApple, .goldenNectar, .hauerPippin], emoji: "🐦⬛"), .init(name: "Miko", species: "Cat", pawSize: .small, favoriteFruits: [.belleDeBoskoop, .tompkinsKing, .lapins], emoji: "🐈"), .init(name: "Ricardo", species: "Rabbit", pawSize: .small, favoriteFruits: [.mariposa, .elephantHeart], emoji: "🐰"), .init(name: "Cornelius", species: "Duck", pawSize: .medium, favoriteFruits: [.greenGage, .goldenNectar], emoji: "🦆"), .init(name: "Maria", species: "Mouse", pawSize: .small, favoriteFruits: [.arbutusUnedo, .elephantHeart], emoji: "🐹"), .init(name: "Haku", species: "Hedgehog", pawSize: .small, favoriteFruits: [.christmasBerry, .creepingSnowberry, .goldenGem], emoji: "🦔"), .init(name: "Rénard", species: "Raccoon", pawSize: .medium, favoriteFruits: [.belleDeBoskoop, .bigBerry, .christmasBerry, .kakiFuyu], emoji: "🦝") ] enum PawSize: Hashable { case small case medium case large } struct Fruit: Identifiable, Hashable { var name: String var color: Color var id: String { name } } struct FruitImage: View { var fruit: Fruit var size: CGSize? = .init(width: 50, height: 50) var scale: CGFloat = 1.0 var bordered = false var body: some View { fruit.color // Actual assets replaced with Color .scaleEffect(scale) .scaledToFill() .frame(width: size?.width, height: size?.height) .mask { RoundedRectangle(cornerRadius: 4) } .overlay { if bordered { RoundedRectangle(cornerRadius: 4) .stroke(fruit.color, lineWidth: 2) } } } } extension Fruit { static let goldenGem = Fruit(name: "Golden Gem Apple", color: .yellow) static let flavorKing = Fruit(name: "Flavor King Plum", color: .purple) static let mariposa = Fruit(name: "Mariposa Plum", color: .red) static let tompkinsKing = Fruit(name: "Tompkins King Apple", color: .yellow) static let greenGage = Fruit(name: "Green Gage Plum", color: .green) static let lapins = Fruit(name: "Lapins Sweet Cherry", color: .purple) static let hauerPippin = Fruit(name: "Hauer Pippin Apple", color: .red) static let belleDeBoskoop = Fruit(name: "Belle De Boskoop Apple", color: .red) static let elstar = Fruit(name: "Elstar Apple", color: .yellow) static let goldenDeliciousApple = Fruit(name: "Golden Delicious Apple", color: .yellow) static let creepingSnowberry = Fruit(name: "Creeping Snowberry", color: .white) static let quercusTomentella = Fruit(name: "Channel Island Oak Acorn", color: .brown) static let elephantHeart = Fruit(name: "Elephant Heart Plum", color: .red) static let goldenNectar = Fruit(name: "Golden Nectar Plum", color: .yellow) static let pinkPearlApple = Fruit(name: "Pink Pearl Apple", color: .pink) static let christmasBerry = Fruit(name: "Christmas Berry", color: .red) static let kakiFuyu = Fruit(name: "Kaki Fuyu Persimmon", color: .orange) static let bigBerry = Fruit(name: "Big Berry Manzanita", color: .red) static let arbutusUnedo = Fruit(name: "Strawberry Tree", color: .red) } extension Array where Element == Fruit { var groupID: Fruit.ID { reduce("") { result, next in result.appending(next.id) } } } var allFruits: [Fruit] = [ .goldenGem, .flavorKing, .mariposa, .tompkinsKing, .greenGage, .lapins, .hauerPippin, .belleDeBoskoop, .elstar, .goldenDeliciousApple, .creepingSnowberry, .quercusTomentella, .elephantHeart, .goldenNectar, .kakiFuyu, .bigBerry, .arbutusUnedo, .pinkPearlApple, ]
-
3:54 - Xcode Previews
import SwiftUI #Preview("Meet Inspector", traits: .fixedLayout(width: 800, height: 500) ) { ContentView() .navigationTitle("SwiftUI Inspectors") .environmentObject(AnimalStore()) } public struct ContentView: View { @State private var state = AppState() @State private var presented = true public var body: some View { AnimalTable(state: $state) .inspector(isPresented: $presented) { AnimalInspectorForm(animal: $state.binding()) .inspectorColumnWidth( min: 200, ideal: 300, max: 400) .toolbar { Spacer() Button { presented.toggle() } label: { Label("Toggle Inspector", systemImage: "info.circle") } } } } } import MapKit struct FruitNibbleBulletin: View { var fruit: Fruit = .pinkPearlApple @Environment(\.dismiss) private var dismiss var body: some View { NavigationStack { ScrollView { VStack(alignment: .leading) { Grid(horizontalSpacing: 12, verticalSpacing: 2) { GridRow { FruitImage(fruit: fruit, size: .init(width: 60, height: 60), bordered: false) Text(""" A \(fruit.name.lowercased()) was nibbled! The bite \ happened at 9:41 AM. The nibbler left behind only \ a few seeds. """ ) } GridRow { Text(""" The Fruit Inspectors were on \ the scene moments after it happened. \ Unfortunately, their efforts to catch the nibbler \ were fruitless. """).gridCellColumns(2) } } GroupBox("Clues") { LabeledContent("Paw Size") { Text("Large") } LabeledContent("Favorite Fruit") { Text("\(fruit.name.capitalized(with: .current))") } LabeledContent("Alibi") { Text("None") } } HStack { VStack { fruit.color .aspectRatio(contentMode: ContentMode.fit) .shadow(radius: 2.5) Text("The pink pearls left behind").font(.caption) .frame(alignment: .leading) } AppleParkMap() .mask(RoundedRectangle(cornerSize: CGSize(width: 20, height: 10))) } Text("The Fruit Inspection team was on the scene minutes after the incident. However, their attempts to discover any meaningful clues around the identity of the nibbler were fruitless.") } .scenePadding(.horizontal) .toolbar { ToolbarItem { Button(role: .cancel) { dismiss() } label: { Label("Close", systemImage: "xmark.circle.fill") } .symbolRenderingMode(.monochrome) .tint(.secondary) } } } #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .navigationTitle("Fruit Nibble Bulletin") } } } struct AppleParkMap: View { @State private var region = MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 37.334_371, longitude: -122.009_558), latitudinalMeters: 100, longitudinalMeters: 100 ) var body: some View { GeometryReader { geometry in Map(position: .constant(.automatic), bounds: .init(centerCoordinateBounds: region, minimumDistance: 100, maximumDistance: 100), interactionModes: [], scope: .none) { } } .frame(height: 180, alignment: .center) } }
-
7:14 - Inspector content inside a navigation structure
struct Example1: View { @State private var state = AppState() var body: some View { NavigationStack { AnimalTable(state: $state) .inspector(isPresented: $state.inspectorPresented) { AnimalInspectorForm(animal: $state.binding()) } .toolbar { Button { state.inspectorPresented.toggle() } label: { Label("Toggle Inspector", systemImage: "info.circle") } } } } }
-
7:55 - Inspector content outside a navigation structure
struct Example2: View { @State private var state = AppState() var body: some View { NavigationStack { AnimalTable(state: $state) } .inspector(isPresented: $state.inspectorPresented) { AnimalInspectorForm(animal: $state.binding()) .toolbar { ToolbarItem(placement: .principal) { HStack { Button { } label: { Image(systemName: "rectangle.and.pencil.and.ellipsis") } Button { } label: { Image(systemName: "pawprint.circle") } } } } } } }
-
8:56 - Inspector with NavigationSplitView: detail column
NavigationSplitView { Sidebar() } detail: { AnimalTable() .inspector(presented: $isPresented) { AnimalInspectorForm() } }
-
9:06 - Inspector with NavigationSplitView: Outside
NavigationSplitView { Sidebar() } detail: { AnimalTable() } .inspector(presented: $isPresented) { AnimalInspectorForm() }
-
9:49 - Presentation customizations
.sheet(item: $nibbledFruit) { fruit in FruitNibbleBulletin(fruit: fruit) .presentationBackground(.thinMaterial) .presentationDetents([.height(200), .medium, .large]) .presentationBackgroundInteraction(.enabled(upThrough: .height(200))) }
-
11:58 - Presentation customizations on Inspector
.inspector(presented: $state.inspectorPresented) { AnimalInspectorForm(animal: $state.binding()) .presentationDetents([.height(200), .medium, .large]) .presentationBackgroundInteraction(.enabled(upThrough: .height(200))) }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.