궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFTUI/View layout

SwiftUI : List (ListStyle / onDelete / onMove)

서근
QUOTE THE DAY

“ 좋은 디자인은 자연을 닮았다. 자연을 닮은 것이 본질적으로 좋은 이유는 자연이 이미 오랜 세월 동안 문제를 해결하기 위해서 노력해 왔기 때문이다. 그렇기 때문에 어떤 답이 자연을 닮았다면 그것은 항상 좋은 신호다. ”

- Paul Graham (폴 그레이엄)
Written by SeogunSEOGUN

SwiftUI : List (ListStyle / onDelete / onMove)

 

 

List에 대해 알아보도록 합시다.

List

List는 단일 열에 정렬 된 데이터 행을 표시하는 컨테이너입니다. 

swift
UNFOLDED
struct ContentView: View {
var body: some View {
List {
Text("첫번째 리스트")
Text("두번째 리스트")
Text("세번째 리스트")
}
}
}

SwiftUI : List (ListStyle / onDelete / onMove) - List

정적 콘텐츠

List의 생성자에 원하는 뷰를 전달하면 하나씩 각 row에 담아 표현합니다. 여기서 뷰 하나는 row 하나에 해당합니다.

TIP
 
 

UIKit에서는 UITableView에서 내용을 표시할 뷰를 셀이라고 불렀다면, SwiftUI에서는 row라고 표현합니다.

다음과 같이 텍스트가 아닌 다른 이미지 뷰를 넣어 봐도 동일하게 List를 작성 할 수 있습니다.

swift
UNFOLDED
List {
Text("List")
Image("seogun").resizable().scaledToFit().frame(width: 100, height: 100)
Rectangle().frame(width: 100, height: 100)
Circle().frame(width: 100, height: 100).foregroundColor(.yellow)
}

SwiftUI : List (ListStyle / onDelete / onMove) - List - 정적 콘텐츠

동적 콘텐츠

Range <Int>

동적 콘텐츠를 표현하는 첫번째 방법은 Range<Int> 타입의 값을 넘겨주는 것입니다. 예를들어 1부터 100까지의 숫자를 출력하려면 다음과 같이 작성 하면 됩니다.

swift
UNFOLDED
List(o..<100) {
Text("\($0)")
}

RandomAccessCollection

두 번째 방법은 RandomAccessCollecion 프로토콜을 준수하는 데이터를 제공하는 것입니다. 이경우에는 데이터의 각 요소들을 구분하고 식별할 수 있도록 반드시 다음 2가지 방법 중 하나를 선택하여 id값을 제공해야만 합니다.

 

첫 번째, id 식별자 지정

첫 번째는 id로 사용할 값을 직접 인수로 제공하는 것입니다. SwifUI에서는 이것을 간단히 self라고 입력할 수도 있습니다. 

.List(["A", "B", "C", "D"], id: \.self) { ... } 

 

 

두 번째, identifiable 프로토콜 채택

두 번째 방법은 매개 변수에 id를 전달하는 대신 데이터 타입 자체에 identifiable프로토콜을 채택하는 것입니다. 타입 자체에 id 프로퍼티를 만들고 이것을 식별자로 삼게 됩니다,  

 

우선 Ghibli 라는 struct를 만들고 Identifiable을 매개변수 타입으로 넣어준 후,

원하는 상수와 식별가능한 코드 id = UUID()를 작성합니다.

TIP
 
 

UUID?? 유형, 인터페이스 및 기타 항목을 식별하는 데 사용할 수 있는 보편적으로 고유한 값입니다.

swift
UNFOLDED
struct Ghibli: Identifiable {
let name: String
let id = UUID()
}
private var ghibli = [
Ghibli(name: "포뇨"),
Ghibli(name: "소스케"),
Ghibli(name: "소피아"),
Ghibli(name: "센"),
Ghibli(name: "하쿠"),
Ghibli(name: "토토로")
]

이제 ContentViewList와 위에 작성한 배열을 넣어줘야 합니다.

swift
UNFOLDED
struct ContentView: View {
var body: some View {
List(ghibli) {
Text($0.name).font(.system(size: 20))
}
}
}

