티스토리 뷰

SwiftUI

SwiftUI - Reusable List View 구현하기

DevDiana 2024. 10. 14. 21:03

안녕하세요. Diana 입니다.

오늘은 SwiftUI에서 Scrollable 하도록 데이터를 구성하는 방법인 List에 대해 알아보려고 합니다.

물논 SwiftUI에서 많은 데이터를 Scrollable하게 구현하는 방법에는 List, Grid, LazyVStack 등의 여러 방법이 있지만 오늘 저는 단순히 수직으로 스크롤 가능한 메뉴 리스트를 구성할 생각이므로 List를 선택하였습니다.

 

시작하겠습니다 ㅎㅎ

 


 

✅ List란?

공식문서에 따르면 List는 단일 열로 정돈된 데이터를 나타내는 방법이며 하나 또는 다중 선택을 선택적으로 지원한다고 합니다.

List를 사용하게 되면 데이터 사이에 Separator를 추가하거나 데이터를 밀어서 삭제하는 등의 스타일링을 추가할 수 도 있는데 이 기능이 필요하지 않은 경우에는 LazyVStack을 대신 사용할 수 있습니다.

 

가장 기본적인 List의 구현은 아래와 같습니다.

var body: some View {
    List {
        Text("Hello")
        Text("World")
        Text("!!")
    }
}

 

 

하지만 실무에서 이렇게 데이터를 하나하나 넣어주다간 정말 무한 야근의 굴레에 빠질 수 있죠.

따라서 조금 더 나은 방법으로는 Identifiable를 만족시키는 데이터 타입을 생성한 뒤 이를 만족시키는 데이터 Array를 생성해주기도 합니다.

 

struct Ocean: Identifiable {
    let name: String
    let id = UUID()
}

private var oceans = [
    Ocean(name: "Pacific"),
    Ocean(name: "Atlantic"),
    Ocean(name: "Indian"),
    Ocean(name: "Southern"),
    Ocean(name: "Arctic")
]

var body: some View {
    List(oceans) {
        Text($0.name)
    }
}

 

 

이 외에도 List에는 섹션 추가, 또는 선택 지원 등의 여러 기능을 구현할 수 있는데 이는 공식문서에 자세히 나와있기 때문에 설명은 패스하고 다음 내용으로 넘어가도록 하겠습니다.

 

✅ Reusable List 구현하기

실은 실무에서 List를 사용하며 코드가 쉽게 복잡해지는 상황을 많이 마주했습니다.

아무래도 데이터를 나타내주는 기능을 담고 있다보니 조건마다 보여지는 데이터가 달라지기만 해도 수 많은 if else 문에 갇히게 되었는데요.

 

마침 같이 일하고 계신 선배 개발자분께서 최대한 코드가 길어지지 않도록 분리한다 라는 좋은 습관을 가지고 있길래 그 분의 스타일을 본받아 Reusable한 List를 구현해보고자 합니다.

 

우선 이번에 구현하고자 하는 List는 아래와 같습니다.

 

 

위의 List를 보면 세 개의 섹션 안에 각각의 아이템들이 들어가 있는 것을 확인할 수 있습니다.

굉장히 단순해보이는 구조지만 잘못하면 금방 코드가 복잡하고 어지러워지더라구요.

따라서 재사용성을 고려해서 신중하게 아이템들부터 구현해보겠습니다.

 

struct Food: Identifiable {
    var id: UUID = UUID()
    var name: String
}

let koreanMockItem: [Food] = [
    .init(name: "Kimchi"),
    .init(name: "Kimbap"),
    .init(name: "Bulgogi")
]

let japaneseMockItem: [Food] = [
    .init(name: "Susi"),
    .init(name: "Gyudon"),
    .init(name: "Ramen")
]

let chineseMockItem: [Food] = [
    .init(name: "Dumpling"),
    .init(name: "Malatang")
]

 

 

우리는 한국음식, 일본음식, 중국음식을 각각의 섹션에 나누어 표현해 줄 것입니다.

각각의 Item들은 Food Type을 만족하고 있으며 Food는 Identifiable을 만족하고 있어 Foreach를 통한 반복문에 사용이 가능합니다.

 

 

이제 본격적으로 List를 구현하기에 앞서 어떻게 구현할지 대충 생각해보겠습니다.

 

 

우선 우리는 각각의 List Item을 눌렀을 때 다음 화면으로 넘어갈 수 있도록 NavigationStack을 통해 List를 감싸줄 것입니다.

