Issue 해결

[Issue 해결] SwiftUI에서 하나의 뷰에 여러 개의 Alert 나타내기

yyomzzi 2023. 7. 22. 16:40

오느른..~ swiftUI로 앱을 만들다가 만나게 된 이슈를 해결한 과정을 보여드릴까 합니댜

swiftUI로 몇 가지 아~주 작은 앱들을 만들어 보면서 최근에 배우게 된 Alert를 많이 사용하게 되는데

그을쎄 요 Alert가 코드로 넣는다고 다 나오는게 아니더군여~!

그래서 어떤 이슈가 있었고 어떻게 해결했는지 과정을 한 번 기록해볼까 함다

어쩌면 별거 없을 수도 있지만 저에게는 꽤나 대박발견인 사건이라,,!


일단 어떤 앱을 만들고 있었는지를 보여드릴게용용용

엄청 별거 없는 앱일 수도 있으니 기대는 접어두시거..

빈 화면이 나타나고 sheet를 통해 이미 설정되어있는 캐릭터를 추가해서

추가된 캐릭터들을 정렬해보는 앱임다,, 쿄쿄 

 

그래서 캐릭터가 추가되거나 정렬을 하기 전에 alert를 통해서 그 작업을 실행할 것인지

사용자에게 물어본 뒤에, 원하면 실행하게 되는 ~ 그런 로직으로 만들어봤음

 

그래서 처음에 입력했던 alert 관련 코드는 이러하다

struct CharacterListView: View {
    
    ...
    
    @State var removeCharacter: Bool = false
    @State var sortedCharacter: Bool = false
    @State var randomCharacter: Bool = false
    
    ...
    
    var body: some View {
        NavigationStack {
        
        ...
        
        
            Button {
                removeCharacter = true
            } label: {
                Label("삭제", systemImage: "trash")
            }
            
            Button {
                sortedCharacter = true
            } label: {
                Text("캐릭터 줄세우기")
                    .font(.title2)
                    .padding()
            }
            .disabled(characterStore.addCharacters.count < 2)
            
            Button {
                randomCharacter = true
            } label: {
                Text("캐릭터 랜덤 줄세우기")
                    .font(.title2)
                    .padding()
            }
            .disabled(characterStore.addCharacters.count < 2)
            
        }
        
        ...
        
        
        // ⭐️ 요기서부터가 알러트가 나오는 시점입니다!!!!!!⭐️
        
        //  알러트(1) : 캐릭터 삭제 알러트
        .alert(isPresented: $removeCharacter) {
            Alert(title: Text("캐릭터를 삭제하시겠습니까?"),
                  primaryButton: .cancel(Text("취소")),
                  secondaryButton: .destructive(Text("삭제")) {
                characterStore.removeCharacter(character: removeSelectCharacter)
            })
        }
        
        //  알러트(2) : 캐릭터 줄세우기 알러트
        .alert(isPresented: $sortedCharacter) {
            Alert(title: Text("캐릭터를 줄세우시겠습니까?"),
                  primaryButton: .cancel(Text("취소")),
                  secondaryButton: .destructive(Text("실행")) {
                characterStore.sortedArray()
            })
        }
        
        
        //  알러트(3) : 캐릭터 랜덤으로 줄세우기 알러트
        .alert(isPresented: $randomCharacter) {
            Alert(title: Text("캐릭터를 랜덤으로 줄세우시겠습니까?"),
                  primaryButton: .cancel(Text("취소")),
                  secondaryButton: .destructive(Text("실행")) {
                characterStore.randomOrderArray()
            })
        }
        
    }
}

일단 이렇게 코드를 입력했던 이유는

일단 alert의 위치가 alert를 나타내려는 버튼이 있는 뷰에다가만 연결하면 된다고 생각을 했었고

또 alert끼리 모아놓는 것이 코드가 깔끔하고 가독성이 편하지 않을까? 하는 마음이었다.

 

그런데 삐용삐용 비상사태 미챠미챠

