1

In our app we observe the size class changes using @Environment(\.verticalSizeClass) and @Environment(\.horizontalSizeClass). This works fine and we achieve the expected layout when the device was rotated.

The issue is when the app navigates to the other screen. If I am on MainScreen in portrait mode then navigate to DetailScreen then rotate the device to landscape the app go back to MainScreen. It seems that the NavigationLink was destroyed when the app redraw the MainScreen due to the rotation in DetailScreen.

Below is simplified code similar to what we use:

struct MainScreen: View {
    @Environment(\.verticalSizeClass) var verticalSizeClass
    @Environment(\.horizontalSizeClass) var horizontalSizeClass

    var body: some View {
        NavigationView {
            if horizontalSizeClass == .regular && verticalSizeClass == .compact {
                VStack {
                    // other views..
                    NavigationLink(
                        destination: { DetailScreen() },
                        label: { Text("Go to Details Screen") }
                    // other views..
                }
            } else {
                HStack {
                    // other views..
                    NavigationLink(
                        destination: { DetailScreen() },
                        label: { Text("Go to Details Screen") }
                    // other views..
                }
            }
        }
        .navigationViewStyle(.stack)
    }
}

struct DetailScreen: View {
    @Environment(\.verticalSizeClass) var verticalSizeClass
    @Environment(\.horizontalSizeClass) var horizontalSizeClass

    var body: some View {
        Text("Details Screen")
        // Some views that also changes layout base on the verticalSizeClass and horizontalSizeClass
    }
}

Does anyone encounter this issue and able to resolve it? Our app support from iOS15.

SquareBox
  • 823
  • 1
  • 10
  • 22
  • Note that there are many question about this issue, but with an `ObservableObject` e.g. https://stackoverflow.com/q/72057565/5133585 With an `ObservableObject`, at least you have more control. With `Environment`s involved, I doubt there is a "pretty" solution. With just two navigation links and two branches, my answer is already quite unreadable... – Sweeper Aug 03 '23 at 03:29

1 Answers1

1

This is a rather ugly solution, but alas, that's probably why NavigationView is deprecated.

The idea is to track which branch of the if you are in. With just one NavigationLink in each branch, you can pass some states to the isActive parameter.

@State var branch1 = false
@State var branch2 = false

Then use some logic to say that if the first link is active, show the first link regardless of what the size class is, and the same for the second link. And also that if neither is active, show the appropriate link according to the size class.

var shouldShowVStack: Bool {
    horizontalSizeClass == .compact && verticalSizeClass == .regular
}
if (branch1 || shouldShowVStack) && !branch2 {
    VStack {
        NavigationLink("Go to Details Screen", destination: DetailScreen(), isActive: $branch1)
    }
} else if (branch2 || !shouldShowHStack) && !branch1 {
    HStack {
        NavigationLink("Go to Details Screen landscape", destination: DetailScreen(), isActive: $branch1)
    }
}

If you have multiple NavigationLinks in each branch, it's more convenient to make your own wrapper around NavigationLink like this:

struct MyNavigationLink<Destination: View>: View {
    
    let destination: () -> Destination
    let label: LocalizedStringKey
    @Binding var isInBranch: Bool
    
    internal init(_ label: LocalizedStringKey, isInBranch: Binding<Bool>, destination: @escaping () -> Destination) {
        self.label = label
        self._isInBranch = isInBranch
        self.destination = destination
    }
    
    var body: some View {
        NavigationLink(label) {
            destination()
                .onAppear {
                    isInBranch = true
                }
                .onDisappear {
                    isInBranch = false
                }
        }
        // or use onChange(of: isActive)
    }
}

Of course, if iOS 16 is available, you should use a NavigationStack. Add appropriate if #unavailables to handle this.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • We did similar approach with `ObservableObject` where we toggle a variable on `onAppear` and `onDisappear` then we just ignore the `@Environment` when that variable is `false`. This works fine but it feel hacky :( – SquareBox Aug 04 '23 at 00:03