[ios] `NavigationView`의`navigationBarItems` 안에`NavigationLink`를 배치 한 후 뒤로 탐색 할 때 SwiftUI 앱이 충돌하는 이유는 무엇입니까?

최소 재현 가능한 예 (Xcode 11.2 베타, Xcode 11.1에서 작동) :

struct Parent: View {
    var body: some View {
        NavigationView {
            Text("Hello World")
                .navigationBarItems(
                    trailing: NavigationLink(destination: Child(), label: { Text("Next") })
                )
        }
    }
}

struct Child: View {
    @Environment(\.presentationMode) var presentation
    var body: some View {
        Text("Hello, World!")
            .navigationBarItems(
                leading: Button(
                    action: {
                        self.presentation.wrappedValue.dismiss()
                    },
                    label: { Text("Back") }
                )
            )
    }
}

struct ContentView: View {
    var body: some View {
        Parent()
    }
}

이 문제는 루트보기가 인 SwiftUI보기 안에 중첩 된 수정 자 NavigationLink내부 에 배치하는 것으로 보입니다 . 충돌 보고서는 앞으로 탐색 한 다음 다시 탐색 할 때 존재하지 않는 뷰 컨트롤러에 팝업하려고한다는 것을 나타냅니다 .navigationBarItemsNavigationViewChildParent

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Tried to pop to a view controller that doesn't exist.'
*** First throw call stack:

대신 NavigationLink아래와 같이 뷰 본문에 배치하면 정상적으로 작동합니다.

struct Parent: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: Child(), label: { Text("Next") })
        }
    }
}

이것이 SwiftUI 버그입니까 아니면 예상되는 동작입니까?

편집 : FB7423964Apple의 외부 담당자가 무게를 측정 해야하는 경우를 대비 하여 ID와 함께 피드백 지원에서 Apple과 관련된 문제를 열었습니다 . 🙂

편집 : 피드백 도우미의 열린 티켓은 10 가지 이상의 유사한보고 된 문제가 있음을 나타냅니다. 그들은로 해상도를 업데이트했습니다 Resolution: Potential fix identified - For a future OS update. 손가락이 교차하여 수정이 곧 착륙했습니다.

편집 : 이것은 iOS 13.3에서 수정되었습니다!



답변

이것은 나에게 큰 고통이었다! 대부분의 앱이 완성 될 때까지 그대로두고 충돌을 처리 할 수있는 공간이있었습니다.

SwifUI에는 멋진 기능이 있지만 디버깅이 어려울 수 있다는 데 모두 동의 할 수 있습니다.

제 생각에는 이것이 버그입니다. 여기 내 근거가 있습니다 :

  • presentationMode dismiss 호출을 약 0.5 초의 비동기 지연으로 랩핑하면 프로그램이 더 이상 충돌하지 않습니다.

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.presentationMode.wrappedValue.dismiss()
    } 
  • 이것은 버그가 SwiftUI가 다른 모든 UIKit 코드와 인터페이스하여 다양한 뷰를 관리하는 방식에 깊은 예상치 못한 동작이라는 것을 나타냅니다. 실제 코드에 따라보기에 약간의 복잡성이 있으면 충돌이 실제로 발생하지 않을 수 있습니다. 예를 들어,보기에서 목록이있는보기로 닫고 해당 목록이 비어 있으면 비동기 지연없이 충돌이 발생합니다. 반면, 해당 목록보기에 하나의 항목 만 있으면 루프 반복이 상위보기를 생성하도록 강제 실행하면 충돌이 발생하지 않습니다.

지연 호출을 래핑하는 솔루션이 얼마나 강력한 지 잘 모르겠습니다. 나는 그것을 훨씬 더 테스트해야합니다. 이것에 대한 아이디어가 있으면 알려주십시오! 당신에게서 배우게되어 매우 기쁩니다!


답변