이 코드로 실행을 해봤더니 제일 마지막에 입력되어있는 캐릭터를 랜덤으로 줄세우는 alert밖에 실행이 안되는거임..!

 

(초기에 구현하다가 안되는거 녹화한거라서 화면구성은 조금 다름)

 

그래서 정말 코드를 이렇게도 바꿔보고 저렇게도 바꿔보고 수정 수정 크리스탈 크리스탈을 해봤는데

알게된 하나의 사실은 가장 마지막에 입력되어있는 alert 코드만 된다는거였음.

그래서 아 혹시 alert 여러개를 연속으로 쓸 수가 없는것인가..? 하는 생각이 들어서

연속적인 적용이 안되는지를 계속 찾아봤는데 특별히 이렇다할 이유를 찾지 못했음. 

째뜬 이 현상에 대한 정확한 이유를 모르겠는거임,,!

 

그래서 다른 조원분들에게 SOS를 쳤고 조원 중 한 분이 요런 글을 하나 보내주셨드아!

 

https://sarunw.com/posts/how-to-show-multiple-alerts-on-the-same-view-in-swiftui/

 

How to show multiple alerts on the same view in SwiftUI | Sarunw

If you have ever worked with an app with multiple alerts, please beware that the system can present only the latest or outermost one. Let's see how we can mitigate this.

sarunw.com

띠용!!! 

일단 제목이 <How to show multiple alerts on the same view in SwiftUI> 였음.

그렇다면 multiple alert를 같은 뷰에서 보여줄 수 있는 방법이 따로 있다는 말이 아니겠음?????

그래서 바로 들어가서 확인을 해보았다.

 

글 내용인 즉슨!

하나의 view에서 multiple alert는 가능하지 않고, 가장 바깥쪽에 있는 알러트만 적용이 된다는 내용인거임!!

엥 ??? 이게 뭔소리야 !!! 그럼 하나의 뷰에 버튼이랑 이런게 여러개가 있는데

대체 어떻게 하나의 alert만 사용할 수 있다는 말이야..?ㅠ 했지만

일단 alert와 상관없이 간과하고 있었던 것이 button, text, label 등등은 모두 View 프로토콜을 상속받은 구조체라는 것..!

그러니까 이 하나하나의 요소들이 다 view라는 것임!! 

 

쨌든,~ 그래서 이 아티클에 의하면

SwiftUI에서는 같은 뷰(또는 뷰 계층 구조의 같은 분기)에 여러 개의 .alert 수정자(modifier)를 사용할 수가 없다는 것!

만약 뷰의 계층 구조에서 같은 분기에 여러 개의 alert 수정자가 있는 경우에는 가장 바깥쪽에 있는 alert만 동작한다.

즉, 가장 최근에(가장 마지막에)추가된 alert만 표시가 되고 이전에 추가된 alert는 무시가 된다는 것이다!!

결국 하나의 뷰, 혹은 하나의 분기에서는 하나의 alert밖에 처리하지 못한다는 거임!! 

그래서 위의 기존의 코드에서도 가장 마지막에 있었던 random 으로 줄세우는 버튼의 alert만 작동을 했었던 것이다.

 

WOW


📍Multiple alerts on the same view

그래서 이 아티클에 의하면 multiple alert가 적용되지 않는 몇가지 상황이 있는데

 

1) 같은 뷰에 여러개의 alert가 존재하는 상황

2) 뷰 계층에서 같은 분기에 여러개의 alert가 존재하는 상황

으로 가정해 볼 수 있다.

 

아티클의 코드가 이해가 잘 되어서 가져와봤숨.

struct ContentView: View {
    @State private var presentAlert1 = false
    @State private var presentAlert2 = false

    var body: some View {
        VStack {
            Button("Alert 1") {
                presentAlert1 = true
            }
            Button("Alert 2") {
                presentAlert2 = true
            }
        }
        .alert(isPresented: $presentAlert1) {
            Alert(
                title: Text("Title 1"),
                message: Text("Message 1")
            )
        }
        .alert(isPresented: $presentAlert2) { // 1

            Alert(
                title: Text("Title 2"),
                message: Text("Message 2")
            )
        }
    }
}

