포스트

To Do List (4)

수정 기능 추가하기

Leading edge부에도 SwipeAction을 추가하여 수정을 하게 만들어보려고한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
VStack {
                List(todoLists, id: \.self) { list in
                    CellView(isOn: list.isCompleted,
                             title: "\(list.title) id: \(list.id)")
                    .swipeActions(edge: .trailing) {
                        Button(action: {
                            modelContext.delete(list)
                        }) {
                            Image(systemName: "trash")
                        }
                    }
                    .tint(.red)
                    .swipeActions(edge: .leading) {
                        Button("edit",
                               systemImage: "pencil") {
                            isEditing.toggle()
                        }
                               .alert("TodoList 수정",
                                      isPresented: $isEditing) {
                                   
                                   TextField("TodoList 수정", text: $title)
                                   Button("OK",
                                          role: .cancel) {
                                        print("work")
                                   }
                                   Button("Cancel", role: .destructive){
                                       
                                   }
                               }
                    }
                    .tint(.blue)
                }
            }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
VStack {
                List(todoLists, id: \.self) { list in
                    CellView(isOn: list.isCompleted,
                             title: "\(list.title)")
                    .swipeActions(edge: .trailing) {
                        Button(action: {
                            modelContext.delete(list)
                        }) {
                            Image(systemName: "trash")
                        }
                    }
                    .tint(.red)
                    .swipeActions(edge: .leading) {
                        Button("edit",
                               systemImage: "pencil") {
                            print(isEditing)
                            isEditing.toggle()
                            print(isEditing)
                        }
                    }
                    .tint(.blue)
                    .alert("TodoList 수정",
                           isPresented: $isEditing) {
                        
                        TextField("TodoList 수정", text: $title)
                        Button("OK",
                               role: .cancel) {
                            print("work")
                        }
                        Button("Cancel", role: .destructive){
                            
                        }
                    }
                }
                
            }

현재 Delete와 유사하게 코드를 작성했다.

하지만 작동하지 않았다.

그래서 isEditing 변수에 관해 어떻게 작동을 하는지 확인을 하기위해 print을 해보았는데

1
2
3
4
true
false
true
false

이런식으로 계속 바뀌는걸 확인했다.

그러다 StackOverflow 에 비슷한 글을 보았고 List 쪽에 modifier를 써야한다는걸 알았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
VStack {
                List(todoLists, id: \.self) { list in
                    CellView(isOn: list.isCompleted,
                             title: "\(list.title) id: \(list.id)")
                    .swipeActions(edge: .trailing) {
                        Button(action: {
                            modelContext.delete(list)
                        }) {
                            Image(systemName: "trash")
                        }
                    }
                    .tint(.red)
                    .swipeActions(edge: .leading) {
                        Button("edit",
                               systemImage: "pencil") {
                            isEditing.toggle()
                        }
                    }
                    .tint(.blue)
                }
                .alert("TodoList 수정",
                       isPresented: $isEditing) {
                    
                    TextField("TodoList 수정", text: $title)
                    Button("OK",
                           role: .cancel) {
                        print("work")
                    }
                    Button("Cancel", role: .destructive){
                        
                    }
                }
            }

CleanShot 2024-10-23 at 12 10 41

작동이 된다.

구분첫 번째 코드 (CellView에서 alert 적용)두 번째 코드 (List에서 alert 적용)
alert 적용 위치CellView에 개별적으로 적용List 전체에 적용
동작 결과작동 안됨: alert가 표시되지 않음정상 작동: alert가 정상적으로 표시됨

