SwiftUI Animation Not Working When View Removed - ios

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.

Related

SwiftUI View does not disappear instantly after the transition animation ends

I created an application with move transition, which sends a View up from below with SwiftUI. However, when View disappears, transition animation is over, but the View remains for about 1 second.
This is my code.
struct ContentView: View {
#State var animation: Bool = false
var body: some View {
VStack {
// This is the Button
Button(action: {
withAnimation(.spring(dampingFraction: 1, blendDuration: 0.5)) {
animation.toggle()
}
}) {
Image(systemName: "star.fill")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(.accentColor)
}
// This condition sends up the View
if animation {
SecondView()
.transition(.move(edge: .bottom))
}
}
.padding()
}
}
struct SecondView: View {
var body: some View {
VStack {
Text("Hello, world!")
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
}
}
}
And this happened.
I think what you need to do is to apply the animation directly to your button by using the animation modifier so your code may look like this:
Button(action: {
withAnimation {
animation.toggle()
}
}) {
Image(systemName: "star.fill")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(.accentColor)
}
.animation(.spring(dampingFraction: 1, blendDuration: 0.5), value: animation)
if animation {
SecondView()
.transition(.move(edge: .bottom))
}

SwiftUI matchedGeometryEffect combined with NavigationView

I'm trying to used matchedGeometryEffect on a pair of views. It works well until you navigate to a child view and then back, in which case the matchedGeometryEffect seems broken briefly (the red rectangle is instantly visible when I try expanding my view)
Is there something I'm missing?
struct ContentView: View {
#Namespace private var namespace
#State private var expanded = false
var body: some View {
NavigationView {
VStack {
NavigationLink("Click Me") {
Text("Hello, world")
}
Group {
if expanded {
Rectangle()
.foregroundColor(.red)
.matchedGeometryEffect(id: "Rect", in: namespace)
.frame(width: 300, height: 300)
}
else {
Rectangle()
.foregroundColor(.blue)
.matchedGeometryEffect(id: "Rect", in: namespace)
.frame(width: 50, height: 50)
}
}
.onTapGesture {
withAnimation(.linear(duration: 2.0)) {
expanded.toggle()
}
}
}
}
}
}
It seems this was a bug in iOS (I was using 15.5), the latest version (iOS 16.1) works without issue.

How to clip a view while using a SwiftUI .move transition / animation

I'm trying to animate in a view and make it appear as if it's a sort of drawer opening from another view. This is all fine except if the first view is not opaque. It appears that you can see the animating view the moment it begins animating. Is there a way to clip this so it appears that the view is growing from the top of the bottom view?
Even without opacity this is an issue if where you're animating in from isn't a covered (demoed in second gif)
Sample Code:
struct ContentView: View {
#State private var showingSecondView: Bool = false
var body: some View {
VStack(spacing: 0) {
Spacer()
if showingSecondView {
ZStack {
Color.green.opacity(0.25)
Text("Second View")
}
.frame(width: 300, height: 300)
.transition(.move(edge: .bottom))
}
ZStack {
Color.black.opacity(1)
Text("First View")
}
.frame(width: 300, height: 300)
Button("Animate In / Out") {
showingSecondView.toggle()
}
.padding()
}
.animation(.easeInOut, value: showingSecondView)
}
}
It is possible to do by clipping exact container of 'drawer'. Here is a demo of possible approach.
Tested with Xcode 13.2 / iOS 15.2 (Simulator slow animation is ON for better demo)
var body: some View {
VStack(spacing: 0) {
Spacer()
VStack {
if showingSecondView {
ZStack {
Color.green.opacity(0.25)
Text("Second View")
}
.transition(.move(edge: .bottom))
} else {
Color.clear // << replacement for transition visibility
}
}
.frame(width: 300, height: 300)
.animation(.easeInOut, value: showingSecondView) // << animate drawer !!
.clipped() // << clip drawer area
ZStack {
Color.black.opacity(0.2)
Text("First View")
}
.frame(width: 300, height: 300)
Button("Animate In / Out") {
showingSecondView.toggle()
}
.padding()
}
}
Here a way for you:
struct ContentView: View {
#State private var isSecondViewPresented: Bool = false
var body: some View {
VStack(spacing: 0) {
Spacer()
ZStack {
Color.green.opacity(0.25).cornerRadius(20)
Text("Second View")
}
.frame(width: 300, height: 300)
.offset(y: isSecondViewPresented ? 0 : 300)
.clipShape(RoundedRectangle(cornerRadius: 20))
ZStack {
Color.black.opacity(0.1).cornerRadius(20)
Text("First View")
}
.frame(width: 300, height: 150)
Button("Animate In / Out") {
isSecondViewPresented.toggle()
}
.padding()
}
.animation(.easeInOut, value: isSecondViewPresented)
}
}

How to show Profile Icon next to Large Navigation Bar Title in SwiftUI?