이 예시가 바로 내가 작성했던 코드랑 똑같은 상황임.

코드를 보면, 현재 VStack 에 alert가 2개가 붙어있는 상황

 

이런 상황은 결국 그림으로 보자면

아티클에 있는거 프리폼으로 한 번 그려봄

VStack이 그리는 view에는 Button 2개가 같은 분기에 있다. 현재 VStack view에 2개의 .alert가 존재한다.

이럴경우에는 가장 마지막에 추가된(가장 최신의) .alert2밖에 적용되지 않는다는 뜻이다.

 


💡Solutions

그러면 어떻게 해결할 수 있는가?

방법은 여러가지가 있을 수 있겠지만, 여기서는 크게 2가지 해결책을 제시한당

1) alert가 작동하는 해당 뷰에 직접 alert 코드 작성하기 (Move each alert to a different view)

2) Identifiable 프로토콜을 이용해서 하나의 alert 로 해결하기 (Single alert with Identifiable item)

=> 객체 안에서 여러 case를 나눠서 alert가 실행될 수 있도록 하기 (대표적으로 열거형 사용) 

 


1) alert가 작동하는 해당 뷰에 직접 alert 코드 작성하기

 

위에서 보여주었던 예시로 어떻게 해결하는지 보자면

결국 view와 alert를 1:1로 매칭해주면 되는거임

struct ContentView5: View {
    @State private var presentAlert1 = false
    @State private var presentAlert2 = false

    var body: some View {
        VStack {
            Button("Alert 1") {
                presentAlert1 = true
            }
            .alert(isPresented: $presentAlert1) {

                Alert(
                    title: Text("Title 1"),
                    message: Text("Message 1")
                )
            }
            Button("Alert 2") {
                presentAlert2 = true
            }
            .alert(isPresented: $presentAlert2) {

                Alert(
                    title: Text("Title 2"),
                    message: Text("Message 2")
                )
            }
        }
    }
}

요로케 Button 마다 각각의 버튼과 연관된 alert를 연결해주면 button view 마다

각각의 유일한 alert만 갖게 되는 것이다!

생각보다 쉽쥬~?

 

그래서 나는 이 방법을 내 코드에 적용해보았음

 

    Group {
            Button {
               sortedCharacter = true
             } label: {
                    Text("캐릭터 줄세우기")
                        .font(.title3)
                        .padding()
                 }
             
             ...
             
            .alert(isPresented: $sortedCharacter) {
                Alert(title: Text("캐릭터를 줄세우시겠습니까? 캐릭터의 파워 순서대로 나타납니다!"),
                    primaryButton: .cancel(Text("취소")),
                    secondaryButton: .destructive(Text("실행")) {
                   isShowingPower = true
                     characterStore.sortedArray()
                })
            }
        }
                
        Group {
            Button {
               randomCharacter = true
           } label: {
                Text("캐릭터 랜덤 줄세우기")
                    .font(.title3)
                    .padding()
                }
                
            ....
             
            .alert(isPresented: $randomCharacter) {
                Alert(title: Text("캐릭터를 랜덤으로 줄세우시겠습니까?"),
                      primaryButton: .cancel(Text("취소")),
                      secondaryButton: .destructive(Text("실행")) {
                    isShowingPower = false
                      characterStore.randomOrderArray()
                })
            }
        }

바로 이러케 각 Button(그냥 파워 순서대로 정렬시키는 버튼과 랜덤으로 정렬시키는 버튼) 마다 

관련된 alert를 직접 연결시켜서 view 마다 하나의 alert만 처리할 수 있도록 했다!! 

쉽다!!


2) Identifiable 프로토콜을 이용해서 하나의 alert 로 해결하기

 

이 방법은 바로 구조체나 열거형과 같은 객체를 통해 alert의 여러 case를 만들어서 각 case 별로 분기처리를 해준다는 건데, 

