Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
SwiftUIで二重スクロール作ってみた / When I tried to make a d...
Search
Elvis Shi
January 24, 2024
Programming
380
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
SwiftUIで二重スクロール作ってみた / When I tried to make a dual-scroll-ish view in SwiftUI
Elvis Shi
January 24, 2024
More Decks by Elvis Shi
See All by Elvis Shi
@Environment(\.keyPath)那么好我不允许你们不知道! / atEnvironment keyPath is so good and you should know it!
lovee
0
480
ゼロから始めるPreferenceの実装 / Let's implement Preferences from scratch
lovee
0
150
Kotlin エンジニアへ送る:Swift 案件に参加させられる日に備えて~似てるけど色々違う Swift の仕様 / from Kotlin to Swift
lovee
1
390
個人アプリを2年ぶりにアプデしたから褒めて / I just updated my personal app, praise me!
lovee
0
740
How did I build an Open-Source SwiftUI Toast Library
lovee
1
170
SwiftUIで使いやすいToastの作り方 / How to build a Toast system which is easy to use in SwiftUI
lovee
3
1.3k
Observation のあれこれ / A brief introduction about Observation
lovee
3
440
ChatGPT 時代の勉強 / Learning under ChatGPT era
lovee
27
9k
属人化しない為の勉強会作り / To make tech meetups with less personal dependencies
lovee
0
370
Other Decks in Programming
See All in Programming
地域 SRE コミュニティ最前線 - ホンマでっかSRE勉強会
tk3fftk
0
120
「AIで開発し、AIを届ける」をEvalでつなぐ 〜AIネイティブに始めるプロダクト開発の実践〜 / Connecting "Develop with AI, deliver AI" with Eval
rkaga
4
5.7k
どこまでゆるくて許されるのか
tk3fftk
0
390
AI 輔助遺留系統現代化的經驗分享
jame2408
1
1.2k
Hunting Vulnerabilities in Symfony with LLMs
vinceamstoutz
0
570
Language Server 使ってる? 〜VSCode と Zed の場合〜 / Are you using a Language Server? ~For VS Code and Zed~
handlename
0
830
AI駆動開発を妨げる技術的負債の解消アプローチ / ai-refactoring-approach
minodriven
17
8.7k
その問い、本当に正しいですか?AI時代のエンジニアに必要な哲学と認知科学 / ai-philosophy-cognitive-science
minodriven
14
6.7k
なぜ型を書くのか? TSKaigi2026で改めて考える #tskaigi_smarthr
kajitack
0
210
キャリア迷子上等 ─ "ない道"は自分で作ればいい
16bitidol
3
2.8k
Hatena Engineer Seminar #37「言語モデルの活用に関する研究」
slashnephy
0
490
20260623_Loop Engineeringで自分の分身の問い合わせBotを作る
ryugen04
0
130
Featured
See All Featured
Stop Working from a Prison Cell
hatefulcrawdad
274
21k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
31
10k
I Don’t Have Time: Getting Over the Fear to Launch Your Podcast
jcasabona
34
2.8k
Money Talks: Using Revenue to Get Sh*t Done
nikkihalliwell
0
280
エンジニアに許された特別な時間の終わり
watany
107
250k
Darren the Foodie - Storyboard
khoart
PRO
3
3.4k
AI: The stuff that nobody shows you
jnunemaker
PRO
8
770
Evolving SEO for Evolving Search Engines
ryanjones
0
240
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
234
17k
Docker and Python
trallard
47
3.9k
Done Done
chrislema
186
16k
GraphQLとの向き合い方2022年版
quramy
50
15k
Transcript
4XJGU6*Ͱ ೋॏεΫϩʔϧ࡞ͬͯΈͨ f o r Ϟ ό ν Ω
ʙ .PC JMF 5J Q T ڞ ༗ ձ ʙ
} 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 {
None
*UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN εΫϩʔϧͷॳظҐஔΛ ը૾ͷԼʹ
*UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN
*UFN *UFN *UFN *UFN εΫϩʔϧͯ͠ ը૾ͷҐஔมΘΒͳ͍ ԼʹεΫϩʔϧʹͭΕͯ എܠͷෆಁ໌্͕͕Δ
*UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN ԼʹΦʔόʔεΫϩʔϧ͚ͨ࣌ͩ͠ ը૾ҰॹʹԼ͕Γ·͢
ϦϑϨογϡϓϩάϨε දࣔ͠·͢
*UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN എܠࣗମৗʹෆಈ
*UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN 0, ϋεϘλϯ͕λοϓ͞Ε·ͨ͠
࣮ͨͩͷը૾Ͱͳ͘ ϘλϯͰͨ͠ʂ
*UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN എܠ ϝΠϯϘλϯ
εΫϩʔϧϏϡʔ
struct BackgroundView: View { var body: some View { LinearGradient(
colors: [ .mint.opacity(0.5), .cyan, ], startPoint: .top, endPoint: .center ) .ignoresSafeArea() } }
struct PrimaryView: View { var body: some View { VStack
{ Button { } label: { Image(systemName: "house.lodge.fill") .resizable() .scaledToFit() } } .frame(maxHeight: .infinity, alignment: .top) } }
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, ϋεϘλϯ͕λοϓ͞Ε·ͨ͠
*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)) } }
*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)) } }
*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Λ Ͳ͏ͬͯऔΔ͔
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)) } }
*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)) } }
*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() } } }
*UFN *UFN *UFN *UFN *UFN *UFN *UFN *UFN ԼʹΦʔόʔεΫϩʔϧ͚ͨ࣌ͩ͠ ը૾ҰॹʹԼ͕Γ·͢
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("ϋεϘλϯ͕λοϓ͞Ε·ͨ͠") }) } }
*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)) .refreshable { reload() } } } ͯ͞ࠓͷ ͜ͷtopPaddingΛ Ͳ͏ͬͯऔΔ͔
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") }) } }
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") }) } }
*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() } } }
struct BackgroundView: View { var body: some View { LinearGradient(
colors: [ .mint.opacity(0.5), .cyan, ], startPoint: .top, endPoint: .center ) .ignoresSafeArea() } }
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") }) } }
*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() } } }
*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() } }
%FNP
͜ͷ࣮ʹͪΐͬͱ͋Γ·͢! w ݱ࣌ͰSFGSFTIBCMFʹBTZODॲཧΛೖΕΒΕͳ͍ w ಠࣗͰ3FGSFTI"DUJPOʹରԠ͢Ε͍͍ w IUUQTEFWFMPQFSBQQMFDPNEPDVNFOUBUJPO TXJGUVJSFGSFTIBDUJPO
͜ͷ࣮ʹͪΐͬͱ͋Γ·͢! w TBGF"SFB1BEEJOHʹෆ߹͕͋Δ w άϧάϧͷҐஔ͕Լ͕͍ͬͯΔ w ্ʹεΫϩʔϧͨ͠ࡍ4BGF"SFBൣғͰεΫϩʔ ϧϏϡʔʹ͋ΔϘλϯ͕λοϓͰ͖ͳ͍ w ΘΓʹεΫϩʔϧϏϡʔͷҰ൪্ʹ4QBDFSΛೖΕ
Δํ๏ߟ͑ΒΕΔ w ͦͷ߹εΫϩʔϧϏϡʔͷԼͷ1SJNBSZ7JFXͷ Ϙλϯ͕λοϓͰ͖ͳ͍
4XJGU6*໘ͤ͐͘
4XJGU6*໘ͤ͐͘ ୭͔ॿ͚͍ͯͩ͘͞ʂ