[SwiftUI] 스유로 섹션 접었다폈다 구현하기

2024. 6. 21. 08:52·Dev/구현

 

 

요즘 회사에서 스유로 리팩토링하는 작업을 담당하고 있는데, 섹션 폴딩을 구현해야할 일이 생겼었다.

 

찾아보면 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
'Dev/구현' 카테고리의 다른 글
  • [SwiftUI] Infinite Carousel 구현하기 1 (feat. Timer)
  • [SwiftUI] pagerView 만들기 (iOS 버전대응)
  • [SwiftUI] CustomPopUpView 애니메이션 효과 해결하기
  • [SwiftUI] TabView page indicator 커스텀하기
Callie_
Callie_
  • Callie_
    CalliOS
    Callie_
  • 전체
    오늘
    어제
    • 분류 전체보기
      • APPLE
      • Dev
        • Swift
        • UIKit
        • SwiftUI
        • Issue
        • 구현
      • Design
        • HIG
      • CS
      • 직관로그 (출시앱)
        • 업데이트
      • 🌱 SeSAC iOS 3기
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    addTarget
    Entry Point
    Swift
    .OverFullScreen
    clipsToBound
    layer.shadow
    DidEndOnExit
    cs
    modalPresentStyle
    IBAction
    TapGestureRecognizer
    화면전환
    네트워크통신
    Enum
    keyboard
    후기
    stroyboard
    생명주기
    tag
    SwiftUI
    cornerradius
    TableViewCell
    .fullScreen
    Button
    ios
    SeSAC
    Info탭
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
Callie_
[SwiftUI] 스유로 섹션 접었다폈다 구현하기
상단으로

티스토리툴바