여기서의 핵심은 Identifiable 프로토콜로 각 case 별로 id 를 부여해서

하나의 Alert 내부에서 각각 다른 alert를 사용하는 방법이다.

(사실 나는 이 방법은 별로 와닿지 않아서 일단 이해만 해보고, 나는 그냥 Enum 을 통해서 적용해본 것을 추가할거여)

 

struct AlertInfo: Identifiable {

    enum AlertType {
        case one
        case two
    }

    let id: AlertType
    let title: String
    let message: String
}

struct ContentView6: View {
    // 2
    @State private var info: AlertInfo?


    var body: some View {
        VStack {
            Button("Alert 1") {
                // 3
                info = AlertInfo(

                    id: .one,
                    title: "Title 1",
                    message: "Message 1")
            }
            Button("Alert 2") {
                // 4
                info = AlertInfo(

                    id: .two,
                    title: "Title 2",
                    message: "Message 2")
            }
        }
        .alert(item: $info, content: { info in // 5

            Alert(title: Text(info.title),
                  message: Text(info.message))
        })
    }
}

바로 요로케 사용하는 것이다! 구조체 안에 열거형으로 case를 나눈 후에,

그 case를 id 값에 넣어 각각 따로 다른 alert로 보여질 수 있도록 처리하는 것이다!

이 방법도 어렵지는 않지만 나에게는 별로 와닿지 않음..

그래서 같은 조원분중에 Enum 마스터 분의 도움으로 Enum으로만 구현해봄!! 

 

enum ShowAlert {
    case removeAlert
    case sortedAlert
    case randomAlert
}

struct CharacterListView: View {

