ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftUIにおけるインスペクタの詳細
インスペクタは、アプリに更なる綿密性をもたらす構造APIです。まずは基本を説明し、導入方法も紹介します。シートのカスタマイズに関する最新アップデートについても学び、2つを組み合わせて完璧なプレゼンテーション体験を生み出す方法を理解しましょう。
関連する章
- 0:32 - Inspector
- 3:27 - 🍎🍐🍋🍒
- 9:11 - 🍏
- 9:37 - Presentation customizations
リソース
関連ビデオ
WWDC23
-
ダウンロードArray
-
-
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))) }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。