이것은 또한 꽤 오랫동안 나를 좌절시켰다. 지난 몇 개월 동안 Xcode 버전, 시뮬레이터 버전 및 실제 장치 유형 및 / 또는 버전에 따라 무작위로 작동하지 않고 다시 작동하지 않는 것으로 바뀌 었습니다. 그러나 최근에 그것은 나에게 지속적으로 실패하고 있었으므로 어제 나는 그것에 깊이 빠져 들었습니다. 현재 Xcode 버전 11.2.1 (11B500)을 사용하고 있습니다.

Nav Bar와 버튼이 추가 된 방식을 중심으로 문제가 발생하는 것처럼 보입니다. 따라서 버튼 자체에 NavigationLink ()를 사용하는 대신 숨겨진 NavigationLink를 활성화하는 @State var를 설정하는 작업과 함께 표준 Button ()을 사용해 보았습니다. 다음은 Robert의 부모보기를 대체합니다.

struct Parent: View {
    @State private var showingChildView = false
    var body: some View {
        NavigationView {
            VStack {
                Text("Hello World")
                NavigationLink(destination: Child(),
                               isActive: self.$showingChildView)
                { EmptyView() }
                    .frame(width: 0, height: 0)
                    .disabled(true)
                    .hidden()
             }
             .navigationBarItems(
                 trailing: Button(action:{ self.showingChildView = true }) { Text("Next") }
             )
        }
    }
}

나에게 이것은 모든 시뮬레이터와 모든 실제 장치에서 일관되게 작동합니다.

내 도우미보기는 다음과 같습니다.

struct HiddenNavigationLink<Destination : View>: View {

    public var destination:  Destination
    public var isActive: Binding<Bool>

    var body: some View {

        NavigationLink(destination: self.destination, isActive: self.isActive)
        { EmptyView() }
            .frame(width: 0, height: 0)
            .disabled(true)
            .hidden()
    }
}

struct ActivateButton<Label> : View where Label : View {

    public var activates: Binding<Bool>
    public var label: Label

    public init(activates: Binding<Bool>, @ViewBuilder label: () -> Label) {
        self.activates = activates
        self.label = label()
    }

    var body: some View {
        Button(action: { self.activates.wrappedValue = true }, label: { self.label } )
    }
}

사용법의 예는 다음과 같습니다.

struct ContentView: View {
    @State private var showingAddView: Bool = false
    var body: some View {
        NavigationView {
            VStack {
                Text("Hello, World!")
                HiddenNavigationLink(destination: AddView(), isActive: self.$showingAddView)
            }
            .navigationBarItems(trailing:
                HStack {
                    ActivateButton(activates: self.$showingAddView) { Image(uiImage: UIImage(systemName: "plus")!) }
                    EditButton()
            } )
        }
    }
}


답변

이것은 주요 버그이며이 문제를 해결하는 올바른 방법을 볼 수 없습니다. iOS 13 / 13.1에서 잘 작동했지만 13.2가 충돌합니다.

실제로 훨씬 간단한 방법으로 복제 할 수 있습니다 (이 코드는 문자 그대로 필요한 전부입니다).

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("Hello, World!").navigationBarTitle("To Do App")
                .navigationBarItems(leading: NavigationLink(destination: Text("Hi")) {
                    Text("Nav")
                    }
            )
        }
    }
}

Apple이 SwiftUI 앱 (내 것을 포함하여)을 확실히 깨뜨릴 수 있기를 바랍니다.


답변

위의 척 H의 답변을 기반으로 한 해결 방법으로 NavigationLink를 숨겨진 요소로 캡슐화했습니다.

struct HiddenNavigationLink<Content: View>: View {
var destination: Content
@Binding var activateLink: Bool

var body: some View {
    NavigationLink(destination: destination, isActive: self.$activateLink) {
        EmptyView()
    }
    .frame(width: 0, height: 0)
    .disabled(true)
    .hidden()
}
}

그런 다음 NavigationView 내에서 사용하고 (중요한) 탐색 모음의 버튼에서 트리거 할 수 있습니다.

VStack {
    HiddenNavigationList(destination: SearchView(), activateLink: self.$searchActivated)
    ...
}
.navigationBarItems(trailing:
    Button("Search") { self.searchActivated = true }
)

