ํ์ฌ์์ ์ฐธ์ฌ์ค์ธ ํ๋ก์ ํธ์์ ์คํฌ๋กค๋ทฐ๋ก ๊ตฌํ๋์ด ์๋ ๋ถ๋ถ์ ํ์ด์ง ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ฌ๋ผ๋ ๊ธฐํ์ ์์ ์ด ์์๋ค.
UIKit์ผ๋ก ๊ตฌํ๋์ด ์์์ผ๋ฉด UIPageViewController๋ฅผ ์ฌ์ฉํ๋ฉด ๋๊ฒ ์ง๋ง, ํ๋ก์ ํธ๋ฅผ SwiftUI ์ค์ฌ์ผ๋ก ์์ ์ค์ธ ์ํฉ์ด๋ผ ์๊ฐ๋ณด๋ค ๊ธฐํ์๋๋ก ๊ฐ๋ฐํ๋ ๊ฒ ๊น๋ค๋ก์ ๋ค.
๊น๋ค๋ก์ ๋ ์ด์ ๋ ๋ ๊ฐ์ง๊ฐ ์์๋๋ฐ,
1. SwiftUI์ ScrollView๋ ์ปค์คํ ์ด ์ ํ์ ์ด๋ผ๋ ์
2. ํ์ฌ ํ๋ก์ ํธ์ ์ต์ ๋ฒ์ ์ด 15์ด์์ธ ์
๋๋ฌธ์ด์๋ค.
๊ทธ๋๋ iOS 17์ด์๋ถํฐ ์ ํ์์ ScrollView์ ๊ธฐ๋ฅ์ ๋ํญ ์ถ๊ฐ ๋ฐ ๊ฐ์ ํด์ฃผ์ด์ 17 ์ด์๋ถํฐ๋ ์ฌ์ค์ ๋ฌธ์ ๊ฐ ์์๋๋ฐ, ๋ฌธ์ ๋ ScrollView์ ๋ฒ์ ๋์์ด์๋ค.
https://developer.apple.com/support/app-store
๋๋ต 15~20%์ ์ฌ๋๋ค์ iOS 17 ๋ฏธ๋ง์ ์ฌ์ฉํ๊ณ ์์๊ณ , ๊ธฐ์ ์ ์ฅ์์ ์๋ฌด๋๋ ๋์น๊ธฐ์ ์๊น์ด ์์ด๊ธฐ์ ...... ๋ฒ์ ๋์์ ํด์ค์ผํ๋ค.
์ฌ๋ฌ ๊นํ์ด๋ ์ฝ๋๋ค์ ์ฐธ๊ณ ํด์ ๊ตฌํํ๊ธฐ ๋๋ฌธ์ ๋๋ ์ ๋ฆฌํ๋ฉด์ ์ต์ข ์ ์ผ๋ก ์ดํดํ๋ ์๊ฐ์ด ํ์ํ๊ธฐ๋ ํ๊ณ , ํญ๋ทฐ๋ฅผ ์จ์ผ์ง! ํ๋ค๊ฐ ์ ์ ์ฌ๋ฐฑ์ ์ข์ฐ ์์ดํ ์ด ๋ณด์ฌ์ผํ๋ ์กฐ๊ฑด์ ์ถฉ์กฑ์์ผ์ผํด์ ์ฌ๊ธฐ์ ๊ธฐ ์ํ ์ฝ๋๋ฅผ ์ฐธ๊ณ ํ๊ณ ๋ง๋ค๋ฉฐ ์ฝ์ง์ ํ์๊ธฐ์,
์ ๋ฆฌํ๊ฒ ๋,
iOS 17 ์ด์ ํ์ด์ง ๊ธฐ๋ฅ๊ณผ iOS 17๋ฏธ๋ง์์ ํ์ด์ง ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ!
1. iOS 17 ๋ฏธ๋ง์์ ์คํฌ๋กค ๋ทฐ ์์ด ์คํฌ๋กค ๋ทฐ + ํ์ด์ง ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
- scrollView๋ฅผ ์ฌ์ฉํ๋ ๋์ gesture๋ฅผ ํ์ฉํ ๋ฐฉ์
๊ตฌํ ์ฝ๋:
import SwiftUI
struct CarouselViewSwiftUI: View {
/// Carousel์์ ๋ณด์ฌ์ค ์์ ๋ฐฐ์ด
let colors: [Color] = [.red, .blue, .green, .pink, .purple]
/// ๋๋๊ทธ ์คํ์
์ ์ฅ
@State var dragOffset: CGFloat = .zero
/// ํ์ฌ ์ธ๋ฑ์ค ์ ์ฅ
@State var currentIndex: Int = 0
/// ์์ดํ
์ฌ์ด ๊ฐ๊ฒฉ
let itemSpacing: CGFloat = 12
var body: some View {
/// ์์ดํ
๋๋น
let itemWidth = 200
/// ํ์ฌ ์ธ๋ฑ์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ดํ
์คํ์
๊ณ์ฐ
let currentOffset = CGFloat(currentIndex) * (CGFloat(itemWidth) + itemSpacing)
GeometryReader { proxy in
HStack(alignment: .center, spacing: itemSpacing) {
ForEach(colors, id: \.self) { color in
color
.frame(width: CGFloat(itemWidth), height: 300)
.cornerRadius(12)
} // forEach
} // hStack
// ๋๋๊ทธ ์ ์ค์ฒ ์ถ๊ฐ
.gesture(dragGesture)
// ๋๋๊ทธ ์คํ์
์ ๋ฐ๋ผ ์์ดํ
์ด๋
.offset(x: 24 + dragOffset - currentOffset)
.frame(width: proxy.size.width,
height: proxy.size.height,
alignment: .leading)
} // geo
.frame(height: 96)
} // body
} // view
extension CarouselViewSwiftUI {
// ๋๋๊ทธ ์ ์ค์ฒ ์ ์
private var dragGesture: some Gesture {
DragGesture()
.onChanged { newValue in
// ๋๋๊ทธ์ค์ธ ์คํ์
์ค์๊ฐ ์
๋ฐ์ดํธ
dragOffset = newValue.translation.width
} // onChanged
.onEnded { endValue in
// - ๋๋๊ทธ๊ฐ ๋๋ฌ์ ๋์ ์ฒ๋ฆฌ
/// ๋๋ ๊ทธ ์ธ์ ๊ฐ
let threshold: CGFloat = 30
/// ์ธ๋ฑ์ค ๊ฐฑ์
var newIndex = currentIndex
/// ๋๋๊ทธ์ ๋ฐฉํฅ์ ๋ฐ๋ผ ์ธ๋ฑ์ค ๋ณ๊ฒฝ (์์ดํ
์ข์ฐ ์ด๋)
if endValue.translation.width > threshold {
newIndex -= 1
} else if endValue.translation.width < -threshold {
newIndex += 1
}
/// ๋ฐ๋ ์ธ๋ฑ์ค ๋ฒ์ ์กฐ์
newIndex = max(min(newIndex, colors.count - 1), 0)
/// ์ธ๋ฑ์ค ์
๋ฐ์ดํธ + ๋๋๊ทธ ์คํ์
์ด๊ธฐํ
withAnimation(.spring()) {
currentIndex = newIndex
dragOffset = .zero
}
} // onEnded
}
}
์ ์ค์ฒ๋ฅผ ํธ๋ฆฌ๊ฑฐ๋ก ๋๋๊ทธ ์คํ์ ๊ณผ ํ์ฌ ์ธ๋ฑ์ค๋ฅผ ๊ด์ฐฐํ์ฌ ์ค์๊ฐ์ผ๋ก ๋ฐ๋ ๊ฐ์ ์ ์ฅ ๋ฐ ์ ๋ฐ์ดํธ ํ๋ ๋ฐฉ์์ด๋ค.
1-1. GeometryReader ์ฌ์ฉ
: GeometryReader๋ ์์๋ทฐ๋ ์ ์ฒด ๋ทฐ ํฌ๊ธฐ์ ๋ฐ๋ผ ํ์๋ทฐ์ ํฌ๊ธฐ ๋ฐ ์์น๋ฅผ ์ ํ ๋ ์ฃผ๋ก ์ฌ์ฉํ๋ค. GeometryReade์ proxy๋ก ์ ์ฒด ๋ทฐ์ ๋ง์ถฐ์ ์คํฌ๋กค ๊ฐ๋ฅํ ์์ญ์ ๋๋น(proxy.size.width)๋ฅผ ๋ง์ถ๊ณ , ๋๋๊ทธ ์ ๊ณ์ฐํ offset ๊ฐ์ ๋ง๋ color ์ธ๋ฑ์ค๋ก ๋ฐ๋๋๋ก ํ๋ค.
1-2. ์คํฌ๋กค ์ก์ ์ ์ํด offset ํ์ฉ
: offset์ ๋๋๊ทธํ ํฌ๊ธฐ์ ํ์ฌ ์ธ๋ฑ์ค์ ์คํ์ ๊ฐ์ ๋นผ์ฃผ์๋๋ฐ, ์ฌ๊ธฐ์ 24๋ฅผ ๋ํด๋์ ์ด์ ๋ leading ํจ๋ฉ๊ฐ์ ์์์ ์ผ๋ก ์ฃผ๊ธฐ ์ํด์๋ค. ์๋๋นผ๋ฉด tabView๋ฅผ ์ด ๊ฒ์ฒ๋ผ ๋๋๊ทธ ์ก์ ์ด ์ผ์ด๋๋ฉด color๊ฐ ์ผ์ชฝ์ ๋ฑ ๋ถ๋๋ค. scrollView๋ฅผ ํ์ฉํ ์ ์์๋ค๋ฉด ํด๋น ํจ๋ฉ์ ์ฃผ๊ธฐ ์ํด์ LazyHStack์ด๋ HStack์ 24์ ํจ๋ฉ๊ฐ์ ์ฃผ์์ ๊ฒ.
1-3. gesture ์ฌ์ฉ
์ฌ์ค gesture๋ฅผ ์ฌ์ฉํ๋ ๊ฒ scrollView๋ฅผ ์ฐ์ง ์์ pager ๊ตฌํ์ ํต์ฌ์ด๋ค.
.onChanged { newValue in
// ๋๋๊ทธ์ค์ธ ์คํ์
์ค์๊ฐ ์
๋ฐ์ดํธ
dragOffset = newValue.translation.width
}
๋๋๊ทธ ๋ ์คํ์ ์ ๋๋๊ทธ ์ ์ค์ฒ๋ก ๋ณํ๋ ๊ฐ์ translation.width๋ก ์ ์ฅํ๊ณ ์๋๋ฐ, translation๊ฐ ๋ฌด์จ ๊ฐ์ธ๊ฐ! ์ถ์ ์ ์๋ค.
์ ํ๋ฌธ์์์ translation์ instance property๋ก, The total translation from the start of the drag gesture to the current event of the drag gesture. ....๋ผ๊ณ ์ ์๋ฅผ ํด์ฃผ์๋๋ฐ, ๋๋๊ทธ๋ฅผ ์์ํ ์ง์ ์์ ํ์ฌ ๋๋๊ทธ ์์น๊น์ง์ ์ด๋ ๊ฑฐ๋ฆฌ๋ ์๋ฏธ๋ค.
์ฆ, translation.width๋ ๋๋๊ทธ๋ฅผ ์์ํ ์ง์ ์์ ํ์ฌ ๋๋๊ทธ ์์น๊น์ง ์ํ์ผ๋ก ์ด๋ํ ๊ฑฐ๋ฆฌ์ด๊ณ , translation.height์ ์์ง์ผ๋ก ์ด๋ํ ๊ฑฐ๋ฆฌ๋ค.
์ฌ๊ธฐ์ ๋๋ HStack์ ์ฐ๊ณ ์๊ณ , ๋๋๊ทธ ์ก์ ์ด ์ข์ฐ๋ก ์ผ์ด๋ ๊ฑฐ๋ผ translation.width๋ฅผ ์ฌ์ฉํ๋ค. ๊ทธ๋์ ๋๋๊ทธ ์ก์ ์ด ์ผ์ด๋ ๋๋ง๋ค ๋๋๊ทธ ์คํ์ ์ translation.width๊ฐ์ผ๋ก ์ ์ฅํด ์ด๋์์ผฐ๋ค.
https://developer.apple.com/documentation/swiftui/draggesture/value/translation
.onEnded { endValue in
/// ๋๋ ๊ทธ ์ธ์ ๊ฐ
let threshold: CGFloat = 30
/// ์ธ๋ฑ์ค ๊ฐฑ์
var newIndex = currentIndex
/// ๋๋๊ทธ์ ๋ฐฉํฅ์ ๋ฐ๋ผ ์ธ๋ฑ์ค ๋ณ๊ฒฝ (์์ดํ
์ข์ฐ ์ด๋)
if endValue.translation.width > threshold {
newIndex -= 1
} else if endValue.translation.width < -threshold {
newIndex += 1
}
/// ๋ฐ๋ ์ธ๋ฑ์ค ๋ฒ์ ์กฐ์
newIndex = max(min(newIndex, colors.count - 1), 0)
/// ์ธ๋ฑ์ค ์
๋ฐ์ดํธ + ๋๋๊ทธ ์คํ์
์ด๊ธฐํ
withAnimation(.spring()) {
currentIndex = newIndex
dragOffset = .zero
}
} // onEnded
๊ทธ๋ฆฌ๊ณ ๋๋๊ทธ ์ก์ ์ด ๋๋๋ฉด ๊ฐฑ์ ๋์ผํ๋ ๊ฐ๋ค์ onEnded์ ์์ฑํ๋ฉด ๋๋ค.
2. iOS 17 ๋ฏธ๋ง์์ ์คํฌ๋กค ๋ทฐ๋ฅผ ์จ์ ํ์ด์ง ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
์ ์ค์ฒ๋ก ๋ง๋ค๋ฉด ๋ค ๋๋ ๊ฑฐ ์๋์ผ? ์ถ์๋ ๋ด๊ฒ ์คํฌ๋กค ๋ทฐ๋ฅผ ๊ผญ ์จ์ผํ๋ ์ํฉ์ด ์๊ฒผ๋ค. ๋ฐ๋ก, ์ก์ ์ด ์ผ์ด๋ฌ์ ๋ ์์ดํ ์์น์กฐ์ ๋ฅผ ์๋์ผ๋ก ์กฐ์ ํด์ฃผ์ด์ผ ํ ๋. ๋ณดํต ์ด๋ฐ ๊ฒฝ์ฐ scrollViewReader์ proxy.scrollTo(id, anchor: .center) ๋ฑ์ ์จ์ ์กฐ์ ํด์ฃผ๋๋ฐ, 1์ ๋ฐฉ๋ฒ์ ์ฐ๋ฉด ์ด ์ฌ์ด ๋ฐฉ๋ฒ์ ์ธ ์๊ฐ ์์๋ค.
๊ทธ๋์ ์ด ๋ถ๋ถ์ ํ์ฅ๋๊ณผ ๊ณ ๋ฏผ์ ํ๋ค๊ฐ ์ฝ๋์ ๊ฐ๊ฒฐ์ฑ์ ์ํด
https://github.com/izakpavel/SwiftUIPagingScrollView
์ ๊นํ์ PagingScrollView ํ์ผ์ ์ฐ๊ธฐ๋ก ํด์ ์คํฌ๋กค๋ทฐ๊ฐ ์ ๊ณตํด์ฃผ๋ api๋ฅผ ํ์ฉํด์ผํ๋ ํ์ด์ง ๋ถ๋ถ์ ํด๋น ์ฝ๋๋ก ๋์ฒดํด์ ์ฌ์ฉํ๋ค.
- ํ์ด์ง ์ธ๋ฑ์ค
- ์์ดํ ๋๋น
- ์์ดํ ํจ๋ฉ (๊ฐ๊ฒฉ)
๋ง ์๋ฉด PagingScrollView ๋ด์ ์ ๋๋ฉ์ด์ ์ ์์ ํด์ ์ฌ์ฉํ๋ฉด ๋ณต์กํ ๊ตฌํ๋ ํผํ๊ณ , ์ฝ๋๋ฅผ ๋ฏ์ด ์ปค์คํ ํ ์๋ ์๋ค.
3. iOS 17 ์ด์ ์คํฌ๋กค ๋ทฐ๋ก ํ์ด์ง ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
๊ทธ๋ฆฌ๊ณ iOS 17๋ถํฐ๋ ์ ํ์ด scrollTargetLayout์ scrollTargetBehavior๋ฅผ ์จ์ ์์ฃผ ๊ฐ๊ฒฐํ๊ฒ ๊ตฌํํ ์ ์๋๋ก ScrollView API๋ฅผ ์ถ๊ฐํด์ฃผ์๋ค.
์ api ๊ธฐ๋ฅ๋ค์ ๋์ฒด ๋ญ๊ณ , ์ด๋ป๊ฒ ์ ์ฉํ ์ ์์๊น?
์ ๋ํด์ ์๋ ๊ฒ์๊ธ์ ๋ฐ๋ก ์ ๋ฆฌํ๋ค.
https://calliek.tistory.com/59
References
https://www.hackingwithswift.com/books/ios-swiftui/moving-views-with-draggesture-and-offset
'๐ Dev > ๊ตฌํ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[SwiftUI] Infinite Carousel ๊ตฌํํ๊ธฐ 2 (feat.Timer) (0) | 2024.09.11 |
---|---|
[SwiftUI] Infinite Carousel ๊ตฌํํ๊ธฐ 1 (feat. Timer) (0) | 2024.09.05 |
[SwiftUI] CustomPopUpView ์ ๋๋ฉ์ด์ ํจ๊ณผ ํด๊ฒฐํ๊ธฐ (0) | 2024.06.27 |
[SwiftUI] ์ค์ ๋ก ์น์ ์ ์๋คํ๋ค ๊ตฌํํ๊ธฐ (0) | 2024.06.21 |
[SwiftUI] pre/next buttons๊ฐ ์๋ ์ด๋ฏธ์ง ์ฌ๋ผ์ด๋ ๊ตฌํํ๊ธฐ (2) | 2024.05.16 |