How to animate view separately in SwiftUI? - swift

I created a red rectangle on a black rectangle. I wanna when I press the toggle, the red one scale with animation, and they both move with animation, two animation work separately but not like this:
struct AnimationTest: View {
#State var toggled = false
var body: some View {
VStack {
ZStack {
Rectangle()
.frame(width: 200, height: 200)
.foregroundColor(Color.black)
Rectangle()
.frame(width: 50, height: 50)
.foregroundColor(Color.red)
.scaleEffect(toggled ? 2.0 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.3), value: toggled)
}
.offset(x: 0.0, y: toggled ? -50 : 0.0)
.animation(.easeOut, value: toggled)
Toggle("Toggle", isOn: $toggled)
.padding(20.0)
}
}
}

Related

Swift UI animation diagonal instead horizontal

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
}
}
}
}

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, shadow only for container

For example, I have this view:
import SwiftUI
struct TarifsScreen: View {
var body: some View {
GeometryReader { geometry in
VStack {
VStack {
Spacer()
Text("Text1")
Spacer()
Text("Text2")
Spacer()
Text("Text3")
Spacer()
}
}
.frame(width: geometry.size.width, height: geometry.size.height)
.shadow(color: Color.white, radius: 10, x: 0, y: 0)
}
}
}
How can I apply shadow only for VStack, not for all elements inside VStack? May be I can do it with ZStack and two containers?
Add background and apply shadow to it, like in below example
VStack {
...
}
.background(Color.white // any non-transparent background
.shadow(color: Color.red, radius: 10, x: 0, y: 0)
)
.frame(width: geometry.size.width, height: geometry.size.height)
Also for a specific view:
var body: some View {
Text("SwiftUI is Awesome").padding().background(
Rectangle()
.fill(Color.white)
.cornerRadius(12)
.shadow(
color: Color.gray.opacity(0.7),
radius: 8,
x: 0,
y: 0
)
)
}
Reference from here.
Result:
If you don't want to add fills or backgrounds, you can better override the .shadow modifier on the child views with a .clear color, to "stop" the shadow modifier from propagating down the hierarchy.
VStack {
// View with shadow
...
VStack {
// View without shadow
...
}
.shadow(color: .clear, radius: 0) // Override shadow
}
.shadow(color: .black.opacity(0.3), radius: 10)
For example if you are using a png image, the other answers would render the alpha channel with a solid color, which is not ideal.

Align rectangle to be within certain distance of safe area bottom edge SWIFTUI

I am used to Interface Builder and layout constraints but now I want to convert my app to Swift UI. What I am trying to do right now is align the top edge of the view marked with a 1 to be within a certain distance of the safe area bottom edge (marked with a 2) so that the top edge that is now at 1 will then be at position 3. I tried using spacers but then it will look different on smaller devices such as an iPhone 8. In IB I could have used a simple layout constraint. How does this work in Swift UI? I have attached the relevant code and an image. Thank you for your help.
struct ContentView: View {
init() {
UINavigationBar.appearance().backgroundColor = .orange
}
var body: some View {
NavigationView {
VStack{
Spacer()
ZStack{
Rectangle()
.fill(Color(hue: 0, saturation: 0, brightness: 0, opacity: 0.1))
Image("")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 150)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
}.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width, alignment: .center)
Spacer(minLength: 100)
ZStack(alignment: .bottom){
ExtractedView()
.edgesIgnoringSafeArea(.bottom)
}
}
.navigationBarTitle(Text("You See"))
.navigationBarHidden(false)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice("iPhone")
}
}
struct ExtractedView: View {
#State private var name: String = ""
var body: some View {
VStack{
ZStack(alignment: .top){
VStack{
RoundedRectangle(cornerRadius: 50)
.frame(width: 60, height: 7)
.padding(.top)
Button(action: /*#START_MENU_TOKEN#*/{}/*#END_MENU_TOKEN#*/) {
Text("Start advertising")
.font(.headline)
.foregroundColor(Color.black)
}.padding(.top)
TextField("Test", text: $name)
.padding(.all)
.background(Color.white.cornerRadius(20))
.padding()
}
RoundedRectangle(cornerRadius: 30)
.fill(Color(hue: 0, saturation: 0, brightness: 0, opacity: 0.1))
.zIndex(-5)
}
}
}
}
Ok, so I was able to solve my problem. The trick is to create two VStacks with different frame alignments. The outer VStack has top alignment so that the Discover view can be at the top. The inner VStack has a bottom alignment so that the sheet can be pulled up from the bottom. Space will be filled from the bottom up in this case.
VStack{
Discover()
.padding(.top, 60.0)
VStack{
Text("Recent Messages:")
.font(.headline)
DetailSheet()
.offset(x: 0, y: 0)
}.frame(width: g.size.width, height: (g.size.height - g.size.width - 80 + 200), alignment: .bottom)
}
.frame(width: g.size.width, height: g.size.height, alignment: .top)
you can try this:
offset -10 is just your offset you want to have....
i hope i understood you right, i am not so sure...
var body: some View {
VStack{
// ZStack(alignment: .top){
VStack{
RoundedRectangle(cornerRadius: 50)
.frame(width: 60, height: 7)
.padding(.top)
Button(action: /*#START_MENU_TOKEN#*/{}/*#END_MENU_TOKEN#*/) {
Text("Start advertising")
.font(.headline)
.foregroundColor(Color.black)
}.padding(.top)
TextField("Test", text: $name)
.padding(.all)
.background(Color.white.cornerRadius(20))
.padding()
}.background(RoundedRectangle(cornerRadius: 30)
.fill(Color(hue: 0, saturation: 0, brightness: 0, opacity: 0.1))
).offset(y:-10)
}
// }