본문 바로가기

학습 노트/Swift UI (2022)

26. List #1

List #1


List는 여러 데이터를 각각의 열로써 수직으로 배치하는 View다.
애플의 모든 플랫폼을 지원하며, 각각에 알맞게 표시한다.
SwiftUI의 List는 UIKit의 TableView와 같은 기능을 한다.

StaticList

struct StaticList: View {
    var body: some View {
        List {
            HStack {
                Text("Hello, World!")
                Text("Hello, World!")
            }
            Text("Hello, World!")

            Image(systemName: "star")

            Toggle(isOn: .constant(true)) {
                Text("On")
            }
        }
    }
}

List에 Embed 된 View를 각각의 개별 Cell로  구성한다.

DynamicList

struct DynamicList: View {
    var items = Product.sampleList
    var body: some View {
        VStack {
            List(items, id: \.name) { item in
                Text(item.name)
            }
        }
    }
}
struct Product {
    let name: String
    let summary: String
    let category: String
    let price: Int
}

extension Product {
    static var sampleList: [Product] {
        return [
            Product(name: "MacBook Air",
                    summary: """
                        Apple M1 칩(8코어 CPU, 8코어 GPU, 16코어 Neural Engine)
                        8GB 통합 메모리
                        512GB SSD 저장 장치¹
                        True Tone이 탑재된 Retina 디스플레이
                        Magic Keyboard
                        Touch ID
                        Force Touch 트랙패드
                        Thunderbolt/USB 4 포트 2개
                        """,
                    category: "Mac",
                    price: 1_630_000),
            .
            .
            .

            Product(name: "Apple Watch Series 7",
                    summary: """
                        45mm
                        상시표시형 Retina 디스플레이
                        알루미늄 / 스텐인리스 스틸 / 티타늄
                        GPS + Cellular
                        """,
                    category: "Apple Watch",
                    price: 659_000)
        ]
    }
}

DynamicList을 구현할 때는 전달되는 데이터가 'identifiable' 프로토콜을 채용한 상태여야 한다.
그렇지 아니한 경우 List의 id 파라미터에 이를 대신할 구분자를 전달해 줘야 한다.
위의 코드에서는 sampleList의 name이 그 역할을 한다.

IdentifiableList

struct DynamicIdentifiableList: View {
    var items = AppleProduct.sampleList

    var body: some View {
        VStack {
            List(items) { item in
                Text(item.name)
            }
        }
    }
}
struct AppleProduct: Identifiable, Hashable {
    let id = UUID()
    let name: String
    let summary: String
    let category: String
    let price: Int
}

extension AppleProduct {
    static var sampleList: [AppleProduct] {
        return [
            AppleProduct(name: "MacBook Air",
                         summary: """
                        Apple M1 칩(8코어 CPU, 8코어 GPU, 16코어 Neural Engine)
                        8GB 통합 메모리
                        512GB SSD 저장 장치¹
                        True Tone이 탑재된 Retina 디스플레이
                        Magic Keyboard
                        Touch ID
                        Force Touch 트랙패드
                        Thunderbolt/USB 4 포트 2개
                        """,
                         category: "Mac",
                         price: 1_630_000),
             .
             .
             .
             
            AppleProduct(name: "Apple Watch Series 7",
                         summary: """
                        45mm
                        상시표시형 Retina 디스플레이
                        알루미늄 / 스텐인리스 스틸 / 티타늄
                        GPS + Cellular
                        """,
                         category: "Apple Watch",
                         price: 659_000)
        ]

    }
}

데이터 구조체에 id 속성을 추가하고, UUID를 저장한다.
구조체는 identifiable, hashable 프로토콜을 채용해야 한다.

이 경우 identifiable 프로토콜을 인식하고, id 속성이 그 역할을 자동으로 하기 때문에,
List의 파라미터에 id를 따로 지정하지 않아도 된다.

Section

Content 외에 Header와 Footer를 추가할 수 있다.

StaticList

struct SectionedList: View {
    var items = CategorizedProduct.sampleList

    var body: some View {
        VStack {
            List {
                Section {
                    Text("1")
                    Text("2")
                }

                Section {
                    Text("3")
                    Text("4")
                    Text("5")
                } header: {
                    Text("Header")
                } footer: {
                    Text("Footer")
                }
            }            
        }
    }
}

List 내에서 Section에 Embed 해 사용하고,
각각의 Section은 header와 footer를 할당할 수 있다.
header는 대문자로 표시된다는 특징이 있다.

DynamicList

struct SectionedList: View {
    var items = CategorizedProduct.sampleList

