swiftUI move transition with a draw limit? - ios

I have a transition between two texts I want to mimic with a SwiftUI transition. this is my reference:
in SwiftUI I have the following code:
Text(model.title)
.transition(.opacity.animation(.default).combined(with: .move(edge: .bottom)))
.id("Title" + model.title)
.animation(.default)
and this is the result:
how could I approach this?
In the reference animation, by the time the text starts to slide it's already close to zero opacity and doesn't really move away from it's frame.
In swiftUI it clearly come from below the current text and moves up

I think .offset instead of .move should do the job:
struct ContentView: View {
let title = ["INITIAL ITEM", "NEW ITEM"]
#State private var i = 0
var body: some View {
VStack {
Spacer()
ZStack {
Text(title[i])
.font(.largeTitle)
.fontWeight(.bold)
.id(i)
.transition(
.offset(x: 0, y: 5)
.combined(with: .opacity)
)
}
Spacer()
Button("Toggle") {
withAnimation(.linear(duration: 0.5)) {
i = 1-i
}
}
}
}
}

Related

SwiftUI iOS15 Image being animated from top left corner instead of cetner

I ma having an unusual issue.I have a loading screen where the center image is suppose to give a heart beat animation. I am accomplishing this by using the scaleEffect. This works fine in iOS 16. However, in iOS 15 (and even 15.4) The animation is wrong.
What is happening is that the animation's reference point is in the top-left corner instead of the picture's center. Additionally, it appears that the entire view is animating rather then the picture.
Coud I get some feedback on my code? I believe that I am setting something incorrectly or maybe there is a better way of accomplishing what I want.
The issue is happening in the LoadingView struct
struct LoadingView: View{
#State private var animatingLogo = false
var body: some View{
// GeometryReader{geoReader in
VStack(alignment: .center, spacing: 6) {
Image("JHLogo")
.frame(width: 200, height: 200, alignment: .center)
.aspectRatio(contentMode: .fill)
.scaleEffect(animatingLogo ? 0.15 : 0.1, anchor: .center)
.onAppear{
withAnimation(.easeInOut(duration: 1).repeatForever()){
animatingLogo.toggle()
}
}
}
.navigationBarHidden(true)
}
}
And here is the top level view in case this is the one that has the issue:
// Initial view that is loaded on app startup
struct MainView: View{
#State private var isLoading = false
var body : some View {
GeometryReader{geoReader in
NavigationView {
if (isLoading) {
LoadingView()
}
else {
CoverPageView()
}
}
.frame(width: geoReader.size.width, height:geoReader.size.height)
.fixedSize()
.aspectRatio(contentMode: .fill)
.onAppear {
startLoadingScreen()
}
}
}
func startLoadingScreen() {
isLoading = true;
DispatchQueue.main.asyncAfter(deadline: .now() + 3){
isLoading = false
}
}
}

In SwiftUI, is there a way to expand and contract a view to show hidden text and buttons in the expanded version of the view?

