I am trying to implement my own transition between views, where clicking the "Back" button will animate the transition in one direction while clicking the "Next" transition will animate it in the other.
struct InputForm: View {
...
#Binding var idx: Int
let nextTransition = AnyTransition.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
let backTransition = AnyTransition.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
#State var activeTransition = AnyTransition.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
var body: some View {
HStack {
Button(action: {
withAnimation {
self.activeTransition = self.nextTransition
self.idx -= self.idx == 0 ? 0 : 1
}
}) {
Text("Back")
}.buttonStyle(CoreBasicButtonStyle(color: .mainGray))
Button(action: {
withAnimation {
self.activeTransition = self.nextTransition
self.idx += self.idx == 1 ? 0 : 1
}
}) {
Text("Next")
}.buttonStyle(CoreBasicButtonStyle(color: .mainGray))
}
}
.animation(.easeInOut)
.transition(activeTransition)
}
}
For some reason, it is only using the initial transition and never changing. Can SwiftUI not detect state changes for .transition?
Related
I am trying to understand transition animations in SwiftUI and it just doesn't work for me. In the following code, the transition after one second is immediate (no fade).
struct TestApp: App {
#State private var isReady: Bool = false
var body: some Scene {
WindowGroup {
if isReady {
Color.red
} else {
Color.blue
.transition(.opacity)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
withAnimation {
isReady = true
}
}
}
}
}
}
}
Transition and animation are different things, there is a view to be shown with transition and there is a view that animates this transition. Also app does not update view, a state should be inside view.
So here is a correct variant:
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView() // root view of the scene !!
}
}
}
struct ContentView: View {
// state of the view refreshes the view
#State private var isReady: Bool = false
var body: some View {
ZStack { // animating container !!
if isReady {
Color.red
// .transition(.opacity) // probably you wanted this as well
} else {
Color.blue
.transition(.opacity) // << transition here !!
}
}
.onAppear { // << responsible for animation !!
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
withAnimation {
isReady = true
}
}
}
}
}
RectangleView has a slide animation, his child TextView has a rotation animation. I suppose that RectangleView with his child(TextView) as a whole slide(easeInOut) into screen when Go! pressed, and TextView rotate(linear) forever. But in fact, the child TextView separates from his parent, rotating(linear) and sliding(linear), and repeats forever.
why animation applied to parent effect child animation?
struct AnimationTestView: View {
#State private var go = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
}
if go {
RectangleView()
.transition(.slide)
.animation(.easeInOut)
}
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
Text("Test")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation)
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
}
}
If there are several simultaneous animations the generic solution (in majority of cases) is to use explicit state value for each.
So here is a corrected code (tested with Xcode 12.1 / iOS 14.1, use Simulator or Device, Preview renders some transitions incorrectly)
struct AnimationTestView: View {
#State private var go = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
}
VStack { // container needed for correct transition !!
if go {
RectangleView()
.transition(.slide)
}
}.animation(.easeInOut, value: go) // << here !!
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
Text("Test")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation, value: animationRotating) // << here !!
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
}
}
PREVIOUS Q: swiftui, animation applied to parent effect child animation
Now the TextView has its own state. RectangleView with TextView slide into screen in 3 seconds, but state of TextView changed after a second while sliding. Now you can see TextView stops sliding, go to the destination at once. I Just want RectangleView with TextView as a whole when sliding, and TextView rotates or keeps still as I want.
import SwiftUI
struct RotationEnvironmentKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var rotation: Bool {
get { return self[RotationEnvironmentKey] }
set { self[RotationEnvironmentKey] = newValue }
}
}
struct AnimationTestView: View {
#State private var go = false
#State private var rotation = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
rotation.toggle()
print("set rotation = \(rotation)")
}
}
Group {
if go {
RectangleView()
.transition(.slide)
.environment(\.rotation, rotation)
}
}.animation(.easeInOut(duration: 3.0), value: go)
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#Environment(\.rotation) var rotation
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
print("refresh, rotation = \(rotation)"); return
HStack {
Spacer()
if rotation {
Text("R")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation, value: animationRotating)
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
} else {
Text("S")
}
}
}
}
I found the issue here: cannot use if cause in TextView, because when rotation changed, will create a new Text in new position, so we can see the R 'jump' to the right position.
code as following:
Text(rotation ? "R" : "S")
.foregroundColor(rotation ? .blue : .white)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(rotation ? animation : nil, value: animationRotating)
.onChange(of: rotation) { newValue in
animationRotating = true
}
Ive make a mini app with just a button and a picker and the idea is to have a done button above the picker so once ive chosen a value i can press done and the picker will close.
I am aware if you click the "click me" button it will open and if you click it again close the picker but im looking for a button that appears with the picker and disapears with the clicker when clicked.
Almost like a toolbar above the picker with a done button
#State var expand = false
#State var list = ["value1", "value2", "value3"]
#State var index = 0
var body: some View {
VStack {
Button(action: {
self.expand.toggle()
}) {
Text("Click me \(list[index])")
}
if expand {
Picker(selection: $list, label: EmptyView()) {
ForEach(0 ..< list.count) {
Text(self.list[$0]).tag($0)
}
}.labelsHidden()
}
}
The third image is what im trying to accomplish and the first 2 are what ive currently got
Thank you for your help
Add a Button to the if-clause:
if expand {
VStack{
Button(action:{self.expand = false}){
Text("Done")
}
Picker(selection: $list, label: EmptyView()) {
ForEach(0 ..< list.count) {
Text(self.list[$0]).tag($0)
}
}.labelsHidden()
}
}
Here is an approach how I would do this... of course tuning is still possible (animations, rects, etc.), but the direction of idea should be clear
Demo of result:
Code:
struct ContentView: View {
#State var expand = false
#State var list = ["value1", "value2", "value3"]
#State var index = 0
var body: some View {
VStack {
Button(action: {
self.expand.toggle()
}) {
Text("Click me \(list[index])")
}
if expand {
Picker(selection: $index, label: EmptyView()) {
ForEach(0 ..< list.count) {
Text(self.list[$0]).tag($0)
}
}.labelsHidden()
.overlay(
GeometryReader { gp in
VStack {
Button(action: {
self.expand.toggle()
}) {
Text("Done")
.font(.system(size: 42))
.foregroundColor(.red)
.padding(.vertical)
.frame(width: gp.size.width)
}.background(Color.white)
Spacer()
}
.frame(width: gp.size.width, height: gp.size.height - 12)
.border(Color.black, width: 8)
}
)
}
}
}
}
Also this would work (especially if you're doing it outside of a Vstack)...
// Toolbar for "Done"
func createToolbar() {
let toolBar = UIToolbar()
toolBar.sizeToFit()
// "Done" Button for Toolbar on Picker View
let doneButton = UIBarButtonItem(title: "Done", style: .plain, target:
self, action: #selector(PageOneViewController.dismissKeyboard))
toolBar.setItems([doneButton], animated: false)
toolBar.isUserInteractionEnabled = true
// Makes Toolbar Work for Text Fields
familyPlansText.inputAccessoryView = toolBar
kidsOptionText.inputAccessoryView = toolBar
ethnicityText.inputAccessoryView = toolBar
}
And be sure to call createToolbar() and you're done!
struct ContentView: View {
var colors = ["Red", "Green", "Blue"]
#State private var selectedColor = 0
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedColor, label: Text("Color")) {
ForEach(0 ..< colors.count) {
Text(self.colors[$0])
}
}
}
}
}
}
}
This has some form specific picker behavior where it opens up inline with no button hiding needed.
Using SwiftUI, I'm trying to show an animated image and hide the text when the user clicks the button. Here is my code:
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
HStack {
if self.showingActivity {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showingActivity ? 360.0 : 0.0))
.animation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false))
}
else {
Text("Continue")
}
}
}
}
The Continue text appears and when clicked it disappears (as expected) and the image shows, but no animation. Any ideas what I might be doing wrong.
Thanks
or try this:
struct ImageView : View {
#State private var showAction = false
var body: some View {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showAction ? 360.0 : 0.0))
.animation(self.showAction ? Animation.linear(duration: 1.5).repeatForever(autoreverses: false) : nil)
.onAppear() {
self.showAction = true
}
}
}
struct ContentView: View {
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
HStack {
if self.showingActivity {
ImageView()
}
else {
Text("Continue")
}
}
}
}
}
You can use .opacity instead of if...else statements, in this case rotation works on image. But in canvas I still see some glitches, if tap button several times but I didn't try on real device (behavior there can be other). Hope it'll help:
struct AnimatableView: View {
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
ZStack {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showingActivity ? 360.0 : 0.0))
.animation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false))
.opacity(self.showingActivity ? 1 : 0)
Text("Continue")
.opacity(self.showingActivity ? 0 : 1)
}
}
}
}