작동하지 않는 이유

  1. List에서의 뷰 재사용 문제:
    • SwiftUIList는 내부적으로 뷰를 재사용한다. 즉, 셀(Cell)이 다시 그려질 때 뷰가 재사용될 수 있기 때문에, 개별 CellView에 대한 상태(State) 관리가 예상대로 동작하지 않을 수 있다.
    • CellView에 직접 alert를 적용하면 SwiftUI가 이 상태를 적절히 처리하지 못해, alert가 표시되지 않는 문제가 발생한다.
  2. 뷰의 계층 구조:
    • alert는 보통 상위 뷰에서 상태를 관리하며, 하위 뷰가 그 상태 변화를 인식할 수 있도록 한다. 첫 번째 코드에서는 CellView에 적용된 alert가 상위 List에 의한 상태 변화를 반영하지 못해, isEditing 상태가 바뀌더라도 alert가 나타나지 않는다.
    • 두 번째 코드에서는 List상위 계층alert가 적용되었기 때문에, 전체 리스트 상태와 연동되어 정상적으로 동작한다.

해결 방법

  • alert 같은 상태 기반의 모디파이어는 상위 뷰에서 적용해야 한다. 이를 통해 SwiftUI의 상태 관리와 뷰 재사용 메커니즘이 정상적으로 작동하게 된다.
  • 즉, 상태(State)뷰 재사용이 일어나는 리스트 구조에서 안정적으로 작동하려면, 리스트 자체 또는 리스트의 상위 뷰에 alert 같은 모디파이어를 적용하는 것이 좋다.

첫 번째 코드가 작동하지 않는 이유는 SwiftUI의 뷰 재사용과 상태 관리 문제 때문이다. alert를 리스트 수준에서 관리하면 이런 문제를 피할 수 있고, 상태 변경이 제대로 반영되어 alert가 정상적으로 표시된다.

문제해결

구분첫 번째 코드두 번째 코드
alert 위치CellView 내부에 있는 각 항목에서 alert가 개별적으로 정의됨List 수준에서 한 번 정의되어 전체 리스트에서 상태를 공유함
isEditing 상태 관리isEditing 상태가 개별 항목에서 변경될 때마다 alert가 그 항목에만 영향을 미침isEditing 상태가 리스트 전체에서 전역적으로 적용되어, 모든 항목에서 동일하게 반응함
상태 변경 시점개별 항목의 상태가 변경될 때마다 그 항목에서 alert가 뜨도록 유도함isEditing 상태가 변경될 때 리스트 전체에서 반응하며, 모든 항목이 동일하게 상태 변화를 겪음
가독성 및 유지보수항목별로 alert가 존재하므로 복잡도가 올라가고 유지보수 시 관리해야 할 항목이 많아짐List 수준에서 한 번만 정의되므로 코드가 더 간결하고 유지보수가 쉬움

첫 번째 코드:

  • CellView 내부에 alert가 정의되어 있어, 각 항목마다 개별적으로 isEditing 상태를 변경하고, 해당 항목에서만 alert를 띄울 수 있다.
  • 그러나 여전히 isEditing 상태는 전역적으로 관리되고 있어, 모든 항목에서 동일한 isEditing 상태를 공유한다는 점에서, 실질적인 개별 항목별 관리가 되지 않는다.

두 번째 코드:

  • alertList 레벨에서 한 번만 정의되어 있어, isEditing 상태가 변경되면 모든 항목에서 반응하게 된다.
  • 리스트 전체에서 하나의 상태로 관리되고 있어, 상태 관리가 더 일관성 있게 동작하지만, 개별 항목의 상태를 별도로 관리하고자 할 때는 유연성이 떨어진다.

결론:

  • 첫 번째 코드는 각 항목에서 개별적으로 alert를 정의해 유연성을 제공하지만, 상태 관리 측면에서는 불완전하다.
  • 두 번째 코드alert를 한 곳에 모아두어 관리하기 쉽고 일관되지만, 개별 항목의 편집 상태를 세밀하게 관리하는 데는 부적합하다.

또 다른 문제 해결