I'm making a simple task app and using ForEach to populate task rows with the task information from my model. I need a way to animate my task view to open up and reveal some description text and two buttons. I want to turn from A into B on tap, and then back again on tap:
Design Image
I've tried a couple things. I successfully got a proof-of-concept rectangle animating in a test project, but there are issues. The rectangle shrinks and grows from the centre point, vs. from the bottom only. When I place text inside it, the text doesn't get hidden and it looks really bad.
struct ContentView: View {
#State var animate = false
var animation: Animation = .spring()
var body: some View {
VStack {
Rectangle()
.frame(width: 200, height: animate ? 60 : 300)
.foregroundColor(.blue)
.onTapGesture {
withAnimation(animation) {
animate.toggle()
}
}
}
}
In my main app, I was able to replace my first task view (closed) with another view that's open. This works but it looks bad and it's not really doing what I want. It's effectively replacing the view with another one using a fade animation.
ForEach(taskArrayHigh) { task in
if animate == false {
TaskView(taskTitle: task.title, category: task.category?.rawValue ?? "", complete: task.complete?.rawValue ?? "", priorityColor: Color("HighPriority"), task: task, activeDate: activeDate)
.padding(.top, 10)
.padding(.horizontal)
.onTapGesture {
withAnimation(.easeIn) {
animate.toggle()
}
}
.transition(.move(edge: .bottom))
} else if animate == true {
TaskViewOpen(task: "Grocery Shopping", category: "Home", remaining: 204, completed: 4)
.padding(.top, 10)
.padding(.horizontal)
.onTapGesture {
withAnimation(.easeIn) {
animate.toggle()
}
}
}
Is there a way to animate my original closed view to open up and reveal the description text and buttons?
You are on the right track with your .transition line you have, but you want to make sure that the container stays the same and the contents change -- right now, you're replacing the entire view.
Here's a simple example illustrating the concept:
struct ContentView: View {
#State var isExpanded = false
var body: some View {
VStack {
Text("Headline")
if isExpanded {
Text("More Info")
Text("And more")
}
}
.padding()
.frame(maxWidth: .infinity)
.transition(.move(edge: .bottom))
.background(Color.gray.cornerRadius(10.0))
.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}
}
Since you're using it inside a ForEach, you'll probably want to abstract this into its own component, as it'll need its own #State to keep track of the expanded state as I've shown here.
Update, based on comments:
Example of using a PreferenceKey to get the height of the expandable view so that the frame can be animated and nothing fades in and out:
struct ContentView: View {
#State var isExpanded = false
#State var subviewHeight : CGFloat = 0
var body: some View {
VStack {
Text("Headline")
VStack {
Text("More Info")
Text("And more")
Text("And more")
Text("And more")
Text("And more")
Text("And more")
}
}
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
.onPreferenceChange(ViewHeightKey.self) { subviewHeight = $0 }
.frame(height: isExpanded ? subviewHeight : 50, alignment: .top)
.padding()
.clipped()
.frame(maxWidth: .infinity)
.transition(.move(edge: .bottom))
.background(Color.gray.cornerRadius(10.0))
.onTapGesture {
withAnimation(.easeIn(duration: 2.0)) {
isExpanded.toggle()
}
}
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
Using Swift 5 you can use withAnimation and have the view hidden based on state.
ExpandViewer
Has a button to show and hide the inner view
Takes in a content view
struct ExpandViewer <Content: View>: View {
#State private var isExpanded = false
#ViewBuilder let expandableView : Content
var body: some View {
VStack {
Button(action: {
withAnimation(.easeIn(duration: 0.5)) {
self.isExpanded.toggle()
}
}){
Text(self.isExpanded ? "Hide" : "View")
.foregroundColor(.white)
.frame(maxWidth: .infinity, minHeight: 40, alignment: .center)
.background(.blue)
.cornerRadius(5.0)
}
if self.isExpanded {
self.expandableView
}
}
}
}
Using the viewer
ExpandViewer {
Text("Hidden Text")
Text("Hidden Text")
}

SwiftUI: Weird behaviour of Slider when it is in the leading/trailing of navigation bar items

I'm having trouble placing a Slider as a view in the trailing of navigation bar. Since it's hooked to a state, changing the state of it is very jumpy and not smooth at all.
Here's my sample code:
struct ContentView: View {
#State var opacityValue: Double = 1
var body: some View {
NavigationView {
GeometryReader { geometry in
VStack {
Slider(value: self.$opacityValue, in: 0...100, step: 5) { changed in
}
Image(systemName: "photo.fill")
.foregroundColor(.blue)
.padding()
.opacity(opacityValue / 100)
}.navigationBarItems(trailing: HStack(spacing: 20) {
Slider(value: self.$opacityValue, in: 0...100, step: 5) { _ in
}.frame(width: 100)
}).frame(width: geometry.size.width / 2)
}
}
}
}
The slider that I have in the regular VStack changes the opacity very smooth (and it even moves the slider that is in the nav bar.) But on the other hand, moving the slider that is in the nav bar is very glitchy/jumpy.
Is there a way to make this work?
You can make new View model for Image with #Binding property and pass your opacity through it, which will stop glitching movement on slider when #State property changes and View have to be refreshed
struct ContentView: View {
#State var opacityValue: Double = 1
var body: some View {
NavigationView {
GeometryReader { geometry in
VStack {
Slider(value: self.$opacityValue, in: 0...100, step: 5) { _ in
}
ImageOpacityView(opacity: $opacityValue)
}.navigationBarItems(trailing: HStack(spacing: 20) {
Slider(value: self.$opacityValue, in: 0...100, step: 5) { _ in
}.frame(width: 100)
}).frame(width: geometry.size.width / 2)
}
}
}
struct ImageOpacityView: View {
#Binding var opacity: Double
var body: some View {
Image(systemName: "photo.fill")
.foregroundColor(.blue)
.padding()
.opacity(opacity / 100)
}
}
}

Why does my SwiftUI animation revert to the previous frame on rotation?

I have a SwiftUI View that is meant to be sort of a "warning" background that flashes on and off. It appears inside another View with a size that changes depending on device orientation. Everything works properly upon the first appearance, but if the device is rotated while the warning view is active it incorporates the frame of the previous orientation into the animation, making it not only flash on and off, but also move in and out of the view its positioned in, as well as change to and from the size it was on the previous orientation. Why is this happening and how can I fix it? Generic code:
struct PulsatingWarningBackground: View {
let color : Color
let speed : Double
#State private var opacity : Double = 1
var body: some View {
GeometryReader {geometry in
self.color
.opacity(self.opacity)
.animation(
Animation
.linear(duration: self.speed)
.repeatForever(autoreverses: true)
)
.onAppear(perform: {self.opacity = 0})
}
}
}
struct PulsatingWarningViewHolder: View {
var body: some View {
GeometryReader {geometry in
ZStack {
Color.white
ZStack {
Color.black
PulsatingWarningBackground(color: .red, speed: 1/4)
}.frame(width: geometry.frame(in: .local).width/5, height: geometry.frame(in: .local).height/10, alignment: .center)
}
}
}
}
You can apply animation only to opacity by using withAnimation(_:_:) inside onAppear(perform:). It works properly as you want.
struct PulsatingWarningBackground: View {
let color : Color
let speed : Double
#State private var opacity : Double = 1
var body: some View {
self.color
.opacity(self.opacity)
.onAppear(perform: {
withAnimation(Animation.linear(duration: self.speed).repeatForever()) {
self.opacity = 0
}
})
}
}
struct PulsatingWarningViewHolder: View {
var body: some View {
GeometryReader {geometry in
ZStack {
Color.white
ZStack {
Color.black
PulsatingWarningBackground(color: .red, speed: 1/4)
}
.frame(width: geometry.frame(in: .local).width/5, height: geometry.frame(in: .local).height/10, alignment: .center)
}
}
}
}

SwiftUI Animation Not Working When View Removed

SwiftUI Animate View Removal Not Working
I'm struggling with the animation modifiers on SwiftUI Views.
I have a mapping function in SwiftUI with MKMapView ViewRepresentable. I have an
annotation with two controls. The code for animation listed here and the
ComboDetailMapView below does indeed animate correctly for presentation of the
subview - but nothing I have tried animates the view as it is removed.
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
if control == view.leftCalloutAccessoryView {
//this animates in - but does NOT animate out - required here for ANY animation
withAnimation(.easeInOut(duration: 1.0)) {
parent.userDefaultsManager.showAddRemoveWaypointsView.toggle()
}
} else if control == view.rightCalloutAccessoryView {
//this animates in - but does NOT animate out - required in this place
withAnimation(.easeInOut(duration: 1.0)) {
parent.userDefaultsManager.displayAddressView.toggle()
}
//...bunch more stuff
}
The view that I am animating is a simple white background image with opacity variation
and an alert style subview.
struct DisplayAddressView: View {
#ObservedObject var userDefaultsManager: UserDefaultsManager
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
GeometryReader { geo in
VStack {
Image("PlainMask")
.resizable()
.frame(width: geo.size.width, height: geo.size.height + 20, alignment: .center)
.opacity(0.9)
}//vstack
}//geo reader
RoundedRectangle(cornerRadius: 20)
.fill(Color.blue).opacity(0.3)
.frame(width: 300, height: 200)
VStack {
Button(action: {
//animation command does not work here
//withAnimation(.easeInOut(duration: 1.0)) {
self.userDefaultsManager.displayAddressView.toggle()
}) {
VStack {
Text("Approximate Address")
//.font(.headline)
.foregroundColor(.primary)
.font(.system(size: 20) )
.multilineTextAlignment(.center)
Divider()
Text(self.userDefaultsManager.approximateAddress)
//.font(.headline)
.foregroundColor(.red)
.font(.system(size: 20) )
.multilineTextAlignment(.center)
}
}
.padding(.bottom)
Divider()
Button(action: {
//animation command does not work here
//withAnimation(.easeInOut(duration: 1.0)) {}
self.userDefaultsManager.displayAddressView.toggle()
}) {
Text("Dismiss")
//.font(.headline)
.foregroundColor(.primary)
.font(.system(size: 20) )
}
}
}//Zstack
}//body
}
And this is the view that launches the DisplayAddressView view:
struct ComboDetailMapView: View {
#ObservedObject var userDefaultsManager: UserDefaultsManager
var aTrip: Trip?
var body: some View {
ZStack {
VStack {
Text(aTrip?.name ?? "Unknown Map Name")
.padding(.top, -50)
.padding(.bottom, -20)
DetailMapView(userDefaultsManager: userDefaultsManager, aTrip: aTrip)
.padding(.top, -20)
Text(self.userDefaultsManager.tripTimeAndDistance)
.multilineTextAlignment(.center)
}//vstack
if userDefaultsManager.displayAddressView {
DisplayAddressView(userDefaultsManager: userDefaultsManager)
//this works to add slide to the other animation
.transition(AnyTransition.opacity.combined(with: .slide))
//this does not work for dismissal
//.transition(.asymmetric(insertion: AnyTransition.opacity.combined(with: .slide), removal: .scale))
}
}//zstack
}//body
}
I've also tried the ideas of this SO 59064231 .No joy.
I confess the procedure to animate views since you can no longer attach the modifier
directly to the view is still confusing me. Any guidance would be appreciated.
Xcode 11.3.1 (11C504)
It turns out that if you step back and think about the functionality that I require here, I can simply use a ternary to control the position of the view. No if statement needed to compare the binding, no complex .transition coordination. As a bonus, opacity can be animated too if desired.
DisplayAddressView(userDefaultsManager: userDefaultsManager)
.offset(x: userDefaultsManager.showAddressView ? 0 : 500)
.animation(.easeInOut(duration: 1.0))
Slick.

Resources