요즘 회사에서 스유로 리팩토링하는 작업을 담당하고 있는데, 섹션 폴딩을 구현해야할 일이 생겼었다.
찾아보면 list를 활용해서 만드는 예시 위주만 나와서 내가 구현하고 하는 것과 맞지 않았다. 내가 구현하고자 한 폴딩 부분은 디자인 가이드상 컬렉션뷰로 구현해야 나오는 뷰였기 때문에 GirdItem을 활용해서 커스텀으로 섹션을 만들어 쓰기로 했다. 마침 참고하기 좋은 해외 포스트도 발견해서 참조했다.
아직 스유에서 사용하는 propertyWrapper가 어색하다면 아래 포스팅을 참고하는 걸 추천! --- propertyWrapper 글 작업 중
1. 섹션 상태를 섹션 이름으로 구별해서 folding 기능을 토글처럼 값을 관찰하는 ObservableObject를 만든다.
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
}
}
- ObservableObject 클래스를 사용해서 값의 변화를 관찰하도록 했다.
- @Published로 sections이란 딕셔너리를 만들어서 섹션 이름과 bool값으로 섹션이 열렸는지 닫혔는지를 관찰한다. @Published의 특성 상 값의 변화가 감지되면 곧바로 뷰를 업데이트 해준다.
- 회사 디자인 가이드상 섹션이 열려 있어야해서 init에서 딕셔너리 값을 true로 반환해서 열어뒀다. (그런데 코드 상 true가 default라 열려있긴 했다.)
2. 커스텀으로 섹션헤더뷰 만들기
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)
}
}
}
- 이제 sectionModel을 활용해서 커스텀한 섹션 뷰를 만들어준다.
- TigersSectionModel을 @StateObject로 관찰중인 객체에 변화를 감지하면 뷰를 업데이트하게 해준다. 참고했던 블로그에서는 @ObservedObject로 설정해두었는데, 그부분을 @StateObject로 변경했다.
------ 두 개의 차이점이 정말,, 교묘해서 조금 더 공부해야겠지만, 내가 공부했을 때 생각한 차이점은 뷰 라이프사이클에 의존적이냐, 아니냐였다. 뷰가 갱신 될 때 값이 파괴되거나 재생성 되는 @ObservedObject는 내게 안정성 면에서 떨어지는 것 같아서, @StateObject로 수정했다. 애플도 후자를 사용하기를 권장하고 있어서 학습을 더해야겠지만, 약간 weak self같은 역할을 해주는 것 같았다.
- 섹션 부분을 탭하면 폴딩 기능이 되면서 화살표 방향도 바뀌어야해서, contentShape으로 섹션 부분을 잡아주고 그 부분을 터치영역이 될 수 있게 해주었다.
- 터치하면 모델의 toggle값이 바뀌어서 폴딩의 open/close 값을 업데이트하고 뷰갱신 할 수 있게 했다. (이 부분을 @StateObject가 해준다.) 그리고 섹션을 이름으로 나누어서 이름과 동일한 섹션을 탭했을 때 값을 갱신 시켰다.
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
- https://imjhk03.github.io/posts/swiftui-tappable-area-using-contentshape/
'iOS > 구현' 카테고리의 다른 글
[SwiftUI] pre/next buttons가 있는 이미지 슬라이더 구현하기 (2) | 2024.05.16 |
---|---|
[SwiftUI] TabView page indicator 커스텀하기 (1) | 2024.04.25 |