회고를 하던 도중

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
VStack {
                List(todoLists, id: \.self) { list in
                    CellView(isOn: list.isCompleted,
                             title: "\(list.title)")
                    .swipeActions(edge: .trailing) {
                        Button(action: {
                            modelContext.delete(list)
                        }) {
                            Image(systemName: "trash")
                        }
                    }
                    .tint(.red)
                    .swipeActions(edge: .leading) {
                        Button("edit",
                               systemImage: "pencil") {
                            isEditing.toggle()
                            print(list.title)
                        }
                    }
                    .tint(.blue)
                    .alert("TodoList 수정",
                           isPresented: $isEditing) {
                        TextField(list.title, text: $title)
                        Button("OK",
                               role: .cancel) {
                            print(list.title)
                        }
                        Button("Cancel", role: .destructive){
                            
                        }
                    }
                }
                
            }

text를 수정할때 가져오는 list.title이 다르다는것을 발견하였다.

즉 나는 첫번째 row에 대해서 내용을 수정하고 싶은데, 정작 수정을 하고보면 두번째 row가 수정이 되는 문제가 발생하였따.

CleanShot 2024-10-24 at 10 37 50

첫번째 row를 버튼 클릭하고 수정 하고 확인 버튼을 눌렀을때 우선 console에 출력을 해보면

1
2
Whew
Wer4334545

이렇게 되어버린다.

즉 같은 list.title이지만 내용은 다르다는것.

print(list.title)swipeActionsalert에서 다르게 출력되는 이유는 SwiftUI의 List에서 셀 재사용 방식 때문이다.

SwiftUI의 셀 재사용 관련 원인:

  1. 뷰의 재사용: SwiftUI의 List셀을 재사용한다. 즉, 셀이 화면에 다시 나타나거나 스크롤될 때 기존의 셀을 새 데이터로 업데이트한다. 이로 인해 swipeActions에서 사용한 list는 그 시점의 데이터를 참조하지만, alert가 호출될 때는 다른 셀이 재사용될 수 있다.

  2. 상태의 비동기 처리: isEditing.toggle()가 호출되면 상태가 변하지만, SwiftUI는 내부적으로 필요한 부분만 다시 렌더링한다. 이로 인해 alert가 표시될 때의 listswipeActions에서의 list가 다르게 출력될 수 있다.

  3. 뷰의 동작 흐름 차이:

    • swipeActions에서는 print(list.title)즉시 해당 셀의 데이터를 출력한다.
    • 그러나 alert가 표시될 때는 셀이 재사용되거나 다른 list 객체가 참조될 수 있어, print(list.title)가 다른 결과를 출력할 수 있다.

해결 방법

현재 편집 중인 항목의 데이터를 선택된 항목의 상태로 관리하여 동일한 데이터를 참조하게 하는 방법을 사용한다.

수정된 코드 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@State private var selectedList: TodoModel? = nil

VStack {
    List(todoLists, id: \.self) { list in
        CellView(isOn: list.isCompleted,
                 title: "\(list.title)")
        .swipeActions(edge: .trailing) {
            Button(action: {
                modelContext.delete(list)
            }) {
                Image(systemName: "trash")
            }
        }
        .tint(.red)
        .swipeActions(edge: .leading) {
            Button("edit",
                   systemImage: "pencil") {
                selectedList = list // 현재 선택된 리스트 저장
                isEditing.toggle()
                print(list.title)
            }
        }
        .tint(.blue)
        .alert("TodoList 수정",
               isPresented: $isEditing) {
            if let selectedList = selectedList {
                TextField(selectedList.title, text: $title)
                Button("OK", role: .cancel) {
                    print(selectedList.title) // 선택된 리스트의 title 출력
                }
                Button("Cancel", role: .destructive) {}
            }
        }
    }
}

정리

  • 뷰 재사용 문제로 인해 List에서 list가 다르게 출력될 수 있다.
  • 선택된 항목을 상태로 관리하여, alert와 swipeActions에서 같은 데이터를 참조하게 함으로써 문제를 해결할 수 있다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.