SwiftUI : List (ListStyle / onDelete / onMove) - List - 동적 콘텐츠 - RandomAccessCollection

이처럼 identifiale 프로토콜을 준수한다면, 이미 식별자가 있으므로 리스트에 id를 제공하지 않아도 무방합니다.

정적 콘텐츠와 동적 콘텐츠 조합

ForEach를 이용하면 이 두 가지를 조합하는 것도 가능합니다.

ForEach

SwiftUI에서 ForEachList처럼 id로 식별할 수 있는 데이터를 받아서 동적으로 뷰를 생성하는 역할을 합니다. 전달받은 매개변수도 RandomAccessCollection이나 Range<Int> 타입을 사용한다는 점도 같습니다. 그래서 아래 코드는 같은 결과를 보여주게 됩니다.

swift
UNFOLDED
List {
ForEach(0..<30) {
Text("\($0)")
}
}
swift
UNFOLDED
List(1..<30) {
Text("\($0)")
}

하지만 리스트에서는 정적인 뷰도 포함할 수 있기 때문에, ForEach와 함께 사용하면 정적 + 동적 콘텐츠를 조합할 수 있는 것입니다.

swift
UNFOLDED
List {
Text("목록") // 하나의 Row를 차지하는 정적 뷰
ForEach(0..<30) { // 30개의 동적 뷰 생성
Text("\($0)")
}
}

조합

이제 동적과 정적 콘텐츠를 조합한 예제를 살펴보도록 하겠습니다.

swift
UNFOLDED
struct ContentView: View {
let drink = ["스프라이트", "콜라", "환타", "오렌지주스"]
let snack = ["프링글스", "엄마손파이", "포카칩"]
var body: some View {
List {
Text("음료수")
ForEach(drink, id: \.self) {
Text("\($0)")
}
Label("과자", systemImage: "star.fill").font(.largeTitle)
ForEach(snack, id: \.self) {
Text("\($0)")
}
}
}
}

SwiftUI : List (ListStyle / onDelete / onMove) - List - 정적 콘텐츠와 동적 콘텐츠 조합 - 조합

Section

섹션에는 headerfooter를 생략하거나 추가할 수 있고, 둘 중 하나만 사용할 수 있습니다.

swift
UNFOLDED
struct ContentView: View {
let drink = ["스프라이트", "콜라", "환타", "오렌지주스"]
let snack = ["프링글스", "엄마손파이", "포카칩"]
var body: some View {
let titles = ["음료수", "과자"] // 분류 제목
let data = [drink, snack] //위에 정의한 목록 데이터
return List {
ForEach(data.indices) { index in // data에 포함된 횟수만큼 섹션 생성
Section(header: Text(titles[index]), footer: HStack { Spacer(); Text("\(data[index].count)건")}
) {
ForEach(data[index], id: \.self) {
Label($0, systemImage: "leaf.fill")
// Text($0)
}
}
}
}
// .listStyle(GroupedListStyle())
}
}

SwiftUI : List (ListStyle / onDelete / onMove) - List - 정적 콘텐츠와 동적 콘텐츠 조합 - SectionSwiftUI : List (ListStyle / onDelete / onMove) - List - 정적 콘텐츠와 동적 콘텐츠 조합 - Section
 왼: list Style미적용   /   오: GroupedListStyle 적용

ListStyle

ListStyle을 사용하기위해서는 List { ... } / .listStyle() 처럼 리스트 밖에 스타일을 추가해줘야 합니다.

swift
UNFOLDED
List { ... }
.listStyle(DefaultListStyle())

ListStyle 종류

사실 ListStyle의 종류는 다양하게 많이 있습니다. 일단 IOS에서 사용 가능한 스타일종류를 보자면 6가지 정도 됩니다.

  • DefaultListStyle()  - 기본 리스트 스타일
  • GroupedListStyle() - 각 섹션을 분리된 그룹으로 묶어 표현하는 스타일
  • InsetGroupedListStyle()
  • PlainListStyle() - 데이터 목록을 각 행마다 하나씩 나열하는 형태의 기본 스타일
  • InsetListStyle()
  • SidebarListStyle()

