Matched Geometry Effect in SwiftUI is not working - swift

I'm wondering why my matchedGeometryEffect isn't working as expected. Did I do something wrong? Because this guy (at 3:15) did the same and it looks much smoother. Im running the code on a M1 MacBook Air that means the speed is probably not the problem.
Here is my Code:
struct ContentView: View {
#Namespace var namespace
#State var bool = true
var body: some View {
HStack{
if bool {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(Color.red)
.padding()
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)){
bool.toggle()
}
}
.matchedGeometryEffect(id: "id", in: namespace)
} else {
Rectangle()
.frame(width: 200, height: 200)
.foregroundColor(Color.red)
.matchedGeometryEffect(id: "id", in: namespace)
}
}
}
}
Thanks for your help, Boothosh

In SwiftUI, the modifier order matters. This is the same with matchedGeometryEffect(id:in:properties:anchor:isSource:).
If you want the rectangles to expand in the center, you need the geometries to align in the center. To do this, we will have Rectangles of the same size. This means that both Rectangles will have the matchedGeometryEffect modifier attached, and then styled after.
Code:
HStack {
if bool {
Rectangle()
.matchedGeometryEffect(id: "id", in: namespace) // <- Here
.frame(width: 100, height: 100)
.foregroundColor(Color.red)
.padding()
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
bool.toggle()
}
}
} else {
Rectangle()
.matchedGeometryEffect(id: "id", in: namespace) // <- Here
.frame(width: 200, height: 200)
.foregroundColor(Color.red)
}
}

Related

SwiftUI scaled background intercepting clicks

I'm encountering an issue with SwiftUI on macOS (12.x) where a scaled background is interfering with mouse clicks. The following is a minimal example:
struct ContentView: View {
var body: some View {
VStack {
Button("Test") {
print("Tested")
}
Image(systemName: "waveform.circle")
.resizable()
.frame(width: 200, height: 200)
.background(
Image(systemName: "waveform.circle")
.resizable()
.foregroundColor(.red)
.scaleEffect(2.0)
.blur(radius: 20)
.clipped()
)
}
.frame(width: 500, height: 500)
}
}
Note that pressing the "Test" button doesn't actually work -- no message to the console is printed.
I've found a workaround using NSHostingView, but it's pretty ugly -- I'd love to know if there's a pure-SwiftUI solution to 'clip' the background View so that not only is it's appearance cut off, but also it's ability to intercept clicks.
Here's a workaround (note that turning off the scale effect also solves the issue).
struct ContentViewWorkAround: View {
#State private var scaleEffectOn = true
#State private var workAround = false
#ViewBuilder private var imageBackground: some View {
switch workAround {
case false:
Image(systemName: "waveform.circle")
.resizable()
.foregroundColor(.red)
.scaleEffect(scaleEffectOn ? 2.0 : 1.0)
.blur(radius: 20)
.clipped()
.frame(width: 200, height: 200) // ineffective at preventing clicks
case true:
NSHostingViewRepresented {
Image(systemName: "waveform.circle")
.resizable()
.foregroundColor(.green)
.scaleEffect(2.0)
.blur(radius: 20)
.clipped()
.frame(width: 200, height: 200)
}
}
}
var body: some View {
VStack {
Button("Test") {
print("Tested")
}
Image(systemName: "waveform.circle")
.resizable()
.frame(width: 200, height: 200)
.background(
imageBackground
)
Toggle("Scale effect", isOn: $scaleEffectOn)
Toggle("Workaround", isOn: $workAround)
}
.frame(width: 500, height: 500)
}
}
struct NSHostingViewRepresented<V>: NSViewRepresentable where V: View {
var content: () -> V
func makeNSView(context: Context) -> NSHostingView<V> {
NSHostingView(rootView: content())
}
func updateNSView(_ nsView: NSHostingView<V>, context: Context) { }
}
Have you tried?
.allowsHitTesting(false)
Deactivates the clicks and allows whatever look you want.
struct ContentView: View {
var body: some View {
VStack {
Button("Test") {
print("Tested")
}
Image(systemName: "waveform.circle")
.resizable()
.frame(width: 200, height: 200)
.background(
Image(systemName: "waveform.circle")
.resizable()
.foregroundColor(.red)
.scaleEffect(2.0)
.blur(radius: 20)
.clipped()
.allowsHitTesting(false)
)
}
.frame(width: 500, height: 500)
}
}

SwiftUI - How to center a component in a horizontal ScrollView when tapped?

