요즘 회사에서 스유로 리팩토링하는 작업을 담당하고 있는데, 섹션 폴딩을 구현해야할 일이 생겼었다.
찾아보면 list를 활용해서 만드는 예시 위주만 나와서 내가 구현하고 하는 것과 맞지 않았다. 내가 구현하고자 한 폴딩 부분은 디자인 가이드상 컬렉션뷰로 구현해야 나오는 뷰였기 때문에 GirdItem을 활용해서 커스텀으로 섹션을 만들어 쓰기로 했다.
마침 참고하기 좋은 해외 포스트를 발견해서 참조했다.
구현
1. 섹션 상태를 섹션 이름으로 구별해서 folding 기능을 토글처럼 값을 관찰하는 ObservableObject를 만든다.
- ObservableObject 클래스를 사용해서 값의 변화를 관찰하도록 했다.
- @Published로 sections이란 딕셔너리를 만들어서 섹션 이름과 bool값으로 섹션이 열렸는지 닫혔는지를 관찰한다. @Published의 특성 상 값의 변화가 감지되면 곧바로 뷰를 업데이트 해준다.
- 프로젝트 디자인 가이드상 섹션이 열려 있어야해서 init에서 딕셔너리 값을 true로 반환해서 열어뒀다. (그런데 코드 상 true가 default라 열려있긴 했다.)
import SwiftUI
// 섹션 상태를 관리하는 모델
class TigersSectionModel: ObservableObject {
@Published var sections: [String: Bool]
let constants = TigersMenuList.Constants()
init() {
self.sections = [
constants.outfield: true,
constants.staff: true,
constants.infield: true,
constants.peacher: true
]
}
func isOpen(title: String) -> Bool {
return sections[title] ?? false
}
func toggle(title: String) {
let current = sections[title] ?? false
sections[title] = !current
}
}
2. 커스텀으로 섹션헤더뷰 만들기
- 이제 sectionModel을 활용해서 커스텀한 섹션 뷰를 만들어준다.
- TigersSectionModel을 @StateObject로 관찰중인 객체에 변화를 감지하면 뷰를 업데이트하게 해준다. 참고했던 블로그에서는 @ObservedObject로 설정해두었는데, 그부분을 @StateObject로 변경했다.
- 섹션 부분을 탭하면 폴딩 기능이 되면서 화살표 방향도 바뀌어야해서, contentShape으로 섹션 부분을 잡아주고 그 부분을 터치영역이 될 수 있게 해주었다.
- 터치하면 모델의 toggle값이 바뀌어서 폴딩의 open/close 값을 업데이트하고 뷰갱신 할 수 있게 했다. (이 부분을 @StateObject가 해준다) 그리고 섹션을 이름으로 나누어서 이름과 동일한 섹션을 탭했을 때 값을 갱신 시켰다.
import SwiftUI
struct TigersSectionHeader: View {
var title: String
@StateObject var model: TigersSectionModel
var body: some View {
HStack {
Text(title)
.font(Font.system(size: 18, weight: .medium))
.foregroundStyle(.black)
Spacer()
Image(systemName: model.isOpen(title: title) ? "chevron.up" : "chevron.down")
}
.contentShape(Rectangle())
.onTapGesture {
self.model.toggle(title: self.title)
}
}
}
3. ListView에서 1,2 뷰를 합쳐서 활용하기
ListView가 contentView역할을 한 예제 코드이므로, 전체 코드는 아래와 같다.
(해당 코드는 단순히 UI 및 로직 구현을 위한 코드로, 중복 코드를 간소화 하지 않음)
import SwiftUI
struct TigersMenuList: View {
let constants = Constants()
// Section Expandable Model
@StateObject var sectionModel = TigersSectionModel()
// Cell
let gridItems = [
GridItem(.flexible(), alignment: .leading),
GridItem(.flexible(), alignment: .leading)
]
var body: some View {
ScrollView {
VStack {
Group {
// MARK: - 감독
VStack {
TigersSectionHeader(title: constants.staff, model: sectionModel)
if sectionModel.isOpen(title: constants.staff) {
LazyVGrid(columns: gridItems, spacing: 16) {
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy6)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
}
}
}
.padding(.vertical, 24)
Divider()
// MARK: - 외야수
VStack {
TigersSectionHeader(title: constants.outfield, model: sectionModel)
if sectionModel.isOpen(title: constants.outfield) {
LazyVGrid(columns: gridItems, spacing: 16) {
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy1)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy2)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy3)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy4)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy5)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
}
}
}
.padding(.top, 32)
.padding(.bottom, 24)
Divider()
// MARK: - 내야수
VStack {
TigersSectionHeader(title: constants.infield, model: sectionModel)
if sectionModel.isOpen(title: constants.infield) {
LazyVGrid(columns: gridItems, spacing: 16) {
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy7)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy8)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy9)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy10)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
}
}
}
.padding(.vertical, 24)
Divider()
// MARK: - 투수
VStack {
TigersSectionHeader(title: constants.peacher, model: sectionModel)
if sectionModel.isOpen(title: constants.peacher) {
LazyVGrid(columns: gridItems, spacing: 16) {
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy11)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy12)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
Button(action: {
print("버튼 동작")
}) {
Text(constants.dummy13)
.font(Font.system(size: 16, weight: .regular))
.foregroundStyle(.gray)
}
}
}
}
.padding(.top, 24)
.padding(.bottom, 32)
} // * group {}
.padding(.horizontal, 20)
} // * VStack
.background(Color.white)
.cornerRadius(16, corners: .allCorners)
// 섹션 폴딩 애니메이션
.animation(.linear(duration: 0.1), value: self.sectionModel.sections)
} // *scroll {}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
extension TigersMenuList {
struct Constants {
// 주문
let outfield = "외야수"
let dummy1 = "나성범"
let dummy2 = "최원준"
let dummy3 = "테스형"
let dummy4 = "김호령"
let dummy5 = "이창진"
// 감독
let staff = "감독"
let dummy6 = "이범호"
// 내야수
let infield = "내야수"
let dummy7 = "김선빈"
let dummy8 = "이우성"
let dummy9 = "박찬호"
let dummy10 = "김도영"
// 투수
let peacher = "투수"
let dummy11 = "양현종"
let dummy12 = "네일"
let dummy13 = "정해영"
}
}
#Preview {
TigersMenuList()
}
- Vstack으로 만들어서 섹션과 lazyVGrid로 list처럼 보이게 했다.
- 위처럼 구현하면 섹션별로 접었다펼쳤다를 커스텀으로 구현할 수 있다.
References
- https://www.keaura.com/blog/swiftui-collapsable-lists
Collapsable Section Headers in SwiftUI — KeAura.com
Have a SwiftUI list with a lot of items? Break it into sections and make each section collapsable. Read on….
www.keaura.com
- https://imjhk03.github.io/posts/swiftui-tappable-area-using-contentshape/
SwiftUI에서 contentShape()을 이용해서 뷰를 탭하게 하는 방법
일반 Text나 Image을 사용하면 탭 제스처를 추가해서 탭 했을 때의 동작을 정의할 수 있다. 하지만 VStack이나 HStack 같은 container view에 제스처를 추가하면 생각처럼 잘 안될 때가 있다. 예를 들어, HSta
imjhk03.github.io
'Dev > 구현' 카테고리의 다른 글
[SwiftUI] Infinite Carousel 구현하기 2 (feat.Timer) (0) | 2024.09.11 |
---|---|
[SwiftUI] Infinite Carousel 구현하기 1 (feat. Timer) (0) | 2024.09.05 |
[SwiftUI] pagerView 만들기 (iOS 버전대응) (2) | 2024.08.14 |
[SwiftUI] CustomPopUpView 애니메이션 효과 해결하기 (0) | 2024.06.27 |
[SwiftUI] TabView page indicator 커스텀하기 (1) | 2024.04.25 |