Simple transition animation not working in SwiftUI - animation

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

Related

How to detect a single mouse down event in SwiftUI on macOS

I want to detect ONE mouse down and ONE mouse up event on my view (a simple Rectangle). Here is the code I already made. Unfortunately I got a lot of 'mouse down' and 'mouse up' on the console. This not what I want. I want just one 'mouse down' when the mouse is pressed on my rectangle and one 'mouse up' when the mouse is released.
var body: some View {
Rectangle()
.onAppear(perform: {
NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown]) { event in
print ("mouse down")
return event
}
NSEvent.addLocalMonitorForEvents(matching: [.leftMouseUp]) { event in
print ("mouse up")
return event
}
})
}
I found the solution by using a View modifier
//
// ContentView.swift
// OnPressedOnRelease
//
import SwiftUI
struct ContentView: View {
#State private var message: String = "Click on rectangle"
var body: some View {
VStack {
Image(systemName: "magicmouse")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("\(message)")
Rectangle()
.modifier(PressActions(
onPress: {
// Do something on press...
message = "Mouse down"
},
onRelease: {
// Do something on release...
message = "Mouse up"
}
))
}
.padding()
.frame(width: 200, height: 200)
}
}
struct PressActions: ViewModifier {
var onPress: () -> Void
var onRelease: () -> Void
func body(content: Content) -> some View {
content
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged({ _ in
onPress()
})
.onEnded({ _ in
onRelease()
})
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I've recently worked with Gestures and a good solution is to do this :
struct ContentView: View {
#State var dragGestureValue: DragGesture.Value?
var body: some View {
Rectangle()
.gesture(pressActionsGesture())
.padding()
}
private func pressActionsGesture() -> some Gesture {
return DragGesture(minimumDistance: 0)
.onChanged {
// If nil, it means its the beginning of the DragGesture
if dragGestureValue == nil {
print("Mouse Down")
}
// Gives DragGesture informations to our #State value
dragGestureValue = $0
}
.onEnded { _ in
print("Mouse Up")
dragGestureValue = nil
}
}
}

Swift if statement in modifier

So I have an basic Taskview:
TabView {
NavigationView {
TasksView()
}
.tabItem {
Image(systemName: "bookmark")
Text("Tasks")
}
NavigationView {
ShopView()
}
.tabItem {
Image(systemName: "cart.fill")
Text("Shop")
}
}
.environment(\.colorScheme, isDarkMode ? .dark : .light)
And now I want to add an if statement to .environment. So like if Default: .environment(.colorScheme, isDarkMode ? .dark : .light)
How can I do this?
Use #Environment to call the current color scheme. Then you can create a color variable.
struct ContentView: View {
#Environment(\.colorScheme) private var colorScheme
var body: some View {
TabView {
NavigationView {
TasksView()
}
.tabItem {
Image(systemName: "bookmark")
Text("Tasks")
}
.foregroundColor(titleColor) // <- here
NavigationView {
ShopView()
}
.tabItem {
Image(systemName: "cart.fill")
Text("Shop")
}
.foregroundColor(titleColor) // <- here
}
}
private var titleColor: Color {
switch colorScheme {
case .light:
return .red
case .dark:
return .yellow
#unknown default:
return .blue
}
}
}
Here's a possible solution. Use an ObservableObject class, store and save the variable. Set it as a StateObject and set as environmentObject in your App, then you can handle updates in your View by accessing the variable.
class ColorScheme: ObservableObject {
#AppStorage("dark") var dark = false
}
struct MyApp: App {
#StateObject var colorscheme = ColorScheme()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(colorscheme)
.colorScheme(colorscheme.dark ? .dark : .light)
}
}
}
struct ContentView: View {
#EnvironmentObject var colorscheme: ColorScheme
var body: some View {
NavigationView {
List {
Toggle("Color Scheme", isOn: colorscheme.$dark)
}
}
}
}

Calling a sheet from a menu item

I have a macOS app that has to display a small dialog with some information when the user presses the menu item "Info".
I've tried calling doing this with a .sheet but can't get it to display the sheet. Code:
#main
struct The_ThingApp: App {
private let dataModel = DataModel()
#State var showsAlert = false
#State private var isShowingSheet = false
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
}
.commands {
CommandMenu("Info") {
Button("Get Info") {
print("getting info")
isShowingSheet.toggle()
}
.sheet(isPresented: $isShowingSheet) {
VStack {
Text("Some stuff to be shown")
.font(.title)
.padding(50)
Button("Dismiss",
action: { isShowingSheet.toggle() })
}
}
}
}
}
}
How would I display a sheet from a menu item?
However, if a sheet is not the way to do it (I think given the simplicity of what I need to show, it would be it), how would you suggest I do it? I tried creating a new view, like I did with the preferences window, but I can't call it either from the menu.
put the sheet directly on ContentView:
#main
struct The_ThingApp: App {
#State private var isShowingSheet = false
var body: some Scene {
WindowGroup {
ContentView()
// here VV
.sheet(isPresented: $isShowingSheet) {
VStack {
Text("Some stuff to be shown")
.font(.title)
.padding(50)
Button("Dismiss",
action: { isShowingSheet.toggle() })
}
}
}
.commands {
CommandMenu("Info") {
Button("Get Info") {
print("getting info")
isShowingSheet.toggle()
}
}
}
}
}

swiftui, animation applied to parent effect child animation

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

swiftui, animation applied to parent effect child animation(Part II)

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
}

Resources