I have a horizontal ScrollView, and within it an HStack. It contains multiple Subviews, rendered by a ForEach. I want to make it so that when these Subviews are tapped, they become centered vertically in the view. For example, I have:
ScrollView(.horizontal) {
HStack(alignment: .center) {
Circle() // for demonstration purposes, let's say the subviews are circles
.frame(width: 50, height: 50)
Circle()
.frame(width: 50, height: 50)
Circle()
.frame(width: 50, height: 50)
}
.frame(width: UIScreen.main.bounds.size.width, alignment: .center)
}
I tried this code:
ScrollViewReader { scrollProxy in
ScrollView(.horizontal) {
HStack(alignment: .center) {
Circle()
.frame(width: 50, height: 50)
.id("someID3")
.frame(width: 50, height: 50)
.onTapGesture {
scrollProxy.scrollTo(item.id, anchor: .center)
}
Circle()
.frame(width: 50, height: 50)
.id("someID3")
.frame(width: 50, height: 50)
.onTapGesture {
scrollProxy.scrollTo(item.id, anchor: .center)
}
...
}
}
But it seemingly had no effect. Does anyone know how I can properly do this?
You can definitely do this with ScrollView and ScrollViewReader. However, I see a couple of things that could cause problems in your code sample:
You use the same id "someID3" twice.
I can't see where your item.id comes from, so I can't tell if it actually contains the same id ("someID3").
I don't know why you have two frames with the same bounds on the same view area. It shouldn't be a problem, but it's always best to keep things simple.
Here's a working example:
import SwiftUI
#main
struct MentalHealthLoggerApp: App {
var body: some Scene {
WindowGroup {
ScrollViewReader { scrollProxy in
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 10) {
Color.clear
.frame(width: (UIScreen.main.bounds.size.width - 70) / 2.0)
ForEach(Array(0..<10), id: \.self) { id in
ZStack(alignment: .center) {
Circle()
.foregroundColor(.primary.opacity(Double(id)/10.0))
Text("\(id)")
}
.frame(width: 50, height: 50)
.onTapGesture {
withAnimation {
scrollProxy.scrollTo(id, anchor: .center)
}
}
.id(id)
}
Color.clear
.frame(width: (UIScreen.main.bounds.size.width - 70) / 2.0)
}
}
}
}
}
}
Here you can see it in action:
[EDIT: You might have to click on it if the GIF won't play automatically.]
Note that I added some empty space to both ends of the ScrollView, so it's actually possible to center the first and last elements as ScrollViewProxy will never scroll beyond limits.
Created a custom ScrollingHStack and using geometry reader and a bit calculation, here is what we have:
struct ContentView: View {
var body: some View {
ScrollingHStack(space: 10, height: 50)
}
}
struct ScrollingHStack: View {
var space: CGFloat
var height: CGFloat
var colors: [Color] = [.blue, .green, .yellow]
#State var dragOffset = CGSize.zero
var body: some View {
GeometryReader { geometry in
HStack(spacing: space) {
ForEach(0..<15, id: \.self) { index in
Circle()
.fill(colors[index % 3])
.frame(width: height, height: height)
.overlay(Text("\(Int(dragOffset.width))"))
.onAppear {
dragOffset.width = geometry.size.width / 2 - ((height + space) / 2)
}
.onTapGesture {
let totalItems = height * CGFloat(index)
let totalspace = space * CGFloat(index)
withAnimation {
dragOffset.width = (geometry.size.width / 2) - (totalItems + totalspace) - ((height + space) / 2)
}
}
}
}
.offset(x: dragOffset.width)
.gesture(DragGesture()
.onChanged({ dragOffset = $0.translation})
)
}
}
}

How can I remove/control fade effect from matchedGeometryEffect?

I have a test code to show the issue, when I am using matchedGeometryEffect, matchedGeometryEffect adds an unwished effect of fade to rendered result, so I like to remove this fading effect or even control it. It could be a good thing when I change the Color of view from one color to other, but in my case it is not good, because the Color is black in all the time.
struct ContentView: View {
#Namespace var animationNamespaceon
#State private var start: Bool = Bool()
var body: some View {
VStack {
Spacer()
circle
Spacer()
Button("update") { start.toggle() }
}
.animation(Animation.linear(duration: 3), value: start)
}
#ViewBuilder var circle: some View {
if start {
Circle()
.foregroundColor(Color.black)
.matchedGeometryEffect(id: "Circle", in: animationNamespaceon)
.frame(width: 300, height: 300)
}
else {
Circle()
.foregroundColor(Color.black)
.matchedGeometryEffect(id: "Circle", in: animationNamespaceon)
.frame(width: 50, height: 50)
}
}
}
This is an effect of default .transition(.opacity) which is applied when view is removed from (inserted to) view hierarchy.
I assume you need linear scale transition instead, like
if start {
Circle()
.foregroundColor(Color.black)
.matchedGeometryEffect(id: "Circle", in: animationNamespaceon)
.transition(.scale(scale: 1))
.frame(width: 300, height: 300)
}
else {
Circle()
.foregroundColor(Color.black)
.matchedGeometryEffect(id: "Circle", in: animationNamespaceon)
.transition(.scale(scale: 1))
.frame(width: 50, height: 50)
}
Tested with Xcode 13 / iOS 15