    var body: some View {
        VStack {
            List {
                ForEach(items) { section in
                    Section {
                        ForEach(section.list) { item in
                            Text(item.name)
                        }
                    } header: {
                        Text(section.header)
                    } footer: {
                        if let footer = section.footer {
                            Text(footer)
                        }
                    }
                }
            }            
        }
    }
}

DynamicList는 ForEach를 사용한다.
Foreach는 전달된 데이터를 열거하는 View로 DynamicList와 GridView에 자주 사용된다.
전달된 itmes의 section 대로 분리하고,
section에 포함된 list의 name 속성으로 데이터를 표시한다.

struct CategorizedProduct: Identifiable, Hashable {
    let id = UUID()
    let header: String
    let footer: String?
    let list: [AppleProduct]
}

extension CategorizedProduct {
    static var sampleList: [CategorizedProduct] {
        return [
            CategorizedProduct(header: "iPhone",
                               footer: "Lorem Ipsum",
                               list: AppleProduct.sampleList.filter { $0.category == "iPhone" }),
            CategorizedProduct(header: "iPad",
                               footer: nil,
                               list: AppleProduct.sampleList.filter { $0.category == "iPad" }),
            CategorizedProduct(header: "Mac",
                               footer: nil,
                               list: AppleProduct.sampleList.filter { $0.category == "Mac" }),
            CategorizedProduct(header: "Apple Watch",
                               footer: nil,
                               list: AppleProduct.sampleList.filter { $0.category == "Apple Watch" })
        ]
    }
}

Section 구성을 위해 ForEach에 전달된 데이터의 구조는 위와 같다.
동적 할당을 위한 id속성과 Sectio의 구분자 역할을 할 header와 footer가 존재하며,
해당 Section에 표시될 데이터가 배열로 저장된다.

결과는 위와 같다.

 

Customizing


List는 배경색, 테두리 색, 여백뿐만 아니라,
기본 디자인 까지도 변경할 수 있다.

ListStyle

  • .insetGroup
    List의 기본 스타일이다.

  • .grouped
    가장 큰 특징을 각 모서리의 여백과 라운드 처리가 사라졌다는 것이다.

  • .plain
    가장 기교 없는 평면적인 디자인이다.
    Section의 구분도 header나 footer가 없다면 선의 굵기로만 미세하게 구분된다.

  • .inset
    현행 버전 기준 plain과 동일한 ui를 보여준다.

  • .sidebar
    sidebar 형태의 List 디자인이다.
    mac과 iPad 등에서 왼쪽에 표시되며, section을 접었다 펴는 기능이 기본으로 제공된다.

  • .automatic
    플랫폼에 어울리는 형태로 자동으로 적용된다.

Cell 여백

listRowInsets

struct CustomizingList: View {
    var body: some View {
        VStack {            
            List {
                Section() {
                    Text("Hello, List!")
                        .listRowInsets(.init(top: 0, leading: 100, bottom: 0, trailing: 0))
                        
                    Text("List Row Insets")
                    
                    Text("List Row Background")
                    
                    Text("List Row Separator")
                    
                    Text("List Row Separator Tint")
                } header: {
                    Text("first header")
                }
                .listRowInsets(.init(top: 0, leading: 60, bottom: 0, trailing: 0))

                Section() {
                    Text("One")
                    
                    Text("Two")
                }header: {
                    Text("second header")
                }

                Section() {
                    Text("Custom Header")
                }
            }
            .listStyle(.automatic)
        }
    }
}

List의 cell에 표시되는 모든 View 혹은 Section 전체에 여백을 설정할 수 있다.

첫 번째 Section 전체에 leading 60의 여백이 적용된 상태에서,
첫 번째 Cell에 해당하는 TextView에는 leading 100의 여백이 적용된 것을 볼 수 있다.
이를 통해 Section과 Cell에 중복으로 inset이 설정된 경우 Cell의 inset이 우선권이 높음을 확인할 수 있다.

Cell 배경

listRowBackground

struct CustomizingList: View {
    var body: some View {
        VStack {            
            List {
                Section() {
                    Text("Hello, List!")
                        .listRowInsets(.init(top: 0, leading: 30, bottom: 0, trailing: 0))
                        
                    Text("List Row Insets")
                    
                    Text("List Row Background")
                    
                    Text("List Row Separator")
                    
                    Text("List Row Separator Tint")
                    
                } header: {
                    Text("first header")
                }
                .listRowInsets(.init(top: 0, leading: 60, bottom: 0, trailing: 0))

                Section() {
                    Text("One")
                        .listRowBackground(Color(.cyan))
                        
                    Text("Two")
                }header: {
                    Text("second header")
                }
                .listRowBackground(Color(.gray))

                Section() {
                    Text("Custom Header")
                }
            }
            .listStyle(.automatic)
        }
    }
}