그리고 내부에 각각의 섹션을 구현할 건데 이 섹션들은 구성이 Header, 그리고 아이템을 가지고 있는 구조로써 반복되므로 분리할 필요가 있어보입니다.

 

 

그럼 구현해보겠습니다.

 

 

struct CustomSection<Content: View>: View {
    let title: String
    @ViewBuilder var content: Content
    
    var body: some View {
        Section(header: Text(title).leftAlignment()) {
            content
        }
    }
}

 

 

우선 우리는 Section을 분리하여 사용할 것이기 때문에 CustomSection이라는 뷰를 생성해주었습니다.

CustomSection은 @ViewBuilder를 만족하는 변수 content를 가지고 있으며 우리는 이 content를 사용하여 원하는 데이터들을 Section 내부에 위치시킬 수 있습니다.

 

 

 

var body: some View {
        NavigationStack {
            List {
                CustomSection(title: "Korean") {
                    ForEach(koreanMockItem) { item in
                        HStack {
                            NavigationLink(item.name, destination: Text(item.name))
                                .font(.system(size: 15))
                                .bold()
                            Spacer()
                        }
                    }
                }
                
                CustomSection(title: "Japanese") {
                    ForEach(japaneseMockItem) { item in
                        HStack {
                            NavigationLink(item.name, destination: Text(item.name))
                                .font(.system(size: 15))
                                .bold()
                            Spacer()
                        }
                    }
                }
                
                CustomSection(title: "Chinese") {
                    ForEach(chineseMockItem) { item in
                        HStack {
                            NavigationLink(item.name, destination: Text(item.name))
                                .font(.system(size: 15))
                                .bold()
                            Spacer()
                        }
                    }
                }
            }
            .frame(maxWidth: .infinity)
            .listStyle(.plain)
            .scrollDisabled(true)
        }
    }

 

 

그리고 위에 구현한 CustomSection을 사용하여 원하는 데이터를 넣은 Section을 구현해주면 위와 같이 구현됩니다.

하지만 위의 코드를 보니 List Cell을 구현하는 부분이 동일하게 반복되는걸 확인할 수 있는데요.

따라서 이 부분도 별도로 분리해주도록 하겠습니다.

 

@ViewBuilder
    func itemCell(item: Food) -> some View {
        HStack {
            NavigationLink(item.name, destination: Text(item.name))
                .font(.system(size: 15))
                .bold()
            Spacer()
        }
    }

 

 

이렇게 분리하면 CustomSection 하위의 ForEach 구문의 코드가 굉장히 단순해집니다.

그리고 이후 조건에 따라 Section을 숨겨야 하거나 할때는 해당 Section에 조건을 걸어 처리할 수 있죠.

 

 

전체 코드는 아래와 같습니다.

import SwiftUI

struct Food: Identifiable {
    var id: UUID = UUID()
    var name: String
}

let koreanMockItem: [Food] = [
    .init(name: "Kimchi"),
    .init(name: "Kimbap"),
    .init(name: "Bulgogi")
]

let japaneseMockItem: [Food] = [
    .init(name: "Susi"),
    .init(name: "Gyudon"),
    .init(name: "Ramen")
]

let chineseMockItem: [Food] = [
    .init(name: "Dumpling"),
    .init(name: "Malatang")
]

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                CustomSection(title: "Korean") {
                    ForEach(koreanMockItem) { item in
                        itemCell(item: item)
                    }
                }
                
                CustomSection(title: "Japanese") {
                    ForEach(japaneseMockItem) { item in
                        itemCell(item: item)
                    }
                }
                CustomSection(title: "Chinese") {
                    ForEach(chineseMockItem) { item in
                        itemCell(item: item)
                    }
                }
            }
            .frame(maxWidth: .infinity)
            .listStyle(.plain)
            .scrollDisabled(true)
        }
    }
    
    @ViewBuilder
    func itemCell(item: Food) -> some View {
        HStack {
            NavigationLink(item.name, destination: Text(item.name))
                .font(.system(size: 15))
                .bold()
            Spacer()
        }
    }
}

struct CustomSection<Content: View>: View {
    let title: String
    @ViewBuilder var content: Content
    
    var body: some View {
        Section(header: Text(title).leftAlignment()) {
            content
        }
    }
}

extension Text {
    func leftAlignment() -> some View {
        return self.font(.system(size: 10)).frame(alignment: .leading)
    }
}

 

이렇게 오늘은 Reusable한 List를 구현해보았습니다.

감사합니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함