스타일을 확인 하기 위해 일단 코드를 작성해주도록 하겠습니다.

swift
UNFOLDED
struct TaskRow: View {
var body: some View {
Text("Hello, World!")
}
}
struct ContentView: View {
var body: some View {
List {
Section(header: Text("Header"), footer: Text("footer"), content: {
TaskRow()
TaskRow()
TaskRow()
})
Section {
TaskRow()
TaskRow()
TaskRow()
}
}
//listStyle을 정해줌
.listStyle(DefaultListStyle())
}
}

SwiftUI : List (ListStyle / onDelete / onMove) - List - ListStyle - ListStyle 종류
SwiftUI : List (ListStyle / onDelete / onMove) - List - ListStyle - ListStyle 종류

 

스타일 스크린샷을 확인해보면 DefaultPlain은 똑같은 화면인 것을 확인 할 수 있는데,

 

- Default는 플랫폼의 기본적인 동작 / appearanceListStyle.

- Plain은 그냥 plain list의 동작 / appearanceListStyle.

 

insetListStyle도 똑같아 보이지만 Default/Plain에 비해  Header 부분 Leading space 가 더 큽니다.

 

Sidebar리스트 스타일은 보시다시피 섹션을 접었다 펼 수 있는데, footer 부분은 사라지는것을 확인 할 수 있습니다.

 

SwiftUI : List (ListStyle / onDelete / onMove) - List - ListStyle - ListStyle 종류

onDelete / onMove

List 내부에있는 텍스트를 옮기거나 삭제할 수 도 있습니다. 이것을 사용하려면 .onDelete(perform: ).onMove(perform: ) 를 사용하고 함수를 정의해줘야 합니다.

onDelete 코드

swift
UNFOLDED
func removeList(at offsets: IndexSet) {
users.remove(atOffsets: offsets)
}

onMove 코드

swift
UNFOLDED
func moveList(from source: IndexSet, to destination: Int) {
users.move(fromOffsets: source, toOffset: destination)
}

위 코드를 사용하기 위해서는 ListNavigationView 로 감싸고, 그 옆에 EditButton을 추가해줘야 합니다. 이 버튼이 없으면 Text를 슬라이스 해서 삭제는 할 수 있지만 Move는 할 수 없습니다.

swift
UNFOLDED
.toolbar { EditButton() }
swift
UNFOLDED
.navigationBarItems(trailing: EditButton())
swift
UNFOLDED
struct ContentView: View {
@State private var users = ["포뇨", "소스케", "서근"]
var body: some View {
NavigationView {
List {
ForEach(users, id: \.self) { user in
Text(user)
}
.onDelete(perform: removeList)
.onMove(perform: moveList)
}
.navigationTitle("학생")
.toolbar { EditButton() }
}
}
//함수구현부
func removeList(at offsets: IndexSet) {
users.remove(atOffsets: offsets)
}
func moveList(from source: IndexSet, to destination: Int) {
users.move(fromOffsets: source, toOffset: destination)
}
}

Selection in Lists

목록 데이터의 Identifiable 단일 인스턴스에 바인딩합니다.

Id 유형은 단일 선택 목록을 생성합니다. Set에 바인딩하면 여러 선택 항목을 지원하는 목록이 생성됩니다.

다음 예제 에서는 위 예제에 다중 선택을 추가하는 방법을 사용하려고 합니다. 목록 아래의 Text view에는 현재 선택된 항목 수가 표시됩니다.

 

첫번째, @State 변수를 추가해 줍니다.

swift
UNFOLDED
struct ContentView: View {
@State private var multiSelection = Set<UUID>()
var body: some View {
}
}

두번째, NavigationView를 생성 후 ListmultiSelection을 추가해줍니다.

swift
UNFOLDED
List(ghibli, selection: $multiSelection)

세번째, NavigationBarTitle을 이용하여 이름을 설정해주고, 그 옆에 .toolbar { EditButton() } 를 추가해 선택 가능한 버튼을 만들어 줍니다.