I am developing an App that supports multiple Profiles. I really like the way Apple displays the Profile Icon next to the Large Navigation Bar Title in all their Apps. See the Screenshot below:
My Question is the following:
Is it possible to achieve this in SwiftUI? And if so, how?
If it's not possible in pure SwiftUI, how can I achieve it including UIKit Code?
Thanks for your help.
I solved this by using SwiftUI-Introspect, to "Introspect underlying UIKit components from SwiftUI".
Here is an example of a view:
struct ContentView: View {
#State private var lastHostingView: UIView!
var body: some View {
NavigationView {
ScrollView {
ForEach(1 ... 50, id: \.self) { index in
Text("Index: \(index)")
}
.frame(maxWidth: .infinity)
}
.navigationTitle("Large title")
.introspectNavigationController { navController in
let bar = navController.navigationBar
let hosting = UIHostingController(rootView: BarContent())
guard let hostingView = hosting.view else { return }
// bar.addSubview(hostingView) // <--- OPTION 1
// bar.subviews.first(where: \.clipsToBounds)?.addSubview(hostingView) // <--- OPTION 2
hostingView.backgroundColor = .clear
lastHostingView?.removeFromSuperview()
lastHostingView = hostingView
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.trailingAnchor.constraint(equalTo: bar.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: bar.bottomAnchor, constant: -8)
])
}
}
}
}
Bar content & profile picture views:
struct BarContent: View {
var body: some View {
Button {
print("Profile tapped")
} label: {
ProfilePicture()
}
}
}
struct ProfilePicture: View {
var body: some View {
Circle()
.fill(
LinearGradient(
gradient: Gradient(colors: [.red, .blue]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 40, height: 40)
.padding(.horizontal)
}
}
The .frame(width: 40, height: 40) & hostingView.bottomAnchor constant will need to be adjusted to your needs.
And the results for each option (commented in the code):
Option 1
Option 2
View sticks when scrolled
View disappearing underneath on scroll
Without NavigationView
I done this with pure SwiftUI. You have to replace the Image("Profile") line with your own image (maybe from Assets or from base64 data with UIImage).
HStack {
Text("Apps")
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
.padding(.all, 30)
This products following result:
With NavigationView
Let's assume that you have NavigationView and inside that there's only ScrollView and .navigationTitle. You can add that profile image there by using overlay.
NavigationView {
ScrollView {
//your content here
}
.overlay(
ProfileView()
.padding(.trailing, 20)
.offset(x: 0, y: -50)
, alignment: .topTrailing)
.navigationTitle(Text("Apps"))
}
Where ProfileView could be something like this:
struct ProfileView: View {
var body: some View {
Image("Profile")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
}
The result will be like this...
...which is pretty close to the App Store:

Unable to change background color of view in SwiftUI

I am trying to change background color main this view but unable to do it. I tried to put background(Color.green) at HStack, VSTack and even on ZStack but it did not work, not sure if i am putting at right place. By default it is taking phone or simulator color which is white but i want to apply custom background color
My Xcode version is 11.5
struct HomePageView: View {
#State var size = UIScreen.main.bounds.width / 1.6
var body: some View {
GeometryReader{_ in
VStack {
HStack {
ZStack{
// main home page components here....
NavigationView{
VStack {
AssignmentDaysView()
}.background(Color.lairBackgroundGray)
.frame(width: 100, height: 100, alignment: .top)
.navigationBarItems(leading: Button(action: {
self.size = 10
}, label: {
Image("menu")
.resizable()
.frame(width: 30, height: 30)
}).foregroundColor(.appHeadingColor), trailing:
Button(action: {
print("profile is pressed")
}) {
HStack {
NavigationLink(destination: ProfileView()) {
LinearGradient.lairHorizontalDark
.frame(width: 30, height: 30)
.mask(
Image(systemName: "person.crop.circle")
.resizable()
.scaledToFit()
)
}
}
}
).navigationBarTitle("Home", displayMode: .inline)
}
HStack{
menu(size: self.$size)
.cornerRadius(20)
.padding(.leading, -self.size)
.offset(x: -self.size)
Spacer()
}
Spacer()
}.animation(.spring()).background(Color.lairBackgroundGray)
Spacer()
}
}
}
}
}
struct HomePageView_Previews: PreviewProvider {
static var previews: some View {
HomePageView()
}
}
In your NavigationView you have a VStack. Instead you can use a ZStack and add a background below your VStack.
Try the following:
NavigationView {
ZStack {
Color.green // <- or any other Color/Gradient/View you want
.edgesIgnoringSafeArea(.all) // <- optionally, if you want to cover the whole screen
VStack {
Text("assignments")
}
.background(Color.gray)
.frame(width: 100, height: 100, alignment: .top)
}
}
Note: you use many stacks wrapped in a GeometryReader which you don't use. Consider simplifying your View by removing unnecessary stacks. Also you may not need a GeometryReader if you use UIScreen.main.bounds (however, GeometryReader is preferred in SwiftUI).
Try removing some layers: you can start with removing the top ones: GeometryReader, VStack, HStack...
Try the following:
Change the view background color especially safe area also
struct SignUpView: View {
var body: some View {
ZStack {
Color.blue //background color
}.edgesIgnoringSafeArea(.all)

Resources