SwiftUI: onDelete()

minin·2021년 7월 5일
1

SwiftUI

목록 보기
1/2

onDelete()

🔖 참고한 문서
🙌 How to create views in a loop using ForEach
🙌 Deleting items using onDelete()

SwiftUI 실습을 하던 중, 별 것 입력하지 않았는데 스와이프를 통해 항목이 삭제되는 것을 발견했다! onDelete()에 대해서 알아보자.

그리고 그 전에, 아래의 코드를 이해하기 위해서 먼저 이해해야 하는,
id: .self$0 에 대해서 알아보자.

id: .self 와 $0

struct ContentView: View {
    @State private var numbers = [Int]()
    @State private var currentNumber = 1

    var body: some View {
        VStack {
            List {
                ForEach(numbers, id: \.self) {
                    Text("\($0)")
                }
            }

            Button("Add Number") {
                self.numbers.append(self.currentNumber)
                self.currentNumber += 1
            }
        }
    }
}

*id: \.self: SwiftUI가 배열 안에 있는 각각의 고유한 element들을 식별할 수 있게 하기 위해서 필요하다. 만약 하나의 아이템을 추가하거나 삭제했을 때, SwiftUI가 어떤 것이 처리되었는지 정확하게 안다는 뜻이다.

*\($0): $0은 첫번째 요소를 뜻한다. 따라서 위와 같이 Text("($0)") 라고 하면, 첫 번째 요소를 적는다는 의미

만약 배열에 커스텀 타입이 있는 경우, 모든 유형에서 고유하게 식별할 수 있는 property에 id를 사용해야 한다.

예를 들어, id 프로퍼티가 UUID인 하나의 구조체를 만들 수 있는데, 여기에서 UUID는 고유함을 보장한다.

struct SimpleGameResult {
    let id = UUID()
    let score: Int
}

struct ContentView: View {
    let results = [
        SimpleGameResult(score: 8),
        SimpleGameResult(score: 5),
        SimpleGameResult(score: 10)
    ]

    var body: some View {
        VStack {
            ForEach(results, id: \.id) { result in
                Text("Result: \(result.score)")
            }
        }
    }
}

여기에서 SwiftUIsms 그들의 id 프로퍼티를 통해서 ForEach 내에 있는 뷰들을 구분할 수 있다.

만약, Identifiable 프로토콜을 사용하면 Foreach(results)만 써도 된다.(id생략 가능)

Identifiable 프로토콜에서 id 프로퍼티를 넣는 것이 각각의 오브젝트의 고유함을 보장하기 때문이다.

그래서 위와 같은 결과를 낸다.

struct IdentifiableGameResult: Identifiable {
    var id = UUID()
    var score: Int
}

struct ContentView: View {
    let results = [
        IdentifiableGameResult(score: 8),
        IdentifiableGameResult(score: 5),
        IdentifiableGameResult(score: 10)
    ]

    var body: some View {
        VStack {
            ForEach(results) { result in
                Text("Result: \(result.score)")
            }
        }
    }
}

onDelete()

다시 돌아와서 다음의 코드를 빌드하면 다음화면과 같아진다.

Add Number 버튼을 누를 때마다 currnetNumber + 1 된 항목이 리스트에 추가된다.

import SwiftUI

struct PracticeView: View {
    @State private var numbers = [Int]()
    @State private var currentNumber = 1
    
    var body: some View {
        VStack {
            List {
                ForEach(numbers, id: \.self) { //\.self: 배열의 각 요소를 고유하게 식별 할 수 있도록. 어떤 것을 추가/삭제했는지 SwiftUI가 정확하게 알고 있따는 뜻
                    Text("\($0)") //first parameter
                }
            } //: LIST
            
            Button("Add Number") {
                self.numbers.append(self.currentNumber)
                self.currentNumber += 1
            }
        }
    }
}