List 혹은 Cell의 배경색을 변경할 수 있다.
listRowInsets와 마찬가지로 Cell과 Section에 동시에 존재할 경우 Cell의 Background가 우선 적용된다.

Cell 테두리

listRowSeperator

struct CustomizingList: View {
    var body: some View {
        VStack {            
            List {
                Section() {
                    Text("Hello, List!")
                        .listRowInsets(.init(top: 0, leading: 30, bottom: 0, trailing: 0))
                    Text("List Row Insets")
                    Text("List Row Background")
                    Text("List Row Separator")
                        .listRowSeparator(.hidden)
                    Text("List Row Separator Tint")
                } header: {
                    Text("first header")
                }
                .listRowInsets(.init(top: 0, leading: 60, bottom: 0, trailing: 0))

                Section() {
                    Text("One")
                        .listRowBackground(Color(.cyan))
                    Text("Two")
                }header: {
                    Text("second header")
                }
                .listRowBackground(Color(.gray))

                Section() {
                    Text("Custom Header")
                }
            }
            .listStyle(.automatic)
        }
    }
}

Cell 간의 구분을 담당하는 separator를 숨기거나 보이게 조절할 수 있다.
또한 edges 파라미터로 상, 하 둘 중 하나를 지정하는 것도 가능하다.
사진에서는 첫 번째 Section의 네 번째 Cell의 separator를 모두 없앤 결과이다.

Cell 테두리 색

listRowSeperatorTint

struct CustomizingList: View {
    var body: some View {
        VStack {            
            List {
                Section() {
                    Text("Hello, List!")
						.listRowInsets(.init(top: 0, leading: 30, bottom: 0, trailing: 0))
                        
                    Text("List Row Insets")
                    
                    Text("List Row Background")
                    
                    Text("List Row Separator")
						.listRowSeparator(.hidden)
                        
                    Text("List Row Separator Tint")
				} header: {
					Text("first header")
				}
				.listRowInsets(.init(top: 0, leading: 60, bottom: 0, trailing: 0))
				.listRowSeparatorTint(Color(.systemBlue))
                
                Section() {
                    Text("One")
						.listRowBackground(Color(.cyan))
                        
                    Text("Two")
				}header: {
					Text("second header")
				}
				.listRowBackground(Color(.gray))
                
                Section() {
                    Text("Custom Header")
                }
            }
			.listStyle(.automatic)
        }
    }
}

Cell 구분 선의 색을 변경할 수 있다.
첫 번째 Section의 Cell 구분 선이 파란색으로 변경된 것을 확인할 수 있다.

 

CustomHeader


SectionHeader는 별도로 구성한 View를 표시하는 것이 가능하다.

struct CustomHeaderView: View {
    let title: String
    let imageName: String
    var body: some View {
        Label(title, systemImage: imageName)
            .font(.title)
            .frame(minHeight: 60)
    }
}

Header를 Label로 구현해 60 포인트의 높이로 표시한다.

struct CustomizingList: View {
    var body: some View {
        VStack {            
            List {
                Section() {
                    Text("Hello, List!")
						.listRowInsets(.init(top: 0, leading: 30, bottom: 0, trailing: 0))
                        
                    Text("List Row Insets")
                    
                    Text("List Row Background")
                    
                    Text("List Row Separator")
						.listRowSeparator(.hidden)
                        
                    Text("List Row Separator Tint")
				} header: {
					Text("first header")
				}
				.listRowInsets(.init(top: 0, leading: 60, bottom: 0, trailing: 0))
				.listRowSeparatorTint(Color(.systemBlue))
                
                Section() {
                    Text("One")
						.listRowBackground(Color(.cyan))
                        
                    Text("Two")
				}header: {
					Text("second header")
				}
				.listRowBackground(Color(.gray))
                
                Section() {
                    Text("Custom Header")
				} header: {
					CustomHeaderView(title: "Star", imageName: "star")
				}
            }
			.listStyle(.automatic)
        }
    }
}

CustomHeaderView를 호출하면서 알맞은 파라미터를 함께 전달하면 해당 View가 Header에 표시된다.

'학습 노트 > Swift UI (2022)' 카테고리의 다른 글

28. ForEach & Grid  (0) 2022.11.09
27. List #2  (0) 2022.11.09
25. StateObject  (0) 2022.11.02
24. Observable Object & Environment Object  (0) 2022.11.01
23. State & Binding  (0) 2022.10.27