ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
進化したScrollViewの詳細
SwiftUIの最新APIを使ってScrollViewを進化させる方法を学びましょう。これまでにないようなScrollViewのカスタマイズ方法を紹介します。セーフエリアとScrollViewのマージンの関係を知り、ScrollViewのコンテンツオフセットの指定方法を学び、スクロールトランジションの実装でコンテンツに少し派手さを加える方法を知りましょう。
関連する章
- 0:00 - Introduction to scroll views
- 2:01 - Margins and safe area
- 4:14 - Targets and positions
- 11:33 - Scroll transitions
リソース
関連ビデオ
WWDC23
-
ダウンロードArray
-
-
0:46 - ScrollView
struct Item: Identifiable { var id: Int } struct ContentView: View { @State var items: [Item] = (0 ..< 25).map { Item(id: $0) } var body: some View { ScrollView(.vertical) { LazyVStack { ForEach(items) { item in ItemView(item: item) } } } } } struct ItemView: View { var item: Item var body: some View { Text(item, format: .number) .padding(.vertical) .frame(maxWidth: .infinity) } }
-
2:29 - Basic Featured Section
struct ContentView: View { @State var palettes: [Palette] = [ .init(id: UUID(), name: "Example One"), .init(id: UUID(), name: "Example Two"), .init(id: UUID(), name: "Example Three"), ] var body: some View { ScrollView { GalleryHeroSection(palettes: palettes) } } } struct Palette: Identifiable { var id: UUID var name: String } struct GalleryHeroSection: View { var palettes: [Palette] var body: some View { GallerySection(edge: .top) { GalleryHeroContent(palettes: palettes) } label: { GalleryHeroHeader(palettes: palettes) } } } struct GallerySection<Content: View, Label: View>: View { var edge: Edge? = nil @ViewBuilder var content: Content @ViewBuilder var label: Label var body: some View { VStack(alignment: .leading) { label .font(.title2.bold()) content } .padding(.top, halfSpacing) .padding(.bottom, sectionSpacing) .overlay(alignment: .bottom) { if edge != .bottom { Divider().padding(.horizontal, hMargin) } } } var halfSpacing: CGFloat { sectionSpacing / 2.0 } var sectionSpacing: CGFloat { 20.0 } var hMargin: CGFloat { #if os(macOS) 40.0 #else 20.0 #endif } } struct GalleryHeroContent: View { var palettes: [Palette] var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: hSpacing) { ForEach(palettes) { palette in GalleryHeroView(palette: palette) } } } } var hSpacing: CGFloat { 10.0 } } struct GalleryHeroView: View { var palette: Palette @Environment(\.horizontalSizeClass) private var sizeClass var body: some View { colorStack .aspectRatio(heroRatio, contentMode: .fit) .containerRelativeFrame( [.horizontal], count: columns, spacing: hSpacing ) .clipShape(.rect(cornerRadius: 20.0)) } private var columns: Int { sizeClass == .compact ? 1 : regularCount } @ViewBuilder private var colorStack: some View { let offsetValue = stackPadding ZStack { Color.red .offset(x: offsetValue, y: offsetValue) Color.blue Color.green .offset(x: -offsetValue, y: -offsetValue) } .padding(stackPadding) .background() } var stackPadding: CGFloat { 20.0 } var heroRatio: CGFloat { 16.0 / 9.0 } var regularCount: Int { 2 } var hSpacing: CGFloat { 10.0 } } struct GalleryHeroHeader: View { var palettes: [Palette] var body: some View { Text("Featured") .padding(.horizontal, hMargin) } var hMargin: CGFloat { 20.0 } }
-
4:00 - Featured Section with Margins
struct ContentView: View { @State var palettes: [Palette] = [ .init(id: UUID(), name: "Example One"), .init(id: UUID(), name: "Example Two"), .init(id: UUID(), name: "Example Three"), ] var body: some View { ScrollView { GalleryHeroSection(palettes: palettes) } } } struct Palette: Identifiable { var id: UUID var name: String } struct GalleryHeroSection: View { var palettes: [Palette] var body: some View { GallerySection(edge: .top) { GalleryHeroContent(palettes: palettes) } label: { GalleryHeroHeader(palettes: palettes) } } } struct GallerySection<Content: View, Label: View>: View { var edge: Edge? = nil @ViewBuilder var content: Content @ViewBuilder var label: Label var body: some View { VStack(alignment: .leading) { label .font(.title2.bold()) content } .padding(.top, halfSpacing) .padding(.bottom, sectionSpacing) .overlay(alignment: .bottom) { if edge != .bottom { Divider().padding(.horizontal, hMargin) } } } var halfSpacing: CGFloat { sectionSpacing / 2.0 } var sectionSpacing: CGFloat { 20.0 } var hMargin: CGFloat { #if os(macOS) 40.0 #else 20.0 #endif } } struct GalleryHeroContent: View { var palettes: [Palette] var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: hSpacing) { ForEach(palettes) { palette in GalleryHeroView(palette: palette) } } } .contentMargins(.horizontal, hMargin) } var hMargin: CGFloat { 20.0 } var hSpacing: CGFloat { 10.0 } } struct GalleryHeroView: View { var palette: Palette @Environment(\.horizontalSizeClass) private var sizeClass var body: some View { colorStack .aspectRatio(heroRatio, contentMode: .fit) .containerRelativeFrame( [.horizontal], count: columns, spacing: hSpacing ) .clipShape(.rect(cornerRadius: 20.0)) } private var columns: Int { sizeClass == .compact ? 1 : regularCount } @ViewBuilder private var colorStack: some View { let offsetValue = stackPadding ZStack { Color.red .offset(x: offsetValue, y: offsetValue) Color.blue Color.green .offset(x: -offsetValue, y: -offsetValue) } .padding(stackPadding) .background() } var stackPadding: CGFloat { 20.0 } var heroRatio: CGFloat { 16.0 / 9.0 } var regularCount: Int { 2 } var hSpacing: CGFloat { 10.0 } } struct GalleryHeroHeader: View { var palettes: [Palette] var body: some View { Text("Featured") .padding(.horizontal, hMargin) } var hMargin: CGFloat { 20.0 } }
-
7:42 - Featured Section + Container Relative Frame
struct ContentView: View { @State var palettes: [Palette] = [ .init(id: UUID(), name: "Example One"), .init(id: UUID(), name: "Example Two"), .init(id: UUID(), name: "Example Three"), ] var body: some View { ScrollView { GalleryHeroSection(palettes: palettes) } } } struct Palette: Identifiable { var id: UUID var name: String } struct GalleryHeroSection: View { var palettes: [Palette] var body: some View { GallerySection(edge: .top) { GalleryHeroContent(palettes: palettes) } label: { GalleryHeroHeader(palettes: palettes) } } } struct GallerySection<Content: View, Label: View>: View { var edge: Edge? = nil @ViewBuilder var content: Content @ViewBuilder var label: Label var body: some View { VStack(alignment: .leading) { label .font(.title2.bold()) content } .padding(.top, halfSpacing) .padding(.bottom, sectionSpacing) .overlay(alignment: .bottom) { if edge != .bottom { Divider().padding(.horizontal, hMargin) } } } var halfSpacing: CGFloat { sectionSpacing / 2.0 } var sectionSpacing: CGFloat { 20.0 } var hMargin: CGFloat { #if os(macOS) 40.0 #else 20.0 #endif } } struct GalleryHeroContent: View { var palettes: [Palette] var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: hSpacing) { ForEach(palettes) { palette in GalleryHeroView(palette: palette) } } .scrollTargetLayout() } .contentMargins(.horizontal, hMargin) .scrollTargetBehavior(.viewAligned) } var hMargin: CGFloat { 20.0 } var hSpacing: CGFloat { 10.0 } } struct GalleryHeroView: View { var palette: Palette @Environment(\.horizontalSizeClass) private var sizeClass var body: some View { colorStack .aspectRatio(heroRatio, contentMode: .fit) .containerRelativeFrame( [.horizontal], count: columns, spacing: hSpacing ) .clipShape(.rect(cornerRadius: 20.0)) } private var columns: Int { sizeClass == .compact ? 1 : regularCount } @ViewBuilder private var colorStack: some View { let offsetValue = stackPadding ZStack { Color.red .offset(x: offsetValue, y: offsetValue) Color.blue Color.green .offset(x: -offsetValue, y: -offsetValue) } .padding(stackPadding) .background() } var stackPadding: CGFloat { 20.0 } var heroRatio: CGFloat { 16.0 / 9.0 } var regularCount: Int { 2 } var hSpacing: CGFloat { 10.0 } } struct GalleryHeroHeader: View { var palettes: [Palette] var body: some View { Text("Featured") .padding(.horizontal, hMargin) } var hMargin: CGFloat { 20.0 } }
-
9:46 - Featured Section + Scroll Position
struct ContentView: View { @State var palettes: [Palette] = [ .init(id: UUID(), name: "Example One"), .init(id: UUID(), name: "Example Two"), .init(id: UUID(), name: "Example Three"), ] var body: some View { ScrollView { GalleryHeroSection(palettes: palettes) } } } struct Palette: Identifiable { var id: UUID var name: String } struct GalleryHeroSection: View { var palettes: [Palette] @State var mainID: Palette.ID? = nil var body: some View { GallerySection(edge: .top) { GalleryHeroContent(palettes: palettes, mainID: $mainID) } label: { GalleryHeroHeader(palettes: palettes, mainID: $mainID) } } } struct GallerySection<Content: View, Label: View>: View { var edge: Edge? = nil @ViewBuilder var content: Content @ViewBuilder var label: Label var body: some View { VStack(alignment: .leading) { label .font(.title2.bold()) content } .padding(.top, halfSpacing) .padding(.bottom, sectionSpacing) .overlay(alignment: .bottom) { if edge != .bottom { Divider().padding(.horizontal, hMargin) } } } var halfSpacing: CGFloat { sectionSpacing / 2.0 } var sectionSpacing: CGFloat { 20.0 } var hMargin: CGFloat { #if os(macOS) 40.0 #else 20.0 #endif } } struct GalleryHeroContent: View { var palettes: [Palette] @Binding var mainID: Palette.ID? var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: hSpacing) { ForEach(palettes) { palette in GalleryHeroView(palette: palette) } } .scrollTargetLayout() } .contentMargins(.horizontal, hMargin) .scrollTargetBehavior(.viewAligned) .scrollPosition(id: $mainID) .scrollIndicators(.never) } var hMargin: CGFloat { 20.0 } var hSpacing: CGFloat { 10.0 } } struct GalleryHeroView: View { var palette: Palette @Environment(\.horizontalSizeClass) private var sizeClass var body: some View { colorStack .aspectRatio(heroRatio, contentMode: .fit) .containerRelativeFrame( [.horizontal], count: columns, spacing: hSpacing ) .clipShape(.rect(cornerRadius: 20.0)) } private var columns: Int { sizeClass == .compact ? 1 : regularCount } @ViewBuilder private var colorStack: some View { let offsetValue = stackPadding ZStack { Color.red .offset(x: offsetValue, y: offsetValue) Color.blue Color.green .offset(x: -offsetValue, y: -offsetValue) } .padding(stackPadding) .background() } var stackPadding: CGFloat { 20.0 } var heroRatio: CGFloat { 16.0 / 9.0 } var regularCount: Int { 2 } var hSpacing: CGFloat { 10.0 } } struct GalleryHeroHeader: View { var palettes: [Palette] @Binding var mainID: Palette.ID? var body: some View { VStack(alignment: .leading, spacing: 2.0) { Text("Featured") Spacer().frame(maxWidth: .infinity) } .padding(.horizontal, hMargin) #if os(macOS) .overlay { HStack(spacing: 0.0) { GalleryPaddle(edge: .leading) { scrollToPreviousID() } Spacer().frame(maxWidth: .infinity) GalleryPaddle(edge: .trailing) { scrollToNextID() } } } #endif } private func scrollToNextID() { guard let id = mainID, id != palettes.last?.id, let index = palettes.firstIndex(where: { $0.id == id }) else { return } withAnimation { mainID = palettes[index + 1].id } } private func scrollToPreviousID() { guard let id = mainID, id != palettes.first?.id, let index = palettes.firstIndex(where: { $0.id == id }) else { return } withAnimation { mainID = palettes[index - 1].id } } var hMargin: CGFloat { 20.0 } } struct GalleryPaddle: View { var edge: HorizontalEdge var action: () -> Void var body: some View { Button { action() } label: { Label(labelText, systemImage: labelIcon) } .buttonStyle(.paddle) .font(nil) } var labelText: String { switch edge { case .leading: return "Backwards" case .trailing: return "Forwards" } } var labelIcon: String { switch edge { case .leading: return "chevron.backward" case .trailing: return "chevron.forward" } } } private struct PaddleButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .padding() .imageScale(.large) .labelStyle(.iconOnly) } } extension ButtonStyle where Self == PaddleButtonStyle { static var paddle: Self { .init() } }
-
12:34 - Featured Section + Scroll Transition
struct ContentView: View { @State var palettes: [Palette] = [ .init(id: UUID(), name: "Example One"), .init(id: UUID(), name: "Example Two"), .init(id: UUID(), name: "Example Three"), ] var body: some View { ScrollView { GalleryHeroSection(palettes: palettes) } } } struct Palette: Identifiable { var id: UUID var name: String } struct GalleryHeroSection: View { var palettes: [Palette] @State var mainID: Palette.ID? = nil var body: some View { GallerySection(edge: .top) { GalleryHeroContent(palettes: palettes, mainID: $mainID) } label: { GalleryHeroHeader(palettes: palettes, mainID: $mainID) } } } struct GallerySection<Content: View, Label: View>: View { var edge: Edge? = nil @ViewBuilder var content: Content @ViewBuilder var label: Label var body: some View { VStack(alignment: .leading) { label .font(.title2.bold()) content } .padding(.top, halfSpacing) .padding(.bottom, sectionSpacing) .overlay(alignment: .bottom) { if edge != .bottom { Divider().padding(.horizontal, hMargin) } } } var halfSpacing: CGFloat { sectionSpacing / 2.0 } var sectionSpacing: CGFloat { 20.0 } var hMargin: CGFloat { #if os(macOS) 40.0 #else 20.0 #endif } } struct GalleryHeroContent: View { var palettes: [Palette] @Binding var mainID: Palette.ID? var body: some View { ScrollView(.horizontal) { LazyHStack(spacing: hSpacing) { ForEach(palettes) { palette in GalleryHeroView(palette: palette) } } .scrollTargetLayout() } .contentMargins(.horizontal, hMargin) .scrollTargetBehavior(.viewAligned) .scrollPosition(id: $mainID) .scrollIndicators(.never) } var hMargin: CGFloat { 20.0 } var hSpacing: CGFloat { 10.0 } } struct GalleryHeroView: View { var palette: Palette @Environment(\.horizontalSizeClass) private var sizeClass var body: some View { colorStack .aspectRatio(heroRatio, contentMode: .fit) .containerRelativeFrame( [.horizontal], count: columns, spacing: hSpacing ) .clipShape(.rect(cornerRadius: 20.0)) .scrollTransition(axis: .horizontal) { content, phase in content .scaleEffect( x: phase.isIdentity ? 1.0 : 0.80, y: phase.isIdentity ? 1.0 : 0.80) } } private var columns: Int { sizeClass == .compact ? 1 : regularCount } @ViewBuilder private var colorStack: some View { let offsetValue = stackPadding ZStack { Color.red .offset(x: offsetValue, y: offsetValue) Color.blue Color.green .offset(x: -offsetValue, y: -offsetValue) } .padding(stackPadding) .background() } var stackPadding: CGFloat { 20.0 } var heroRatio: CGFloat { 16.0 / 9.0 } var regularCount: Int { 2 } var hSpacing: CGFloat { 10.0 } } struct GalleryHeroHeader: View { var palettes: [Palette] @Binding var mainID: Palette.ID? var body: some View { VStack(alignment: .leading, spacing: 2.0) { Text("Featured") Spacer().frame(maxWidth: .infinity) } .padding(.horizontal, hMargin) #if os(macOS) .overlay { HStack(spacing: 0.0) { GalleryPaddle(edge: .leading) { scrollToPreviousID() } Spacer().frame(maxWidth: .infinity) GalleryPaddle(edge: .trailing) { scrollToNextID() } } } #endif } private func scrollToNextID() { guard let id = mainID, id != palettes.last?.id, let index = palettes.firstIndex(where: { $0.id == id }) else { return } withAnimation { mainID = palettes[index + 1].id } } private func scrollToPreviousID() { guard let id = mainID, id != palettes.first?.id, let index = palettes.firstIndex(where: { $0.id == id }) else { return } withAnimation { mainID = palettes[index - 1].id } } var hMargin: CGFloat { 20.0 } } struct GalleryPaddle: View { var edge: HorizontalEdge var action: () -> Void var body: some View { Button { action() } label: { Label(labelText, systemImage: labelIcon) } .buttonStyle(.paddle) .font(nil) } var labelText: String { switch edge { case .leading: return "Backwards" case .trailing: return "Forwards" } } var labelIcon: String { switch edge { case .leading: return "chevron.backward" case .trailing: return "chevron.forward" } } } private struct PaddleButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .padding() .imageScale(.large) .labelStyle(.iconOnly) } } extension ButtonStyle where Self == PaddleButtonStyle { static var paddle: Self { .init() } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。