“// HACK”주석으로 감싸서 Apple에서이 문제를 해결하면 교체 할 수 있습니다.


답변

귀하가 제공 한 정보와 특히 @Robert가 NavigationView가 배치 된 위치에 대해 작성한 의견을 바탕으로 적어도 특정 시나리오에서 문제를 해결할 수있는 방법을 찾았습니다.

내 경우에는 다음과 같이 NavigationView로 묶인 TabView가 있습니다.

struct ContentViewThatCrashes: View {
@State private var selection = 0

var body: some View {
    NavigationView{
        TabView(selection: $selection){
            NavigationLink(destination: NewView()){
                Text("First View")
                    .font(.title)
            }
            .tabItem {
                VStack {
                    Image("first")
                    Text("First")
                }
            }
            .tag(0)
            NavigationLink(destination: NewView()){
                Text("Second View")
                    .font(.title)
            }
            .tabItem {
                VStack {
                    Image("second")
                    Text("Second")
                }
            }
            .tag(1)
        }
    }
  }
}

모든 사람이 iOS 13.2에서보고하고 iOS 13.1에서 작동함에 따라이 코드가 충돌합니다. 몇 가지 연구를 한 후에 나는이 상황에 대한 해결 방법을 찾았습니다.

기본적으로 NavigationView를 다음과 같이 각 탭에서 개별적으로 각 화면으로 이동합니다.

struct ContentViewThatWorks: View {
@State private var selection = 0

var body: some View {
    TabView(selection: $selection){
        NavigationView{
            NavigationLink(destination: NewView()){
                Text("First View")
                    .font(.title)
            }
        }
        .tabItem {
            VStack {
                Image("first")
                Text("First")
            }
        }
        .tag(0)
        NavigationView{
            NavigationLink(destination: NewView()){
                Text("Second View")
                    .font(.title)
            }
        }
        .tabItem {
            VStack {
                Image("second")
                Text("Second")
            }
        }
        .tag(1)
    }
  }
}

어떻게 든 SwiftUI의 단순성에 반대하지만 iOS 13.2에서 작동합니다.


답변

Xcode 11.2.1 스위프트 5

알았다! 이것을 알아내는 데 며칠이 걸렸습니다 …

SwiftUI를 사용할 때 내 목록의 맨 아래가 화면을 넘어 확장 된 경우에만 목록 항목을 “이동”하려고하면 충돌이 발생합니다. 내가 찾은 것은 List () 아래에 너무 많은 “stuff”가 있으면 이동 중에 충돌한다는 것입니다. 예를 들어 내 List () 아래에 Text (), Spacer (), Button (), Spacer () Button ()이 있습니다. 그 객체 중 하나를 주석 처리하면 갑자기 충돌을 다시 만들 수 없었습니다. 제한 사항이 무엇인지 확실하지 않지만이 충돌이 발생하면 목록 아래의 객체를 제거하여 도움이되는지 확인하십시오.


답변

충돌이 보이지 않지만 코드에 몇 가지 문제가 있습니다.

선행 항목을 설정하면 실제로 탐색 전환의 기본 동작을 종료합니다. (앞면에서 스 와이프하여 작동하는지 확인).

버튼이 없어도됩니다. 그대로두고 무료 뒤로 버튼이 있습니다.

그리고 HIG 에 따르면 , 뒤로 버튼 제목은 그것이 무엇인지가 아니라 어디로 가야하는지 보여야 합니다! 따라서 첫 번째 페이지의 제목을 설정하여 팝업 페이지로 돌아 가기 버튼을 표시하십시오.

struct Parent: View {
    var body: some View {
        NavigationView {
            Text("Hello World")
                .navigationBarItems(
                    trailing: NavigationLink(destination: Child(), label: { Text("Next") })
                )
                .navigationBarTitle("First Page",displayMode: .inline)
        }
    }
}

struct Child: View {
    @Environment(\.presentationMode) var presentation
    var body: some View {
        Text("Hello, World!")
    }
}

struct ContentView: View {
    var body: some View {
        Parent()
    }
}