SwiftUI: Frame modifier in ZStack affecting other views

struct CircleTestView: View {
let diameter: CGFloat = 433
var body: some View {
ZStack {
Color(.yellow)
.ignoresSafeArea()
VStack {
Circle()
.fill(Color(.green))
.frame(width: diameter, height: diameter)
.padding(.top, -(diameter / 2))
Spacer()
}
VStack {
Spacer()
Button {} label: {
Color(.red)
.frame(height: 55)
.padding([.leading, .trailing], 16)
}
}
}
}
}
The code above creates the first image, yet for some reason if I remove the line the sets the frame for the Circle (ie. .frame(width: diameter, height: diameter)) I get the second image.
2.
I want the circle how it is in the first screen, and the button how it is in the second screen, but can't seem to achieve this. Somehow setting the frame of the Circle is affecting the other views, even though they're in a ZStack. Is this a bug with ZStacks, or am I misunderstanding how they work?
Lets call this one approach a:
struct CircleTestView: View {
let diameter: CGFloat = 433
var body: some View {
ZStack {
Color(.yellow)
.ignoresSafeArea()
VStack {
Circle()
.fill(Color(.green))
.frame(width: diameter, height: diameter)
.padding(.top, -(diameter / 2))
Spacer()
}
VStack {
Spacer()
Button {} label: {
Color(.red)
.frame(height: 55)
}
}
.padding(.horizontal, 16)
}
}
}
Lets call this one approach b:
struct CircleTestView: View {
let diameter: CGFloat = 433
var body: some View {
ZStack {
Color(.yellow)
.ignoresSafeArea()
VStack {
Circle()
.fill(Color(.green))
.offset(x: 0, y: -(diameter / 1.00))
// increment/decrement the offset by .01 example:
// .offset(x: 0, y: -(diameter / 1.06))
Spacer()
}
VStack {
Spacer()
Button {} label: {
Color(.red)
.frame(height: 55)
.padding([.leading, .trailing], 16)
}
}
}
}
}
A combination of the two approaches would land you at approach c.
Do any of these achieve what you are looking for?

How to fix problem of flicking views using .matchedgeometryeffect with animation

Anybody knows how to fix problem of flicking views using .matchedgeometryeffect with animation
Flicking views gif
In my mind I need to disable opacity transition but I dont know how
struct SwiftUIView: View {
#State private var isChanged = false
#Namespace var space
var body: some View {
ZStack {
if !isChanged {
Rectangle()
.matchedGeometryEffect(id: "1", in: space)
.frame(width: 100, height: 100)
} else {
Rectangle()
.matchedGeometryEffect(id: "1", in: space)
.frame(width: 300, height: 300)
}
}
.onTapGesture {
withAnimation() {
isChanged.toggle()
}
}
}
}
You can modify the transition:
func transition(_ t: AnyTransition) -> some View
like this :
ZStack {
if !isChanged {
Rectangle()
.matchedGeometryEffect(id: "1", in: space)
.transition(.scale) // <--- Define the transition to Scale
.frame(width: 100, height: 100)
} else {
Rectangle()
.matchedGeometryEffect(id: "1", in: space)
.transition(.scale) // <--- Define the transition to Scale
.frame(width: 300, height: 300)
}
}
.onTapGesture {
withAnimation() {
isChanged.toggle()
}
}
If you only need to scale your view you can also apply:
ZStack {
Rectangle()
.matchedGeometryEffect(id: "1", in: space)
.frame(width: 100, height: 100)
.scaleEffect(isChanged ? 3 : 1)
}
.onTapGesture {
withAnimation() {
isChanged.toggle()
}
}