    @State var showAlert: ShowAlert = .removeAlert
    @State var isShowingAlert: Bool = false


...

var body: some View {
		Button {
              removeSelectCharacter = character
              showAlert = .removeAlert
               isShowingAlert = true
        } label: {
               Label("삭제", systemImage: "trash")
        }

...

	HStack {
            
        Group {
           Button {
                   showAlert = .sortedAlert
                   isShowingAlert = true
               } label: {
                    Text("캐릭터 줄세우기")
                       .font(.title3)
                       .padding()
                    }
                    

         Group {
            Button {
                    showAlert = .randomAlert
                    isShowingAlert = true
                } label: {
                    Text("캐릭터 랜덤 줄세우기")
                        .font(.title3)
                        .padding()
                    }
            }
	}
...

		.alert(isPresented: $isShowingAlert) {
         	   switch showAlert {
         	   case .removeAlert:
          	      return Alert(title: Text("캐릭터를 삭제하시겠습니까?"), 
				primaryButton: .cancel(Text("취소")), 
				secondaryButton: .destructive(Text("삭제")) {
                    characterStore.removeCharacter(character: removeSelectCharacter)
                })
        	    case .sortedAlert:
             	   return Alert(title: Text("캐릭터를 줄세우시겠습니까? 캐릭터의 파워 순서대로 정렬됩니다!"), 
				primaryButton: .cancel(Text("취소")), 
				secondaryButton: .destructive(Text("실행")) {
                    characterStore.sortedArray()
                    isShowingPower = true
                })
          		case .randomAlert:
            	    return Alert(title: Text("캐릭터를 랜덤으로 줄세우시겠습니까?"), 
				primaryButton: .cancel(Text("취소")), 
				secondaryButton: .destructive(Text("실행")) {
                    characterStore.randomOrderArray()
                    isShowingPower = false
                })
            }

바로 이런 방법이다! ShowAlert라고 열거형 type으로 선언해서 미리 case를 나누고,

정렬 Button들을 담고 있는 HStack view에 대해서 딱 하나의 alert만 선언했다.

그리고 alert 내에서 열거형의 분기처리를 했다. 

첫번째 방법보다 쉽지는 않지만 이 방법 또한 깔끔하게 코드를 정리할 수 있는 방법이라고 생각한다!!

 


📍Multiple alerts on the same branch in a view hierarchy

이 방법은 동일한 view 계층 구조에 있는 multiple alerts에 대한 내용임

사실 이 내용은 이 아티클에서 처음 봐서 아직 잘 감이 안오지만,, 이해한 대로 써보겄음

이 예시 또한 참고한 아티클에서 가져온 예시입니댜

 

struct AlertInfo: Identifiable {
    enum AlertType {
        case one
        case two
    }

    let id: AlertType
    let title: String
    let message: String
}

struct ContentView: View {
    @State private var info: AlertInfo?

    var body: some View {
        VStack {
            Button("Alert 1") {
                info = AlertInfo(
                    id: .one,
                    title: "Title 1",
                    message: "Message 1")
            }
            Button("Alert 2") {
                info = AlertInfo(
                    id: .two,
                    title: "Title 2",
                    message: "Message 2")
            }
            // 1
            NestedContentView()

        }
        .alert(item: $info, content: { info in
            Alert(title: Text(info.title),
                  message: Text(info.message))
        })
    }
}

// 2
struct NestedContentView: View {

    @State private var presentAlert = false

    var body: some View {
        VStack {
            Button("Nested Alert") {
                presentAlert = true
            }
        }
        .alert(isPresented: $presentAlert) {
            Alert(
                title: Text("Nested Title"),
                message: Text("Nested Message")
            )
        }
    }
}

이런 구조의 코드가 있음. 이 코드에서 중요한건 현재 ContentView의  VStack 안에는 2개의 버튼과

NestedContentView 라는 view를 가지고 있는데,

이 view는 외부에 따로 선언되어 있고, view내부를 보면 해당 view 만의 alert를 또 가지고 있다. 

Content view와 NestedContentView는 서로 다른 view 인것 처럼 보이지만

NestedContentView는 현재 ContentView 내부의 VStack 에 존재하고 있기 때문에 

같은 view 계층 구조에 있다고 볼 수 있다. (ContentView가 부모뷰)

 

 

 

 

그래서 현재 이 view 계층 구조에는 alert가 2개가 존재하는 것이다.

하나는 VStack에 직접 연결되어 있는 alert 이고 다른 하나는 NestedContentView에 있는 alert이다.

이런 경우에도 Only the outermost one will work (The one in ContentView). 이라고

아티클은 적어놓았다. 이 또한 가장 마지막, 최신의 alert만 작동하는 것이다.

그래서 ContentView의 alert만 작동하게 된다.

그렇다면 이런 경우는 어떻게 해결 하는가?

해당 아티클에서는 "Move each alert to a leaf node" 라고 알려준다..

(무슨 뜻이지..)

struct ContentView: View {
    @State private var presentAlert1 = false
    @State private var presentAlert2 = false

    var body: some View {
        VStack {
            Button("Alert 1") {
                presentAlert1 = true
            }
            .alert(isPresented: $presentAlert1) {
                Alert(
                    title: Text("Title 1"),
                    message: Text("Message 1")
                )
            }
            Button("Alert 2") {
                presentAlert2 = true
            }
            .alert(isPresented: $presentAlert2) {
                Alert(
                    title: Text("Title 2"),
                    message: Text("Message 2")
                )
            }
            NestedContentView()
        }


    }
}

struct NestedContentView: View {
    @State private var presentAlert = false

    var body: some View {
        VStack {
            Button("Nested Alert") {
                presentAlert = true
            }
            .alert(isPresented: $presentAlert) { // 1

                Alert(
                    title: Text("Nested Title"),
                    message: Text("Nested Message")
                )
            }
        }
    }
}

이렇다고 한다...! 사실 이 내용은 잘 이해가 가지 않아서 여기저기 찾아봤다.

일단 leaf node라는 단어와 초면이어서 찾아보았더니 트리 구조에서 나온 말이라고 한다. 

트리 구조.. 일단 뭔가 뿌리에서 나와서 줄기가 있고 나뭇잎이 있고 뭔가 서로 계층 이뤄서 이어진다는 것 같은데

그래서 node가 뭔데?ㅠ

 

 

 

그렇다고 한다. 데이터의 상하위 계층을 나타내는 것이 node 인데

여기서 "Move each alert to a leaf node" 해석하면 각각의 alert를 leaf node로 옮겨라!! 라고 했다.

그래서 찾아보니까 leaf node, 잎 node..! 잎이 나무의 무엇인가! 가장 끝 아닌가욜

잎에서는 무엇이 더 가지치면서 나오는 것이 아니니 가장 마지막! 결국 자식 노드가 없는 노드를 뜻한다고 함!!

그래서 지금 현재 코드에서 보자면 자식노드가 없는 leaf node 는 버튼과 같은 view인거임~

 

그래서 각 알람을 leaf node로 옮기면 된다고 함! 그러면 NestedContentView의 alert도 나타날 수 있는거임~

그래서 이것을 코드로 보자면

struct ContentView: View {
    @State private var presentAlert1 = false
    @State private var presentAlert2 = false

    var body: some View {
        VStack {
            Button("Alert 1") {
                presentAlert1 = true
            }
            .alert(isPresented: $presentAlert1) {
                Alert(
                    title: Text("Title 1"),
                    message: Text("Message 1")
                )
            }
            Button("Alert 2") {
                presentAlert2 = true
            }
            .alert(isPresented: $presentAlert2) {
                Alert(
                    title: Text("Title 2"),
                    message: Text("Message 2")
                )
            }
            NestedContentView()
        }


    }
}

struct NestedContentView: View {
    @State private var presentAlert = false

    var body: some View {
        VStack {
            Button("Nested Alert") {
                presentAlert = true
            }
            .alert(isPresented: $presentAlert) { // 1

                Alert(
                    title: Text("Nested Title"),
                    message: Text("Nested Message")
                )
            }
        }
    }
}

이렇게 바꿔주면 된다고 함! 휴우~ 이렇게 node 와 트리구조도 생각치 않게 보게 되었군용 ㅎㅎ


그래서 저 아티클에는 이 글 이후에도 @EnvironmentObject를 이용해서 모든 파일에 전반적으로 적용하는 방법까지

소개하는 것 같은데 아직 EnvironmentObjcet를 잘 몰라서 이건 좀 더 공부를 해봐야겠다.

 

 

알면 알수록 재밌는 Alert !!!

특히 나는 alert가 적절하게 잘 사용되어 있는 앱을 편리하다고 생각하기 때문에

(나처럼 덤벙대는 사람들은 터치 잘못해서 실수로 무언가를 삭제하고 ..

했던 경험이 있는데 alert가 딱! 알려주면서 원치 않는 작업을 피하게 도와주니까!)

alert에 대해서 더 잘 배워야 겠다는 생각이 들고,

또 이렇게 리뷰를 해보니까 내가 만들었던 앱에서는 alert가 또 너무 남발되어 있다는 생각도 든다.

 

 

적재적소에 딱! alert가 작동하게 해서 내 앱이 사용자들에게 원치않는 작업을 막을 수 있게 해주지만

또 너무 많은 alert로 피로감은 들지 않게 해야겠다는 생각이 들었다~

 

 

또 공부해봐야지!!! 

혹시 정정해야할 내용 있으시면 언제든 알려주세요 !!! ><

 

📝 Reference ♥️

 

https://sarunw.com/posts/how-to-show-multiple-alerts-on-the-same-view-in-swiftui/

 

How to show multiple alerts on the same view in SwiftUI | Sarunw

If you have ever worked with an app with multiple alerts, please beware that the system can present only the latest or outermost one. Let's see how we can mitigate this.

sarunw.com

 

https://sarunw.com/posts/how-to-present-alert-in-swiftui-ios15/

 

How to present an alert in SwiftUI in iOS 15 | Sarunw

Learn the difference between all-new alert APIs in iOS 15.

sarunw.com

https://ko.wikipedia.org/wiki/트리_구조

 

트리 구조 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. -->

ko.wikipedia.org