ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SwiftUI - Table
    SwiftUI 2024. 5. 13. 18:57

    안녕하세요. 그린입니다 🍏

    이번 포스팅에서는 SwiftUI의 Table 컴포넌트에 대해 학습해보겠습니다 🙋🏻

     

    우선 SwiftUI에서 테이블 형식으로 뷰를 그리는것에는 다양한 방법이 있죠.

    List나 Stack을 이용해 더욱 더 커스텀하게 개발을 할 수도 있어요.

     

    오늘 같이 알아볼 이 Table도 당연하게 모두 컴포넌트를 목록 형태로 나타낼때 사용하는 컴포넌트입니다.

    여태까지는 주로 List를 통해 목록을 구현해왔다면, Table이라는것도 있다는걸 소개해보는 시간이에요ㅎㅎ

     

    그럼 한번 Table 알아볼까요?

     


    Table

    우선 List는 SwiftUI가 나온 시점인 iOS 13 이상부터 사용이 가능하지만, Table은 iOS 16에서 나온 비교적 얼마 되지 않은 컴포넌트입니다.

    즉, List로 일일히 컴포넌트들을 잡아가며 그려야했던것이 조금 더 편리한 템플릿을 제공해주고 기능도 탑재되어 나왔다는걸 추측해볼 수 있어요 🙋🏻

     

    여기서 잠깐!

    중요한건, List는 UITableView와 유사한 느낌으로 iOS나 macOS 모두 하나의 구현으로 동일하게 나타나지만,

    Table은 주로 macOS에서 목록을 표시하기 위한 제공으로 iOS 디바이스에서는 그 디바이스에 맞춰 최적화되어 의도와 다르게 나타날 수 있습니다.

    iOS 16부터는 iPad에 대한 지원도 가능하여 실제로 macOS, iPadOS에서 적극적으로 사용할것 같네요.

    이게 가장 큰 차이일것 같아요!

     

    Table은 하나 이상의 열에 정렬된 데이터 행을 표시하는 컨테이너로, 선택적으로 하나 이상의 구성들을 선택할 수 있는 기능을 제공합니다.

     

    struct Table<Value, Rows, Columns> where Value == Rows.TableRowValue, Rows : TableRowContent, Columns : TableColumnContent, Rows.TableRowValue == Columns.TableRowValue

     

    정의는 이러합니다.

    Value 즉, 데이터 값과 Rows, Columns 인자들을 사용해 만들 수 있는것이죠.

     

    간단히 공식문서를 따라 만들어볼까요?

     

    import SwiftUI
    
    // Data
    struct Person: Identifiable {
      let givenName: String
      let familyName: String
      let emailAddress: String
      let id = UUID()
      
      
      var fullName: String { givenName + " " + familyName }
    }
    
    struct ContentView: View {
      @State private var people = [
        Person(givenName: "Juan", familyName: "Chavez", emailAddress: "juanchavez@icloud.com"),
        Person(givenName: "Mei", familyName: "Chen", emailAddress: "meichen@icloud.com"),
        Person(givenName: "Tom", familyName: "Clark", emailAddress: "tomclark@icloud.com"),
        Person(givenName: "Gita", familyName: "Kumar", emailAddress: "gitakumar@icloud.com")
      ]
      
      var body: some View {
        Table(people) {
          TableColumn("Given Name", value: \.givenName)
          TableColumn("Family Name", value: \.familyName)
          TableColumn("E-Mail Address", value: \.emailAddress)
        }
      }
    }

     

    이렇게 데이터를 구성하고 Table 컴포넌트 호출 시 데이터만 필수로 넣어준 후 TableColums으로 3개의 열을 만들어줍니다.

     

    한번 돌려볼까요?

     

     

    어라? 이상하지 않나요?

    분명 Given / Family / E-Mail 이 세개의 열로 표현되게 만들었지만 아이폰에서는 Given 정보만 보입니다.

     

    이는 위에서도 말했듯, Table이 macOS, iPadOS에 최적화되었기에 거기선 정상적으로 의도대로 나타나지만, 나머지 경우에는 플랫폼 특성에 따라 바뀔 수 있다는 점입니다.

     

    그럼 iPad로 돌려볼까요?

     

     

    이제 의도대로 정상적으로 나오네요.

    그렇기에, Table을 테스트하거나 학습해보신다면 iPadOS나 macOS에서 실습해보시길 권장드립니다.

     

    또한 Table에서는 선택에 대한 기능을 제공해줘요.

    즉, 테이블에서 어떠한 행을 선택하면 그 행을 감지할 수 있도록 구현할 수 있습니다.

    특히 다중 행의 선택을 지원합니다!

     

    struct ContentView: View {
      @State private var people = [
        Person(givenName: "Juan", familyName: "Chavez", emailAddress: "juanchavez@icloud.com"),
        Person(givenName: "Mei", familyName: "Chen", emailAddress: "meichen@icloud.com"),
        Person(givenName: "Tom", familyName: "Clark", emailAddress: "tomclark@icloud.com"),
        Person(givenName: "Gita", familyName: "Kumar", emailAddress: "gitakumar@icloud.com")
      ]
      // 🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻
      @State private var selectedPeople = Set<Person.ID>()
      
      var body: some View {
        // 🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻
        Table(people, selection: $selectedPeople) {
          TableColumn("Given Name", value: \.givenName)
          TableColumn("Family Name", value: \.familyName)
          TableColumn("E-Mail Address", value: \.emailAddress)
        }
        Text("\(selectedPeople.count) people selected")
      }
    }

     

    이렇게 selectedPeople State 변수를 통해 Table 생성 시 selection 인자에 넣어주기만 하면 됩니다.

     

    그럼 한번 돌려볼까요?

     

    요렇게 멀티 셀렉트가 가능하고 인지할 수 있죠.

     

    또한, 테이블 정렬 기능도 사용할 수 있습니다.

     

    struct ContentView: View {
      @State private var people = [
        Person(givenName: "Juan", familyName: "Chavez", emailAddress: "juanchavez@icloud.com"),
        Person(givenName: "Mei", familyName: "Chen", emailAddress: "meichen@icloud.com"),
        Person(givenName: "Tom", familyName: "Clark", emailAddress: "tomclark@icloud.com"),
        Person(givenName: "Gita", familyName: "Kumar", emailAddress: "gitakumar@icloud.com")
      ]
      @State private var sortOrder = [KeyPathComparator(\Person.givenName)]
      
      var body: some View {
        Table(people, sortOrder: $sortOrder) {
          TableColumn("Given Name", value: \.givenName)
          TableColumn("Family Name", value: \.familyName)
          TableColumn("E-Mail address", value: \.emailAddress)
        }
        .onChange(of: sortOrder) { _, sortOrder in
          people.sort(using: sortOrder)
        }
      }
    }

     

    이렇게 정렬될 기준인 givenName을 이용해 KeyPathComparator 변수를 사용하면 됩니다.

    그럼 이제 GivenName에 엑셀 필터 기능처럼 버튼이 생기고 클릭 시 자동으로 내림/오름차순 순으로 정렬할 수 가 있죠!

     

    이것도 돌려볼까요~?

     

    아주 간단하죠!?

     

    이뿐만이 아니라, 데이터 컬렉션의 컨텐츠가 아닌 정적인 행에서 테이블을 생성할 수도 있습니다.

    이 경우엔 열과 행을 모두 제공해줘야해요.

     

    struct Purchase: Identifiable {
        let price: Decimal
        let id = UUID()
    }
    
    struct ContentView: View {
      let currencyStyle = Decimal.FormatStyle.Currency(code: "USD")
    
      var body: some View {
          Table(of: Purchase.self) {
              TableColumn("Base price") { purchase in
                  Text(purchase.price, format: currencyStyle)
              }
              TableColumn("With 15% tip") { purchase in
                  Text(purchase.price * 1.15, format: currencyStyle)
              }
              TableColumn("With 20% tip") { purchase in
                  Text(purchase.price * 1.2, format: currencyStyle)
              }
              TableColumn("With 25% tip") { purchase in
                  Text(purchase.price * 1.25, format: currencyStyle)
              }
          } rows: {
              TableRow(Purchase(price: 20))
              TableRow(Purchase(price: 50))
              TableRow(Purchase(price: 75))
          }
      }
    }

     

    만들어진 데이터가 아닌 정적으로 데이터를 이렇게 넣어줄때는 TableColum와 TableRow를 고정으로 생성하여 이용하면 됩니다.

     

    그럼 이렇게 테이블이 만들어집니다.

     

     

    이 외 별도의 뷰 모디파이어를 이용해 테이블 디자인을 변경해줄 수 있습니다.

    tableStyle을 뷰 모디파이어를 이용하면 되는데, 한번 필요하실때 확인해보셔도 좋습니다!

     

    tableStyle(_:) | Apple Developer Documentation

    Sets the style for tables within this view.

    developer.apple.com

     

    그리고 아까 초반에 iOS 즉, 아이폰 환경에서는 플랫폼의 특성에 따라 의도치않게 나타날 수 있다고 한것 기억하시나요?

     

    일반적으로 소형 디바이스 사이즈에선 테이블의 열을 표시할 공간이 제한되어 있습니다.

    공간을 절약하기 위해서 테이블이 자동으로 감지하여 첫번째 헤더 이후의 다른 헤더와 모든 열을 숨깁니다.

    그래서 그랬던건데요..!

     

    우리는 사실 다 노출시키고 싶잖아요? 🥲

     

    그럴때는 다음과 같이 전처리를 통해 직접 노출시켜줄 수 있어요.

    즉, 다양한 플랫폼에서 Table을 다채롭게 활용할 수 있다는것이죠.

     

    struct ContentView: View {
      @State private var people = [
        Person(givenName: "Juan", familyName: "Chavez", emailAddress: "juanchavez@icloud.com"),
        Person(givenName: "Mei", familyName: "Chen", emailAddress: "meichen@icloud.com"),
        Person(givenName: "Tom", familyName: "Clark", emailAddress: "tomclark@icloud.com"),
        Person(givenName: "Gita", familyName: "Kumar", emailAddress: "gitakumar@icloud.com")
      ]
      @State private var sortOrder = [KeyPathComparator(\Person.givenName)]
      
      #if os(iOS)
      @Environment(\.horizontalSizeClass) private var horizontalSizeClass
      private var isCompact: Bool { horizontalSizeClass == .compact }
      #else
      private let isCompact = false
      #endif
      
      var body: some View {
        Table(people, sortOrder: $sortOrder) {
          TableColumn("Given Name", value: \.givenName) { person in
            VStack(alignment: .leading) {
              Text(isCompact ? person.fullName : person.givenName)
              if isCompact {
                Text(person.emailAddress)
                  .foregroundStyle(.secondary)
              }
            }
          }
          TableColumn("Family Name", value: \.familyName)
          TableColumn("E-Mail Address", value: \.emailAddress)
        }
        .onChange(of: sortOrder) { _, sortOrder in
          people.sort(using: sortOrder)
        }
      }
    }

     

    이렇게, iOS에서 현재 디바이스의 수평 사이즈클래스가 컴팩트하다면 다른 형태로 정보들을 모두 보여줄 수 있는것이죠.

     

    실제로 이렇게 노출됩니다.

     

     

    역시 애플... 지원안하고 있는줄 알고 초반에 놀랐네요 😄

     

    iOS 17 이상 부터는 Disclosure 기능도 제공합니다.

    즉, 하위로 보여주고 숨기고 할 수 있어요.

     

    코드로 볼까요?

     

    import SwiftUI
    
    // Data
    struct Person: Identifiable, Equatable {
      let id = UUID()
      let name: String
      var city: String
      let hobby: String
      var friends = [Person]()
    }
    
    struct ContentView: View {
      @State var people: [Person] = [
        .init(
          name: "green",
          city: "seoul",
          hobby: "movie",
          friends: [
            .init(name: "brown", city: "osaka", hobby: "football"),
            .init(name: "black", city: "tokyo", hobby: "soccer"),
            .init(name: "white", city: "busan", hobby: "movie"),
            .init(name: "purple", city: "daegu", hobby: "football")
          ]
        ),
        .init(name: "red", city: "tokyo", hobby: "soccer"),
        .init(name: "blue", city: "busan", hobby: "tennis"),
        .init(name: "yellow", city: "ulsan", hobby: "cook")
      ]
      
      @State private var bookmarksExpanded = false
      
      var body: some View {
        Table(of: Person.self) {
          TableColumn("Name", value: \.name)
          TableColumn("City", value: \.city)
          TableColumn("Hobby", value: \.hobby)
        } rows: {
          ForEach(people) { person in
            if person.friends.isEmpty {
              TableRow(person)
            } else {
              DisclosureTableRow(person) {
                ForEach(person.friends)
              }
            }
          }
        }
      }
    }

     

    이렇게, 해당하는 조건에 따라 하위 테이블을 표시해줄 수 있습니다.

     

     

    17 이상부터 다양한 기능들이 좀 더 생겨나서 보다 손쉽게 이런 테이블 구조의 화면을 개발할 수 있을것 같아요!

     

    마지막으로, 해당 테이블 행을 삭제하거나 편집 등 다양한 기능들을 contextMenu를 통해 쉽게 구현해낼수도 있습니다.

     

    추가된 코드를 볼까요~?

     

    import SwiftUI
    
    // Data
    struct Person: Identifiable, Equatable {
      let id = UUID()
      let name: String
      var city: String
      let hobby: String
      var friends = [Person]()
    }
    
    struct ContentView: View {
      @State var people: [Person] = [
        .init(
          name: "green",
          city: "seoul",
          hobby: "movie",
          friends: [
            .init(name: "brown", city: "osaka", hobby: "football"),
            .init(name: "black", city: "tokyo", hobby: "soccer"),
            .init(name: "white", city: "busan", hobby: "movie"),
            .init(name: "purple", city: "daegu", hobby: "football")
          ]
        ),
        .init(name: "red", city: "tokyo", hobby: "soccer"),
        .init(name: "blue", city: "busan", hobby: "tennis"),
        .init(name: "yellow", city: "ulsan", hobby: "cook")
      ]
      
      @State private var bookmarksExpanded = false
      
      var body: some View {
        Table(of: Person.self) {
          TableColumn("Name", value: \.name)
          TableColumn("City", value: \.city)
          TableColumn("Hobby", value: \.hobby)
        } rows: {
          ForEach(people) { person in
            if person.friends.isEmpty {
              TableRow(person)
                // 🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻
                .contextMenu {
                  Button("Edit") {
                  }
                  Button("See Details") {
                  }
                  Divider()
                  Button("Delete", role: .destructive) {
                    delete(person)
                  }
                }
            } else {
              DisclosureTableRow(person) {
                ForEach(person.friends)
              }
            }
          }
        }
      }
      
      // 🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻🙋🏻
      func delete(_ person: Person) {
        if let index = people.firstIndex(of: person) {
          people.remove(at: index)
        }
      }
    }

     

    열심히 손들고 있는 주석 부분만 보면됩니다.

    해당 Row에 contextMenu를 사용하여 원하는 수정 / 더보기 / 삭제 등을 구현해주면됩니다.

     

    마지막으로 동작을 볼까요?

     

     

    자 이렇게 Table을 학습해보며 다양하게 기능들을 추가해봤어요!

    잘만 활용된다면 아주 편리하게 사용될 수 있을것 같긴합니다.

    다만, macOS, iPadOS에서 적극적으로 사용될것 같고 iOS에서는 흠.. 잘 모르겠네요!

    아직 저는 커스텀하게 다 만드는게 편할 수도 있어보입니다 😄

     


    레퍼런스

     

    Table | Apple Developer Documentation

    A container that presents rows of data arranged in one or more columns, optionally providing the ability to select one or more members.

    developer.apple.com

    https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-multi-column-lists-using-table

    https://www.swiftyplace.com/blog/chy7hvne

    'SwiftUI' 카테고리의 다른 글

    PhotosPicker 사용하기  (59) 2024.06.13
    SwiftUI에서 Search Interface 추가하기  (68) 2024.05.17
    SwiftUI - transformEnvironment  (60) 2024.05.09
    SwiftUI에서 스크롤 뷰 내 컨텐츠 속도 제어하기  (87) 2024.05.02
    SwiftUI onDrag & onDrop  (97) 2024.04.29
Designed by Tistory.