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
Related
I have code like this in below:
struct ContentView: View {
#State private var show: Bool = false
var body: some View {
ZStack {
Button("show") { show.toggle() }.foregroundColor(.black)
//.zIndex(1)
if (show) {
Circle()
.fill(.blue)
.frame(width: 100, height: 100)
.transition(AnyTransition.asymmetric(insertion: .offset(x: 0, y: 300), removal: .offset(x: 0, y: 300)))
.onTapGesture { show.toggle() }
//.zIndex(2)
}
}
.frame(width: 300, height: 300)
.padding()
.animation(.linear(duration: 1.5), value: show)
}
}
The issue with this code is that in insertion view stays in correct zIndex(in Top layer) but in removal goes to wrong zIndex(in Bottom layer), I can correct this issue with using direct zIndex but the goal of this question is to find a way without using zIndex modifier, I thing it is possible but not sure how, maybe it has something to do with transition.
This behavior seems like a bug.
I think this is a side effect of the fact that as soon as you remove the Circle() View, it is immediately gone, and the animation happens after the fact. So upon removal, there is instantly just one item in the ZStack and it is on top.
A workaround is to not completely remove a view from the ZStack. This can be accomplished by wrapping the if show { } with an HStack, VStack, or ZStack:
struct ContentView: View {
#State private var show: Bool = false
var body: some View {
ZStack {
Button("show") { show.toggle() }.foregroundColor(.black)
//.zIndex(1)
HStack { // Note: VStack and ZStack also work
if (show) {
Circle()
.fill(.blue)
.frame(width: 100, height: 100)
.transition(AnyTransition.asymmetric(insertion: .offset(x: 0, y: 300), removal: .offset(x: 0, y: 300)))
.onTapGesture { show.toggle() }
//.zIndex(2)
}
}
}
.frame(width: 300, height: 300)
.padding()
.animation(.linear(duration: 1.5), value: show)
}
}
In this case, while the Circle() View is immediately removed, the HStack to which it belongs remains, and it remains in the same position in the ZStack thus fixing the animation.
That said, I'm not sure why adding explicit .zIndex() modifiers helps.
Note: Wrapping if show() { } in a Group does not help because Group is not a View.
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})
)
}
}
}
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)
}
}
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?
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()
}
}