프로젝트 진행 중 내가 맡은 ui에서 슬라이드뷰를 만들어야 했는데,
피그마에 올라와 있는 디자인 가이드가 위 이미지와 같았다. 따라서 SwiftUI에서 Tabview가 제공해주는 원형 인디케이터를 사용할 수가 없었고, custom 형식으로 직접 구현해야했다.
내가 구현해야 하는 것:
1. 이미지 슬라이더
2. 일정시간 간격을 두고 슬라이더의 이미지가 바뀌어야 함
3. 이미지가 바뀔 때, 해당 이미지 순서의 인디케이터 모양이 원형에서 가로로 긴 타원형으로 변경되어야 함
그래서
https://www.youtube.com/watch?v=uo8gj7RT3H8
위 영상을 참고하여 코드를 작성했었다.
iOS 15기준으로 위 영상의 코드로 구현을 하면 index가 0에서 1을 넘어갈 경우 외에는 슬라이더 기능이 되지 않았다.
iOS 15이후 부터 발생하는 문제로, 영상 속 코드는 슬라이드가 넘어가면 인덱스가 인식 되지 않고 사라지는 게 기능 불능의 원인이었다.
해결 방법:
- 슬라이드 배열의 인덱스를 인식하지 못 하고 있는 게 문제였기 때문에, 인덱스를 인식 시켜주고, 그 인덱스 기준으로 슬라이드 이미지 교체 + 인디케이터 형태 custom 해주는 식으로 방향을 잡았다.
- 그래서 (1) currentIndext에 현재 페이지 인덱스를 저장하는 @state 형식으로 선언해주고, (2) currentIndex를 기준으로 오프셋 변경 여부를 구하고, (3) tag로 확실하게 저장하고, (4) 인디케이터 형태를 그에 맞게 바꿔줬다.
아래는 영상을 기반으로 작성한 코드를 개선한 코드이자 내가 원한 UI 및 기능 구현을 한 코드다.
기타 설정 등은 주석 참고.
import SwiftUI
struct MainEventPopupSlidesView: View {
// 타이머 설정
private let timer = Timer.publish(
every: 3,
on: .main,
in: .common
).autoconnect()
// 이미지 배열
private let images: [String] = [
"bottle_coffee1",
"bottle_coffee2",
"bottle_coffee3"
]
@State var offset: CGFloat = 0 // 현재 슬라이드 오프셋 저장
@State private var currentIndex = 0 //현재 페이지 인덱스
var body: some View {
ScrollView(.init()){
TabView(selection: $currentIndex){ // 현재 선택 항목 추적용
ForEach(images.indices, id: \.self) { index in
if index == 0 {
// 오프셋 구하기용
Image(images[index])
.resizable()
.scaledToFill()
.overlay(
// offset 계산
GeometryReader { proxy -> Color in
let minX = proxy.frame(in: .global).minX
DispatchQueue.main.async {
withAnimation(.default) {
self.offset = -minX
}
}
return Color.clear
}
.frame(width: 0, height: 0)
, alignment: .leading
)
.tag(index)
} else {
Image(images[index])
.resizable()
.scaledToFill()
.tag(index)
}
}
}
.tabViewStyle(
PageTabViewStyle(
indexDisplayMode: .never
)
)
// 페이지 인디케이터
.overlay(
HStack(spacing: 4) {
ForEach(images.indices, id: \.self) { index in
Capsule()
// 캡슐 디자인
// 선택된 페이지와 일치하는 경우 채우기 색 설정
.stroke(.uiColorGray350, lineWidth: 1)
// 선택 인덱스 기준 크기 변경
.frame(width: getIndex() == index ? 16 : 6, height: 6)
//선택 인덱스 따라 투명도 변경
.opacity(getIndex() == index ? 1 : 0.5)
.background(getIndex() == index ? .uiColorGray350 : Color.clear)
.cornerRadius(3, corners: .allCorners)
}
}
.padding(.bottom, 24), alignment: .bottom
)
}
.ignoresSafeArea()
// 페이지 변경 될 때 오프셋 업데이트
.onChange(of: currentIndex) { newIndex in
let newOffset = CGFloat(newIndex) * getWidth()
withAnimation {
offset = newOffset
}
}
// 타이머 설정
.onReceive(timer) { _ in
let newIndex = currentIndex < images.count - 1 ? currentIndex + 1 : 0
currentIndex = newIndex
}
}
/// 현재 페이지의 인덱스를 가져옴
func getIndex() -> Int {
let index = Int(round(Double(offset / getWidth())))
return index
}
/// 오프셋 값 가져옴
func getOffset() -> CGFloat {
let progress = offset / getWidth()
return 22 * progress
}
}
extension View {
/// 화면 너비 가져옴
func getWidth() -> CGFloat {
return UIScreen.main.bounds.width
}
}
결과물 (이미지 캡처):
캡처한 이미지 형태로 슬라이드가 잘 작동되는 걸 확인할 수 있다.
References
https://seons-dev.tistory.com/entry/SwiftUI-TabView
'iOS > 구현' 카테고리의 다른 글
[SwiftUI] 스유로 섹션 접었다폈다 구현하기 (0) | 2024.06.21 |
---|---|
[SwiftUI] pre/next buttons가 있는 이미지 슬라이더 구현하기 (2) | 2024.05.16 |