Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SwiftUIで二重スクロール作ってみた / When I tried to make a dual-scroll-ish view in SwiftUI

Elvis Shi
January 24, 2024

SwiftUIで二重スクロール作ってみた / When I tried to make a dual-scroll-ish view in SwiftUI

Elvis Shi

January 24, 2024
Tweet

More Decks by Elvis Shi

Other Decks in Programming

Transcript

  1. 4XJGU6*Ͱ ೋॏεΫϩʔϧ࡞ͬͯΈͨ f o r  Ϟ ό ν Ω

     ʙ .PC JMF  5J Q T  ڞ ༗ ձ ʙ   
  2. } var employedBy = "YUMEMI Inc." var job = "iOS

    Tech Lead" var favoriteLanguage = "Swift" var twitter = "@lovee" var qiita = "lovee" var github = "el-hoshino" var additionalInfo = """ ٱʑͷొஃ͗ͯ͢Կ΋͔΋๨ΕͯΔʂ """ final class Me: Developable, Talkable {
  3. *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN

    *UFN *UFN *UFN *UFN εΫϩʔϧͯ͠΋ ը૾ͷҐஔ͸มΘΒͳ͍ ԼʹεΫϩʔϧʹͭΕͯ എܠͷෆಁ໌౓্͕͕Δ
  4. struct BackgroundView: View { var body: some View { LinearGradient(

    colors: [ .mint.opacity(0.5), .cyan, ], startPoint: .top, endPoint: .center ) .ignoresSafeArea() } }
  5. struct PrimaryView: View { var body: some View { VStack

    { Button { } label: { Image(systemName: "house.lodge.fill") .resizable() .scaledToFit() } } .frame(maxHeight: .infinity, alignment: .top) } }
  6. struct PrimaryView: View { @State private var showsDialog = false

    var body: some View { VStack { Button { showsDialog = true } label: { Image(systemName: "house.lodge.fill") .resizable() .scaledToFit() } } .frame(maxHeight: .infinity, alignment: .top) .confirmationDialog("Message", isPresented: $showsDialog, actions: { Text("OK") }, message: { Text("ϋ΢εϘλϯ͕λοϓ͞Ε·ͨ͠") }) } } 0, ϋ΢εϘλϯ͕λοϓ͞Ε·ͨ͠
  7. *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN

    *UFN *UFN *UFN *UFN *UFN *UFN struct OperationScrollView: View { var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.flexible(minimum: 150, maximum: 300)), GridItem(.flexible(minimum: 150, maximum: 300))], content: { ForEach(0 ..< 20) { i in Text("Item: \(i)") .frame(maxWidth: .infinity) .frame(height: 100) .background(Color.blue) } }) .padding(.horizontal) } .background(Color.black.opacity(0.8)) } }
  8. *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN

    struct OperationScrollView: View { var topPadding: CGFloat var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.flexible(minimum: 150, maximum: 300)), GridItem(.flexible(minimum: 150, maximum: 300))], content: { ForEach(0 ..< 20) { i in Text("Item: \(i)") .frame(maxWidth: .infinity) .frame(height: 100) .background(Color.blue) } }) .padding(.horizontal) } .safeAreaPadding(.top, topPadding) .background(Color.black.opacity(0.8)) } }
  9. *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN

    *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN struct OperationScrollView: View { @State var componentsOffset: CGFloat var topPadding: CGFloat private var scrollViewOpacity: CGFloat { max(min((-componentsOffset-10) / topPadding, 0.8), 0) } var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.flexible(minimum: 150, maximum: 300)), GridItem(.flexible(minimum: 150, maximum: 300))], content: { ForEach(0 ..< 20) { i in Text("Item: \(i)") .frame(maxWidth: .infinity) .frame(height: 100) .background(Color.blue) } }) .padding(.horizontal) } .safeAreaPadding(.top, topPadding) .background(Color.black.opacity(scrollViewOpacity )) } } ͯ͞໰୊͸ ͜ͷcomponentsOffsetΛ Ͳ͏΍ͬͯऔΔ͔
  10. public struct PositionReader: ViewModifier { struct PositionKey: PreferenceKey { static

    var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value += nextValue() } } var coordinateSpace: CoordinateSpace var position: KeyPath<CGRect, CGFloat> @Binding var value: CGFloat public func body(content: Content) -> some View { content .background( GeometryReader { geometry in Color.clear .preference(key: PositionKey.self, value: geometry.frame(in: coordinateSpace)[keyPath: position]) } ) .onPreferenceChange(PositionKey.self, perform: { newValue in value = newValue }) } } public extension View { func reading(_ keyPath: KeyPath<CGRect, CGFloat>, in coordinateSpace: CoordinateSpace, andAssignTo value: Binding<CGFloat>) -> some View { self.modifier(PositionReader(coordinateSpace: coordinateSpace, position: keyPath, value: value)) } }
  11. *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN

    *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN struct OperationScrollView: View { @State var componentsOffset: CGFloat var topPadding: CGFloat @Namespace private var scrollView: Namespace.ID private var scrollViewOpacity: CGFloat { max(min((-componentsOffset-10) / topPadding, 0.8), 0) } var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.flexible(minimum: 150, maximum: 300)), GridItem(.flexible(minimum: 150, maximum: 300))], content: { ForEach(0 ..< 20) { i in Text("Item: \(i)") .frame(maxWidth: .infinity) .frame(height: 100) .background(Color.blue) } }) .padding(.horizontal) .reading(\.minY, in: .named(scrollView), andAssignTo: $componentsOffset) } .safeAreaPadding(.top, topPadding) .background(Color.black.opacity(scrollViewOpacity )) .coordinateSpace(.named(scrollView)) } }
  12. *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN struct OperationScrollView:

    View { @State var componentsOffset: CGFloat var topPadding: CGFloat @Namespace private var scrollView: Namespace.ID private var scrollViewOpacity: CGFloat { max(min((-componentsOffset-10) / topPadding, 0.8), 0) } var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.flexible(minimum: 150, maximum: 300)), GridItem(.flexible(minimum: 150, maximum: 300))], content: { ForEach(0 ..< 20) { i in Text("Item: \(i)") .frame(maxWidth: .infinity) .frame(height: 100) .background(Color.blue) } }) .padding(.horizontal) .reading(\.minY, in: .named(scrollView), andAssignTo: $componentsOffset) } .safeAreaPadding(.top, topPadding) .background(Color.black.opacity(scrollViewOpacity)) .coordinateSpace(.named(scrollView)) .refreshable { reload() } } }
  13. struct PrimaryView: View { var topSpacing: CGFloat @State private var

    showsDialog = false var body: some View { VStack { Spacer() .frame(height: max(0, topSpacing)) Button { showsDialog = true } label: { Image(systemName: "house.lodge.fill") .resizable() .scaledToFit() } } .frame(maxHeight: .infinity, alignment: .top) .confirmationDialog("Message", isPresented: $showsDialog, actions: { Text("OK") }, message: { Text("ϋ΢εϘλϯ͕λοϓ͞Ε·ͨ͠") }) } }
  14. *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN

    struct OperationScrollView: View { @State var componentsOffset: CGFloat var topPadding: CGFloat @Namespace private var scrollView: Namespace.ID private var scrollViewOpacity: CGFloat { max(min((-componentsOffset-10) / topPadding, 0.8), 0) } var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.flexible(minimum: 150, maximum: 300)), GridItem(.flexible(minimum: 150, maximum: 300))], content: { ForEach(0 ..< 20) { i in Text("Item: \(i)") .frame(maxWidth: .infinity) .frame(height: 100) .background(Color.blue) } }) .padding(.horizontal) .reading(\.minY, in: .named(scrollView), andAssignTo: $componentsOffset) } .safeAreaPadding(.top, topPadding) .background(Color.black.opacity(scrollViewOpacity)) .coordinateSpace(.named(scrollView)) .refreshable { reload() } } } ͯ͞ࠓ౓ͷ໰୊͸ ͜ͷtopPaddingΛ Ͳ͏΍ͬͯऔΔ͔
  15. struct PrimaryView: View { @Binding var baseImageHeight: CGFloat var topSpacing:

    CGFloat @State private var showsDialog = false var body: some View { VStack { Spacer() .frame(height: max(0, topSpacing)) Button { showsDialog = true } label: { Image(systemName: "house.lodge.fill") .resizable() .scaledToFit() } .reading(\.height, in: .local, andAssignTo: $baseImageHeight) } .frame(maxHeight: .infinity, alignment: .top) .confirmationDialog("Message", isPresented: $showsDialog, actions: { Text("OK") }, message: { Text("Did tap house button") }) } }
  16. struct PrimaryView: View { @Binding var baseImageHeight: CGFloat var topSpacing:

    CGFloat @State private var showsDialog = false var body: some View { VStack { Spacer() .frame(height: max(0, topSpacing)) Button { showsDialog = true } label: { Image(systemName: "house.lodge.fill") .resizable() .scaledToFit() } .reading(\.height, in: .local, andAssignTo: $baseImageHeight) } .frame(maxHeight: .infinity, alignment: .top) .confirmationDialog("Message", isPresented: $showsDialog, actions: { Text("OK") }, message: { Text("Did tap house button") }) } }
  17. *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN

    struct OperationScrollView: View { @Binding var componentsOffset: CGFloat var topPadding: CGFloat @Namespace private var scrollView: Namespace.ID private var scrollViewOpacity: CGFloat { max(min((-componentsOffset-10) / topPadding, 0.8), 0) } var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.flexible(minimum: 150, maximum: 300)), GridItem(.flexible(minimum: 150, maximum: 300))], content: { ForEach(0 ..< 20) { i in Text("Item: \(i)") .frame(maxWidth: .infinity) .frame(height: 100) .background(Color.blue) } }) .padding(.horizontal) .reading(\.minY, in: .named(scrollView), andAssignTo: $componentsOffset) } .safeAreaPadding(.top, topPadding) .background(Color.black.opacity(scrollViewOpacity)) .coordinateSpace(.named(scrollView)) .refreshable { reload() } } }
  18. struct BackgroundView: View { var body: some View { LinearGradient(

    colors: [ .mint.opacity(0.5), .cyan, ], startPoint: .top, endPoint: .center ) .ignoresSafeArea() } }
  19. struct PrimaryView: View { @Binding var baseImageHeight: CGFloat var topSpacing:

    CGFloat @State private var showsDialog = false var body: some View { VStack { Spacer() .frame(height: max(0, topSpacing)) Button { showsDialog = true } label: { Image(systemName: "house.lodge.fill") .resizable() .scaledToFit() } .reading(\.height, in: .local, andAssignTo: $baseImageHeight) } .frame(maxHeight: .infinity, alignment: .top) .confirmationDialog("Message", isPresented: $showsDialog, actions: { Text("OK") }, message: { Text("Did tap house button") }) } }
  20. *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN

    struct OperationScrollView: View { @Binding var componentsOffset: CGFloat var topPadding: CGFloat @Namespace private var scrollView: Namespace.ID private var scrollViewOpacity: CGFloat { max(min((-componentsOffset-10) / topPadding, 0.8), 0) } var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.flexible(minimum: 150, maximum: 300)), GridItem(.flexible(minimum: 150, maximum: 300))], content: { ForEach(0 ..< 20) { i in Text("Item: \(i)") .frame(maxWidth: .infinity) .frame(height: 100) .background(Color.blue) } }) .padding(.horizontal) .reading(\.minY, in: .named(scrollView), andAssignTo: $componentsOffset) } .safeAreaPadding(.top, topPadding) .background(Color.black.opacity(scrollViewOpacity)) .coordinateSpace(.named(scrollView)) .refreshable { reload() } } }
  21. *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN

    struct ContentView: View { @State private var baseImageHeight: CGFloat = 0 @State private var componentsOffset: CGFloat = 0 var body: some View { PrimaryView( baseImageHeight: $baseImageHeight, topSpacing: componentsOffset - baseImageHeight ) .overlay { OperationScrollView( componentsOffset: $componentsOffset, topPadding: baseImageHeight ) } .background { BackgroundView() } }