์์ฆ ํ์ฌ์์ ์ค์ ๋ก ๋ฆฌํฉํ ๋งํ๋ ์์ ์ ๋ด๋นํ๊ณ ์๋๋ฐ, ์น์ ํด๋ฉ์ ๊ตฌํํด์ผํ ์ผ์ด ์๊ฒผ์๋ค.
์ฐพ์๋ณด๋ฉด 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/
'๐ Dev > ๊ตฌํ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[SwiftUI] pagerView ๋ง๋ค๊ธฐ (iOS ๋ฒ์ ๋์) (2) | 2024.08.14 |
---|---|
[SwiftUI] CustomPopUpView ์ ๋๋ฉ์ด์ ํจ๊ณผ ํด๊ฒฐํ๊ธฐ (0) | 2024.06.27 |
[SwiftUI] pre/next buttons๊ฐ ์๋ ์ด๋ฏธ์ง ์ฌ๋ผ์ด๋ ๊ตฌํํ๊ธฐ (2) | 2024.05.16 |
[SwiftUI] TabView page indicator ์ปค์คํ ํ๊ธฐ (1) | 2024.04.25 |
[UIKit] Enum with Reusable VC (0) | 2023.10.10 |