Swift UI animation diagonal instead horizontal - swift

I'm trying to make a loading view with some animations, but instead of the RoundRectangle offset it self horizontaly as it's defined in the code, it is actually moving in diagonal.
Why is it animating in diagonal?
Here's my struct:
struct LoadingIndicator: View {
let textToDisplay:String
#State private var isLoading:Bool = false
var body: some View {
ZStack {
Text(textToDisplay)
.fontWeight(.bold)
.offset(x: 0, y: -25)
RoundedRectangle(cornerRadius: 3)
.stroke(Color.gray, lineWidth: 3)
.frame(width: 250, height: 3)
RoundedRectangle(cornerRadius: 3)
.stroke(Color.indigo, lineWidth: 3)
.frame(width: 30, height: 3)
.offset(x: (isLoading ? 110 : -110), y: 0)
.animation(.linear(duration: 1).repeatForever(), value: isLoading)
}.onAppear {
self.isLoading = true
}
}
}
This is what I got:
This is what I wanted to achieve:

After seing this question: SwiftUI: Broken explicit animations in NavigationView?
I solved my problem:
struct LoadingIndicator: View {
let textToDisplay:String
#State private var isLoading:Bool = false
var body: some View {
ZStack {
Text(textToDisplay)
.fontWeight(.bold)
.offset(x: 0, y: -25)
RoundedRectangle(cornerRadius: 3)
.stroke(Color.gray, lineWidth: 3)
.frame(width: 250, height: 3)
RoundedRectangle(cornerRadius: 3)
.stroke(Color.indigo, lineWidth: 3)
.frame(width: 30, height: 3)
.offset(x: (isLoading ? 110 : -110), y: 0)
.animation(.linear(duration: 1).repeatForever(), value: isLoading)
}.onAppear {
DispatchQueue.main.async {
self.isLoading = true
}
}
}
}

Related

SwiftUI Animation causes binding conditional view to flash on and off

I have an issue with this setup:
ZStack {
ViewOne
if something ? ViewTwo : nil
}
.animation()
The problem is that when the animation starts, ViewTwo flashes on and off. I'm thinking it has something to do with the view re-rendering or something? But I can't quite figure it out. I've tried moving the animation around, using it on each view separately, combining it all in one view, but it always flashes when it's based on a conditional. I'd like BOTH views to animate together.
Here is a piece of reproducible code snippet.
struct ContentView: View {
#State var isAnimating: Bool
var body: some View {
ZStack {
VStack {
ForEach((1...5).reversed(), id: \.self) {_ in
ZStack {
RoundedRectangle(cornerRadius: 5)
.foregroundColor(.blue)
.frame(width: 200, height: 50)
.rotationEffect(.degrees(isAnimating == true ? 5 : 0))
isAnimating
? ButtonImage()
: nil
}
.animation(
.easeInOut(duration: 0.3)
.repeatForever(autoreverses: true)
, value: isAnimating
)
}
Button(action: {
self.isAnimating.toggle()
}, label: {
Text("Animate")
})
}
}
.rotationEffect(.degrees(isAnimating == true ? 5 : 0))
}
}
struct ButtonImage: View {
private let buttonSize: CGSize = CGSize(width: 25, height: 25)
var body: some View {
Button(action: {
// to something
}) {
Image(systemName: "flame")
.resizable()
.renderingMode(.template)
.background(Color.red)
.foregroundColor(Color.yellow)
.frame(width: buttonSize.width, height: buttonSize.height)
}
.frame(width: buttonSize.width, height: buttonSize.height)
.offset(x: -buttonSize.width / 2, y: -buttonSize.height / 2)
}
Any ideas of how to resolve this? Showing a view based on a condition, while also animating it without it flashing?
Figured out a way! Not sure if it's the best approach, but it works.
Add the animation to both views separately, then add an opacity modifier to the second view. Here is the code I used.
struct ContentView: View {
#State var isAnimating: Bool
var body: some View {
ZStack {
VStack {
ForEach((1...5).reversed(), id: \.self) {_ in
ZStack {
RoundedRectangle(cornerRadius: 5)
.foregroundColor(.blue)
.frame(width: 200, height: 50)
.rotationEffect(.degrees(isAnimating == true ? 5 : 0))
.animation(
.easeInOut(duration: 0.3)
.repeatForever(autoreverses: true)
, value: isAnimating
)
ButtonImage(isAnimating: $isAnimating)
.opacity(isAnimating ? 1 : 0)
}
}
Button(action: {
self.isAnimating.toggle()
}, label: {
Text("Animate")
})
}
}
.rotationEffect(.degrees(isAnimating == true ? 5 : 0))
}
}
struct ButtonImage: View {
private let buttonSize: CGSize = CGSize(width: 25, height: 25)
#Binding var isAnimating: Bool
var body: some View {
Button(action: {
// to something
}) {
Image(systemName: "flame")
.resizable()
.renderingMode(.template)
.background(Color.red)
.foregroundColor(Color.yellow)
.frame(width: buttonSize.width, height: buttonSize.height)
}
.frame(width: buttonSize.width, height: buttonSize.height)
.offset(x: -buttonSize.width / 2, y: -buttonSize.height / 2)
.rotationEffect(.degrees(isAnimating == true ? 5 : 0))
.animation(
.easeInOut(duration: 0.3)
.repeatForever(autoreverses: true)
, value: isAnimating
)
}
}

How can I do a MatchedGeometryEffect animation?

Do you have any approach about how to scale ( with MatchedGeometryEffect ) my 'MealTabView' ( the cells that you can see in the picture ) so that it can fill an entire view ? :
Here's the source code for the 'cells' that you can see :
struct MealTabView:View{
var meal: Meal
#Binding var selectedBtn: [Int]
var body: some View{
ZStack{
Button(action: {
if selectedBtn.contains(self.meal.id){
selectedBtn.remove(at: selectedBtn.firstIndex(of: self.meal.id)!)
} else{
selectedBtn.append(self.meal.id)
}
}, label: {
ZStack(alignment:.center){
RoundedRectangle(cornerRadius: 20)
.foregroundColor(.white)
.frame(width: 150, height: 200)
.shadow(color: .black.opacity(0.1), radius: 5)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(selectedBtn.contains(self.meal.id) ? Color.black : Color.white, lineWidth: 4)
)
VStack(alignment:.center){
switch meal.id { // for the future images
default:
Image("meal")
.resizable()
.frame(width: 100, height: 100)
.padding(.top,30)
}
Spacer()
Text(meal.title)
.padding(.bottom,20)
.wotfard(size: 20) // custom modifier for a special font
.foregroundColor(.black)
}
}.frame(width: 150, height: 200)
})
}
}
}

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 do I make a card in ScrollView in SwiftUI "pop" to center