swift
UNFOLDED
NavigationView {
List(ghibli, selection: $multiSelection) {
Text($0.name).font(.system(size: 20))
}
.navigationBarTitle("Studio Ghibli")
.toolbar { EditButton() }
}

네번째, 화면 하단에 몇개가 선택 되었는지 표시해줍니다.

swift
UNFOLDED
var body: some View {
NavigationView { ... }
Text("\(multiSelection.count)selections")
}

SwiftUI : List (ListStyle / onDelete / onMove) - List - onDelete / onMove - Selection in Lists

전체 코드

더보기
swift
UNFOLDED
import SwiftUI
struct Ghibli: Identifiable {
let name: String
let id = UUID()
}
private var ghibli = [
Ghibli(name: "포뇨"),
Ghibli(name: "소스케"),
Ghibli(name: "소피아"),
Ghibli(name: "센"),
Ghibli(name: "하쿠"),
Ghibli(name: "토토로")
]
struct ContentView: View {
@State private var multiSelection = Set<UUID>()
var body: some View {
NavigationView {
List(ghibli, selection: $multiSelection) {
Text($0.name).font(.system(size: 20))
}
.navigationBarTitle("Studio Ghibli")
.toolbar { EditButton() }
}
Text("\(multiSelection.count) selections")
}
}

SwiftUI : List (ListStyle / onDelete / onMove) - List - onDelete / onMove - 전체 코드

리스트의 여백 설정 listRowInsets 

list에는 기본적으로 padding이 들어가있습니다. 이미지를 적용시켜보면 더 쉽게 볼 수 있습니다. 이 여백을 설정하고 싶으면 listRowInsets 를 사용하면 됩니다.

swift
UNFOLDED
.listRowInsets(EdgeInsets.init())
///
.listRowInsets(EdgeInsets.init(top: CGFloat, leading: CGFloat, bottom: CGFloat, trailing: CGFloat))

예시와 함께 확인해보도록 하겠습니다.

우선 Mycard라는 파일을 만들어서 아래와 같이 코드를 작성해주겠습니다.

swift
UNFOLDED
//MyCard
struct MyCard: View {
var MyImage: String
var MyText: String
var Houre: Int
var Min: Int
var BgColor: Color
var body: some View {
HStack {
Image(systemName: "\(MyImage)")
.font(.system(size: 50))
.foregroundColor(.white)
.frame(width: 90)
.padding()
VStack(alignment: .leading, spacing: 0){
Divider().opacity(0)
Text("\(MyText)")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white)
.padding(.bottom, 10)
.lineLimit(1)
Text("예상 소요 시간 : \(Houre)시간 \(Min)분")
.font(.footnote)
.foregroundColor(.white)
}
}
.frame(height: 100, alignment: .center)
.background(BgColor)
}
}

SwiftUI : List (ListStyle / onDelete / onMove) - List - 리스트의 여백 설정 listRowInsets 

그리고 MyList라는 새로운 파일을 생성해서 위 이미지를 호출합니다.

swift
UNFOLDED
struct MyList: View {
var body: some View {
List {
Section(header: Text("오늘 할일")) {
ForEach(1...5, id: \.self) { index in
MyCard(MyImage: "scribble.variable", MyText: "SwiftUI : Text \(index)", Houre: 1, Min: 20 , BgColor: Color(#colorLiteral(red: 0.1960784346, green: 0.3411764801, blue: 0.1019607857, alpha: 1)))
}
}
//.listRowInsets(EdgeInsets.init())
.listRowInsets(EdgeInsets.init(top: 10, leading: 10, bottom: 10, trailing: 10))
}
.listStyle(GroupedListStyle())
}
}

위 코드에서 .listRowInsets(EdgeInsets.init())가 있을 때 와 없을 때를 비교 해 볼 수 있습니다.

SwiftUI : List (ListStyle / onDelete / onMove) - List - 리스트의 여백 설정 listRowInsets 
왼: 기본 리스트 스타일 / 가운데: init()&amp;nbsp; /&amp;nbsp; 오:(top: 10, leading: 10, bottom: 10, trailing: 10)

 

 

읽어주셔서 감사합니다🤟

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.