Home > Software engineering >  Odd animation with .matchedGeometryEffect and NavigationView
Odd animation with .matchedGeometryEffect and NavigationView

Time:08-09

I'm working on creating a view which uses matchedGeometryEffect, embedded within a NavigationView, and when presenting the second view another NavigationView.

The animation "works" and it matches correctly, however when it toggles from view to view it happens to swing as if unwinding from the navigation stack.

However, if I comment out the NavigationView on the secondary view the matched geometry works correctly.

I am working on iOS 14.0 and above.

Sample code

Model / Mock Data

struct Model: Identifiable {
  let id = UUID().uuidString
  let icon: String
  let title: String
  let account: String
  let colour: Color
}

let mockItems: [Model] = [
  Model(title: "Test title 1", colour: .gray),
  Model(title: "Test title 2", colour: .blue),
  Model(title: "Test title 3", colour: .purple)
  ...
]

Card View

struct CardView: View {
  let item: Model
  var body: some View {
    VStack {
      Text(item.title)
        .font(.title3)
        .fontWeight(.heavy)
    }
    .padding()
    .frame(maxWidth: .infinity, minHeight: 100, alignment: .leading)
    .background(item.colour)
    .foregroundColor(.white)
    .clipShape(RoundedRectangle(cornerRadius: 12))
  }
}

Secondary / Detail View

struct DetailView: View {
  @Binding var isShowingDetail: Bool
  let item: Model
  let animation: Namespace.ID
  var body: some View {
    NavigationView {  // <--- comment out here and it works
      VStack {
        CardView(item: item)
          .matchedGeometryEffect(id: item.id, in: animation)
          .onTapGesture {
            withAnimation { isShowingDetail = false }
          }
          ScrollView(.vertical, showsIndicators: false) {
            Text("Lorem ipsum dolor...")
          }
      }
      .padding(.horizontal)
      .navigationBarTitleDisplayMode(.inline)
    }
    .navigationViewStyle(.stack)
  }
}

Primary View

struct ListView: View {
  @State private var selectedCard: Model?
  @State private var isShowingCard: Bool = false
  @Namespace var animation
  var body: some View {
    ZStack {
      NavigationView {
        ScrollView(.vertical, showsIndicators: false) {
          ForEach(mockItems) { item in
            CardView(item: item)
              .matchedGeometryEffect(id: item.id, in: animation)
              .onTapGesture {
                withAnimation {
                  selectedCard = item
                  isShowingCard = true
                }
              }
          }
        }
        .navigationTitle("Test title")
      }
      .padding(.horizontal)
      .navigationViewStyle(.stack)

      // show detail view
      if let selectedCard = selectedCard, isShowingCard {
        DetailView(
          isShowingDetail: $isShowingCard,
          item: selectedCard,
          animation: animation
        )
      }
    }
  }
}

Video examples

With NavigationView in DetailView

enter image description here

Without NavigationView in DetailView

Ignore the list view still visible

enter image description here

CodePudding user response:

You don't need second NavigationView (actually I don't see links at all, so necessity of the first one is also under question). Anyway we can just change the layout order and put everything into one NavigationView, like below.

Tested with Xcode 13.4 / iOS 15.5

demo

struct ListView: View {
  @State private var selectedCard: Model?
  @State private var isShowingCard: Bool = false
  @Namespace var animation
  var body: some View {
      NavigationView {
        ZStack {         // container !!
          if let selectedCard = selectedCard, isShowingCard {
            DetailView(
              isShowingDetail: $isShowingCard,
              item: selectedCard,
              animation: animation
            )
          } else {
            ScrollView(.vertical, showsIndicators: false) {
              ForEach(mockItems) { item in
                CardView(item: item)
                  .matchedGeometryEffect(id: item.id, in: animation)
                  .onTapGesture {
                      selectedCard = item
                      isShowingCard = true
                  }
              }
            }
            .navigationTitle("Test title")
          }
      }
      .padding(.horizontal)
      .navigationViewStyle(.stack)
      .animation(.default, value: isShowingCard)   // << animated here !!
    }
  }
}

struct DetailView: View {
  @Binding var isShowingDetail: Bool
  let item: Model
  let animation: Namespace.ID
  var body: some View {
      VStack {
        CardView(item: item)
          .matchedGeometryEffect(id: item.id, in: animation)
          .onTapGesture {
                isShowingDetail = false
          }
          ScrollView(.vertical, showsIndicators: false) {
            Text("Lorem ipsum dolor...")
          }
      }
      .padding(.horizontal)
      .navigationBarTitleDisplayMode(.inline)
      .navigationViewStyle(.stack)
  }
}

Test module is here

  • Related