여기에서, 이 리스트는 dynamic rows로 만들어졌기 때문에 ForEach가 필요하지 않다고 여겨진다. 그리고 실제로 아래처럼 적어도 실행 가능하다.(??)왜 ForEach가 필요하지 않다고 여겨지는지 모르겠따....필요하다고 여겨지는데...dynamic row를 공부해야겠다. 무튼 아래의 코드로 수정했을 때에도 똑같이 돌아가는 것을 확인했다.

List(numbers, id: \.self) {
    Text("\($0)")
}

그런데 문제는, *onDelete() modifier은 ForEach 구문에서만 존재한다는 것이다.* 그래서 유저가 아이템들을 지울 수 있게 하고 싶다면 반드시 아이템들을 ForEach 내에 위치시켜야 한다.

onDelete()를 사용하기 위해서, IndexSet 타입의 싱글 파라미터를 받아오는 메소드를 이용해야 한다. 이는 정렬을 제외한 정수 셋과 비슷한데, ForEach 내의 모든 아이템들의 위치가 모두 지워져야 한다고 말한다.

왜냐, ForEach는 싱글 array로 생성되기 때문에, 우리는 Idnex set을 지나쳐 우리의 numbers 배열로 갈 수 있기 때문인다. numbers는 index set을 수용하는 특별한 remove(atOffsets:)를 가지고 있는 메소드이다.

그래서, ContentView에 아래를 추가해야한다.

func removeRows(at offsets: IndexSet) {
    numbers.remove(atOffsets: offsets)
}

드디어, SwiftUI에게 ForEach로부터 데이터를 지우고 싶을 때 해당 메소드를 호출하라고 할 수 있다. (다음처럼 수정해라)

List{
                ForEach(numbers, id: \.self) { //\.self: 배열의 각 요소를 고유하게 식별 할 수 있도록. 어떤 것을 추가/삭제했는지 SwiftUI가 정확하게 알고 있따는 뜻
                    Text("\($0)") //first parameter
                }
                .onDelete(perform: removeRows)

Edit 버튼을 통해 여러 항목을 동시에 삭제할 수 있도록 하기 위해서는 VStackNavigationView로 감싸고, modifier를 VStack에 더한다.

NavigationView {
            VStack {
                List{
                    ForEach(numbers, id: \.self) { //\.self: 배열의 각 요소를 고유하게 식별 할 수 있도록. 어떤 것을 추가/삭제했는지 SwiftUI가 정확하게 알고 있따는 뜻
                        Text("\($0)") //first parameter
                    }
                    .onDelete(perform: removeRows)
                } //: LIST
                
                Button("Add Number") {
                    self.numbers.append(self.currentNumber)
                    self.currentNumber += 1
                }
            } //:VStack
            .navigationBarItems(leading: EditButton())
        } //: NAVIGATIONBAR

오잉 근데 별로 편하지 않은데..? 저렇게 바를 만들어도 한번에 삭제될 수 있는 건 아니라서 편하지 않다. 어떻게 해야 항목을 선택할 수 있지?

아무튼, 최종코드!

import SwiftUI

struct PracticeView: View {
    @State private var numbers = [Int]()
    @State private var currentNumber = 1
    
    var body: some View {
        NavigationView {
            VStack {
                List{
                    ForEach(numbers, id: \.self) { //\.self: 배열의 각 요소를 고유하게 식별 할 수 있도록. 어떤 것을 추가/삭제했는지 SwiftUI가 정확하게 알고 있따는 뜻
                        Text("\($0)") //first parameter
                    }
                    .onDelete(perform: removeRows)
                } //: LIST
                
                Button("Add Number") {
                    self.numbers.append(self.currentNumber)
                    self.currentNumber += 1
                }
            } //:VStack
            .navigationBarItems(leading: EditButton())
        } //: NAVIGATIONBAR
    }
    
    func removeRows(at offsets: IndexSet) {
        numbers.remove(atOffsets: offsets)
    }
}

struct PracticeView_Previews: PreviewProvider {
    static var previews: some View {
        PracticeView()
    }
}
profile
🍫 iOS 🍫 Swift

0개의 댓글