🪑

ネイティブなアニメーションのためのmatchedGeometryEffect

2022/09/05に公開

matchedGeometryEffectあり/なし

matchedGeometryEffectなし

altテキスト

matchedGeometryEffectあり

altテキスト

matchedGeometryEffectの効果

matchedGeometryEffect無しだと突然赤のボックスが下に遷移する形になっていますが、matchedGeometryEffectを使うとViewが実際に移動しているようなアニメーションが付与されます。

これは同じViewグループ内で別Viewであっても同一であることをmatchedGeometryEffectで示しているからです。

同じViewグループであることを示すのには@Namespaceを使用します。

コード

struct Person: Identifiable {
  let id = UUID()
  let name: String
}

struct PersonCell: View {
  let person: Person

  var body: some View {
    RoundedRectangle(cornerRadius: 17)
      .foregroundStyle(.red)
      .overlay {
        Text(person.name)
      }
  }
}

struct ContentView: View {
  let people: [Person] = [
    .init(name: "king"),
    .init(name: "squid"),
    .init(name: "mesh"),
    .init(name: "sunrise"),
    .init(name: "month"),
    .init(name: "tanter"),
  ]

  @Namespace var namespace

  @State var unselectedPersonIDs: [UUID] = []
  @State var selectedPersonIDs: [UUID] = []

  var unselectedPeople: [Person] {
    unselectedPersonIDs.map { id in
      people.first { $0.id == id }!
    }
  }

  var selectedPeople: [Person] {
    selectedPersonIDs.map { id in
      people.first { $0.id == id }!
    }
  }

  var body: some View {
    VStack {
      ScrollView {
        LazyVGrid(columns: .init(repeating: .init(), count: 3)) {
          ForEach(unselectedPeople) { person in
            PersonCell(person: person)
              .frame(width: 100, height: 100)
              .matchedGeometryEffect(id: person.id, in: namespace)
              .onTapGesture {
                withAnimation {
                  selectedPersonIDs.append(person.id)
                  unselectedPersonIDs.removeAll { $0 == person.id }
                }
              }
          }
        }
      }
      HStack {
        ForEach(selectedPeople) { person in
          PersonCell(person: person)
            .frame(width: 100, height: 100)
            .matchedGeometryEffect(id: person.id, in: namespace)
            .onTapGesture {
              withAnimation {
                unselectedPersonIDs.append(person.id)
                selectedPersonIDs.removeAll { $0 == person.id }
              }
            }
        }
      }
    }
    .onAppear {
      unselectedPersonIDs = people.map(\.id)
    }
  }
}

Tips

  • アニメーション中のViewにもタップ判定がある

    • 描画だけでなくタップも可能
  • 綺麗にならないこともある

    • Viewの境目?などが原因?

Discussion