I have a horizontally scrolling ScrollView in SwiftUI. I need to somehow center the element that is shown more than a half. I can't figure out how to get ScrollView position in percent, and I don't know how to optimize using GeometryReader for all possible devices.
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
ForEach (sectionData) { item in
GeometryReader { geometry in
SectionView(section: item)
.rotation3DEffect(
Angle(degrees: Double(geometry.frame(in:.global).minX - 30) / -20),
axis: (x: 0.0, y: 10.0, z: 0.0))
}
.frame(width: 275, height: 275)
}
}
.padding(30)
.padding(.bottom, 30)
}
.offset(y: -30)
This could be an aproach
public struct SectionView: View {
var section: Int
public var body: some View {
ZStack {
Rectangle()
.frame(width: 275, height: 275, alignment: .center)
Text("\(section)")
.foregroundColor(.white)
}
}
}
public struct ExampleView: View {
#State private var isShowingNumber: Int = 0
public var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { value in
ZStack (alignment: .center) {
LazyHStack(alignment: .center, spacing:275) {
ForEach (1..<20) { item in
Rectangle()
.frame(width: 1, height: 1, alignment: .center)
.foregroundColor(.clear)
.onAppear() {
print(item)
withAnimation() {
value.scrollTo(item, anchor: .center)
}
}
}
}
HStack(spacing:10) {
ForEach (1..<20) { item in
GeometryReader { geometry in
ZStack {
SectionView(section: item)
.id(item)
.rotation3DEffect(
Angle(degrees: Double(geometry.frame(in:.global).minX - 30) / -20),
axis: (x: 0.0, y: 10.0, z: 0.0))
}
}
.frame(width: 275, height: 275)
}
}
.padding(30)
.padding(.bottom, 30)
}
}
}
.offset(y: -30)
}
}

SwiftUI: (Bool variable in ForEach) What is wrong with my variable in ForEach?

I want to make 2 bool variables for each string in array using ForEach but the bool variables don't work when I press the button, What can I do?
(with this code I wanna make a button, and when it is pressed, show more info with a transition, I want two variables for each one)
ForEach(0 ..< Array.count, id:\.self){ ArrayCount in
let Title = Array.count[ArrayCount]
let Description = UserDefaults.standard.string(forKey: Title+"Description") ?? ""
var ShowTip = false
var PressAnim = false
VStack(alignment: .center){
Button(action:{
withAnimation {
ShowTip.toggle()
PressAnim.toggle()
}
}){
ZStack
{
RoundedRectangle(cornerRadius: 20)
.foregroundColor(.clear)
.background(Color(UIColor.systemGray4))
.cornerRadius(20)
.frame(width: WidthScreen)
.rotationEffect(.degrees(EditMode ? 2 : 0))
.animation(self.EditMode ? Animation.easeInOut(duration: 0.1).repeatForever(autoreverses: true).delay(0.1) : Animation.easeInOut(duration: 0))
.rotationEffect(.degrees(EditMode ? -2 : 0))
.animation(self.EditMode ? Animation.easeInOut(duration: 0.1).repeatForever(autoreverses: true).delay(0.2) : Animation.easeInOut(duration: 0))
HStack{
Text(Title)
.font(.custom("Arial", size: 20))
.fontWeight(.bold)
.foregroundColor(Color(UIColor.label))
.frame(width: WidthScreen - 25, height: 50, alignment: .leading)
.offset(x: 10, y: 0)
Image("Arrow2").resizable()
.renderingMode(.template)
.foregroundColor(Color(UIColor.label))
.rotationEffect(.degrees(PressAnim ? 90 : 0))
.frame(width:10, height: 19)
.offset(x: -10, y: 0)
}
}
}
.frame(width: WidthScreen, height: 50, alignment: .center)
.fixedSize()
if ShowTip{
ZStack
{
RoundedRectangle(cornerRadius: 15)
.foregroundColor(.clear)
.background(Color(UIColor.systemGray4))
.cornerRadius(15)
.frame(width: WidthScreen)
HStack{
Text(Description)
.foregroundColor(Color(UIColor.label))
.padding(.all, 15)
}
}
.frame(width: WidthScreen, alignment: .center)
.transition(.asymmetric(insertion: .scale, removal: .opacity))
.fixedSize()
}
}
}
Move them out of body context and make them state property of your view, like
#State private var ShowTip = false
#State private var PressAnim = false
Important: do name variables lowercased and meaningful, because, for example, your Array conflicts with standard swift type Array, so consequences might be unpredictable. The same is for your custom types - give them unique names.