I am trying to display a ForEach
of items, that could transition into like a fake sheet view, but I don't know what I am doing wrong, because the ForEach
just displays every item on top of each other.
Here is a picture how it should look (which it does, if I remove the .matchedGeometryEffect
):
But this is how it looks, when I want to use .matchedGeometryEffect
:
The effect does work, but basically only for this item on top, because it's the only one clickable.
And this is my code:
import SwiftUI
struct Item: Identifiable {
let id = UUID().uuidString
let title: String
let subtitle: String
let image: ImageResource
}
struct ContentView: View {
@Namespace var sheet
private var rectangleId = "Rectangle"
private var titleId = "Title"
private var subtitleId = "Subtitle"
@State var isExpanded: Bool = false
@State private var selected: Item?
let items: [Item] = [
.init(title: "Test 1", subtitle: "testing stuff 1", image: .test),
.init(title: "Test 2", subtitle: "testing stuff 2", image: .test1),
.init(title: "Test 3", subtitle: "testing stuff 3", image: .test2),
.init(title: "Test 4", subtitle: "testing stuff 4", image: .test3),
.init(title: "Test 5", subtitle: "testing stuff 5", image: .test4),
]
var body: some View {
NavigationStack {
ZStack {
if isExpanded {
sheetView(item: selected!)
} else {
ScrollView {
ForEach(items, id: \.id) { item in
normalView(item: item)
}
}
.padding()
}
}
}
}
@ViewBuilder
func normalView(item: Item) -> some View {
Image(item.image)
.resizable()
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 12))
.matchedGeometryEffect(id: rectangleId, in: sheet)
.onTapGesture {
withAnimation {
self.selected = item
isExpanded.toggle()
}
}
.overlay(alignment: .bottomLeading) {
VStack(alignment: .leading) {
Text(item.title)
.font(.title)
.matchedGeometryEffect(id: titleId, in: sheet)
Text(item.subtitle)
.matchedGeometryEffect(id: subtitleId, in: sheet)
}
.foregroundStyle(.white)
.padding()
}
}
@ViewBuilder
func sheetView(item: Item) -> some View {
ScrollView {
Image(item.image)
.resizable()
.scaledToFit()
.matchedGeometryEffect(id: rectangleId, in: sheet)
.overlay(alignment: .bottomLeading) {
VStack(alignment: .leading) {
Text(item.title)
.font(.title)
.matchedGeometryEffect(id: titleId, in: sheet)
Text(item.subtitle)
.matchedGeometryEffect(id: subtitleId, in: sheet)
}
.foregroundStyle(.white)
.padding()
}
ForEach(0..<50) { item in
Text("New test text lol \(item)")
}
}
.ignoresSafeArea(edges: .top)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Image(systemName: "plus.circle.fill")
}
}
}
}
}
#Preview {
ContentView()
}
This does not work because you are always showing the same matchedGeometryEffect
id, something like this should fix your problem:
import SwiftUI
struct Item: Identifiable {
let id = UUID().uuidString
let title: String
let subtitle: String
let image: ImageResource
}
struct ContentView: View {
@Namespace var sheet
private var rectangleId = "Rectangle"
private var titleId = "Title"
private var subtitleId = "Subtitle"
@State var isExpanded: Bool = false
@State private var selected: Item?
let items: [Item] = [
.init(title: "Test 1", subtitle: "testing stuff 1", image: .test),
.init(title: "Test 2", subtitle: "testing stuff 2", image: .test1),
.init(title: "Test 3", subtitle: "testing stuff 3", image: .test2),
.init(title: "Test 4", subtitle: "testing stuff 4", image: .test3),
.init(title: "Test 5", subtitle: "testing stuff 5", image: .test4),
]
var body: some View {
NavigationStack {
ZStack {
if isExpanded {
sheetView(item: selected!)
} else {
ScrollView {
ForEach(items, id: \.id) { item in
normalView(item: item)
}
}
.padding()
}
}
}
}
@ViewBuilder
func normalView(item: Item) -> some View {
Image(item.image)
.resizable()
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 12))
// Differentiate items
.matchedGeometryEffect(id: item.id + "background", in: sheet)
.onTapGesture {
withAnimation {
self.selected = item
isExpanded.toggle()
}
}
.overlay(alignment: .bottomLeading) {
VStack(alignment: .leading) {
Text(item.title)
.font(.title)
.matchedGeometryEffect(id: item.id + "title", in: sheet)
Text(item.subtitle)
.matchedGeometryEffect(id: item.id + "subtitle", in: sheet)
}
.foregroundStyle(.white)
.padding()
}
}
@ViewBuilder
func sheetView(item: Item) -> some View {
ScrollView {
Image(item.image)
.resizable()
.scaledToFit()
.matchedGeometryEffect(id: item.id + "background", in: sheet)
.overlay(alignment: .bottomLeading) {
VStack(alignment: .leading) {
Text(item.title)
.font(.title)
.matchedGeometryEffect(id: item.id + "title", in: sheet)
Text(item.subtitle)
.matchedGeometryEffect(id: item.id + "subtitle", in: sheet)
}
.foregroundStyle(.white)
.padding()
}
ForEach(0..<50) { item in
Text("New test text lol \(item)")
}
}
.ignoresSafeArea(edges: .top)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
withAnimation {
isExpanded.toggle()
}
} label: {
Image(systemName: "plus.circle.fill")
}
}
}
}
}
#Preview {
ContentView()
}