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

SwiftUI : List (ListStyle / onDelete / onMove)

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

 

 

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

List

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

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

정적 콘텐츠

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

TIP
 
 

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

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

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)     
}

동적 콘텐츠

Range <Int>

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

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

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와 위에 작성한 배열을 넣어줘야 합니다.

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

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

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

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

ForEach

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

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

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

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

조합

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

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)")
            }
        }
    }
}

Section

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

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())
    }
}

 왼: list Style미적용   /   오: GroupedListStyle 적용

ListStyle

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

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

ListStyle 종류

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

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

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

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())
    }
}

 

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

 

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

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

 

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

 

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

 

onDelete / onMove

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

onDelete 코드

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

onMove 코드

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

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

.toolbar { EditButton() }
.navigationBarItems(trailing: EditButton())
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 변수를 추가해 줍니다.

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

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

List(ghibli, selection: $multiSelection)

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

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

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

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

전체 코드

더보기
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")
    }
    
}

리스트의 여백 설정 listRowInsets 

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

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

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

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

//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)
 
    }
}

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

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())가 있을 때 와 없을 때를 비교 해 볼 수 있습니다.

왼: 기본 리스트 스타일 / 가운데: init()&amp;nbsp; /&amp;nbsp; 오:(top: 10, leading: 10, bottom: 10, trailing: 10)

 

 

읽어주셔서 감사합니다🤟

 


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


서근


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