Weird animation of a Button in Stack Views in iOS 16 - ios

I created a SampleView where a Button is wrapped in a ZStack, and the whole ZStack is animated on tap of the Button. It looks like below:
struct SampleView: View {
#State private var toggle: Bool = false
var body: some View {
ZStack {
Button {
toggle.toggle()
} label: {
Text("Tap here to move!")
.font(.system(size: 20, weight: .black))
.foregroundColor(.black)
}
.padding(10)
.background(.red)
}
.offset(x: 0, y: toggle ? 0 : 200)
.animation(.easeInOut(duration: 1), value: toggle)
}
}
struct SampleView_Previews: PreviewProvider {
static var previews: some View {
// ZStack {
SampleView()
// }
}
}
There's a bunch of other views that I want to present, so I simply wrapped SampleView inside a VStack (or ZStack). Now, the animation started to break:
struct SampleView_Previews: PreviewProvider {
static var previews: some View {
ZStack {
SampleView()
}
}
}
I noticed that I can work around this behavior by wrapping the button action with withAnimation.
withAnimation(.easeInOut(duration: 1)) {
toggle.toggle()
}
Still, I wonder what's going on here. Is this a bug?

This certainly looks like a bug. The Text label of the button is not getting the .animation() value applied to it, but the background is.
Here is another way to fix it. Apply the .animation() modifier to the Text label as well:
struct SampleView: View {
#State private var toggle: Bool = false
var body: some View {
ZStack {
Button {
toggle.toggle()
} label: {
Text("Tap here to move!")
.font(.system(size: 20, weight: .black))
.foregroundColor(.black)
.animation(.easeInOut(duration: 1), value: toggle) // here
}
.padding(10)
.background(.red)
}
.offset(x: 0, y: toggle ? 0 : 200)
.animation(.easeInOut(duration: 1), value: toggle)
}
}
More Evidence of the Bug
In this example, we have two buttons in a VStack that move together. This works correctly in iOS 15.5, but it breaks in iOS 16. The animation breaks for the button that is being pressed, but it works for the other button.
struct ContentView: View {
#State private var toggle: Bool = false
var body: some View {
VStack {
Button {
toggle.toggle()
} label: {
Text("Tap here to move!")
.font(.system(size: 20, weight: .black))
.foregroundColor(.black)
}
.padding(10)
.background(.red)
Button {
toggle.toggle()
} label: {
Text("Tap here to move!")
.font(.system(size: 20, weight: .black))
.foregroundColor(.black)
}
.padding(10)
.background(.blue)
}
.offset(x: 0, y: toggle ? 0 : 200)
.animation(.easeInOut(duration: 1), value: toggle)
}
}
The same fixes apply here: add the .animation() modifier to the Text label of each button, or wrap the button action with withAnimation { }.

I think it's a bug too. I found a temporary workaround.
You can try onTapGesture.
I fixed the weird animation.
struct SampleView: View {
#State private var toggle: Bool = false
var body: some View {
ZStack {
// Button {
// toggle.toggle()
// } label: {
Text("Tap here to move!")
.font(.system(size: 20, weight: .black))
.foregroundColor(.black)
// }
.padding(10)
.background(.red)
.onTapGesture {
toggle.toggle()
}
}
.offset(x: 0, y: toggle ? 0 : 200)
.animation(.easeInOut(duration: 1), value: toggle)
}
}
struct SampleView_Previews: PreviewProvider {
static var previews: some View {
ZStack {
SampleView()
}
}
}

Related

SwiftUI Rotation Animation off center

I am trying to make a simple dropdown list item in SwiftUI. This is what the code looks like:
struct SomeObject: Hashable {
var title: String = "title"
var entries: [String] = ["details", "details2", "details3"]
}
struct ContentView: View {
var data: [SomeObject] = [SomeObject()]
var body: some View {
List(data, id: \.self) { item in
HStack {
Text(item.title)
Spacer()
}
ForEach(item.entries, id: \.self) { entry in
ListItemView(entry)
}
}.listStyle(.plain)
}
}
struct ListItemView: View {
#State var expanded: Bool = false
#State var rotation: Double = 0
private let entry: String
init(_ entry: String) {
self.entry = entry
}
var body: some View {
VStack {
Divider().frame(maxWidth: .infinity)
.overlay(.black)
HStack {
Text(entry)
.fixedSize(horizontal: false, vertical: true)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.black)
.padding()
.rotationEffect(.degrees(expanded ? 180 : 360))
.animation(.linear(duration: 0.3), value: expanded)
}.padding(.horizontal)
.padding(.vertical, 6)
if expanded {
Text("Details")
}
Divider().frame(maxWidth: .infinity)
.overlay(.black)
}
.listRowSeparator(.hidden)
.listRowInsets(.init())
.onTapGesture {
expanded.toggle()
}
}
}
For some reason when clicking on the list item the animation looks like this:
How can I make the arrow rotate on its center point without moving up or down at all?
The problem you have there is that the arrow is animated but when the hidden text appears, that vertical expansion is not animated. That contrast between an element animated and another that is not makes the chevron looks like it is not doing it properly. So, try to animate the VStack like this:
struct CombineView: View {
#State var expanded: Bool = false
#State var rotation: Double = 0
let entry: String = "Detalle"
var body: some View {
VStack {
Divider().frame(maxWidth: .infinity)
.overlay(.black)
HStack(alignment: .center) {
Text(entry)
.fixedSize(horizontal: false, vertical: true)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.black)
.padding()
.rotationEffect(.degrees(expanded ? 180 : 360))
.animation(.linear(duration: 0.3), value: expanded)
}.padding(.horizontal)
.padding(.vertical, 6)
.background(.green)
if expanded {
Text("Details")
}
Divider().frame(maxWidth: .infinity)
.overlay(.black)
}.animation(.linear(duration: 0.3), value: expanded)//Animation added
.listRowSeparator(.hidden)
.listRowInsets(.init())
.onTapGesture {
expanded.toggle()
}
}
}
I hope this works for you ;)

Data loss on View Class, when keyboard appears

When ever I click on textField and key board appears, my list of data i.e coming from API is vanashing,
import SwiftUI
import ExytePopupView
struct Wallet: View {
#State private var searchText = ""
#State private var showCancelButton: Bool = false
#ObservedObject var walltetVM = ShopViewModel()
#State var showShopDetails: Bool = false
#State var openShowDetails: Bool = false
#State var selectedShopId:String = ""
#State var selectedCouponDetail: CouponModel?
var body: some View {
NavigationView {
VStack {
// Search view
VStack{
HStack {
Button {
} label: {
Image(systemName: "magnifyingglass")
.foregroundColor(AppColors.semiBlue)
.frame(width: 20, height: 20, alignment: .center)
.padding()
}
ZStack(alignment: .leading) {
if searchText.isEmpty {
Text(MaggnetLocalizedString(key: "Restaurant, Beauty shop...", comment: ""))
.foregroundColor(AppColors.blackWhite)
.font(Font(AppFont.lightFont(lightFontWithSize: 15)))
}
TextField("", text: $searchText)
.font(Font(AppFont.regularFont(regularFontWithSize: 15)))
}
.foregroundColor(AppColors.blackWhite)
.padding(.horizontal,10)
.padding(.leading,-15)
Divider()
Button {
} label: {
HStack {
Image("places")
}
.padding(.horizontal,20)
.padding(.leading,-12)
}
}
.frame(height: 45)
.background(AppColors.fadeBackground)
.clipShape(Capsule())
}
.padding(.horizontal)
.padding(.vertical,10)
ScrollView(.vertical) {
VStack{
NavigationLink(destination:ShopDetail(shopId:self.selectedShopId)
.environmentObject(walltetVM),
isActive: $openShowDetails) {
EmptyView()
}.hidden()
Points()
ForEach(0..<walltetVM.finalsCouponList.count,id: \.self){
index in
VStack{
// SHOP LIST HEADERS
HStack{
Text(walltetVM.finalsCouponList[index].name)
.multilineTextAlignment(.leading)
.font(Font(AppFont.mediumFont(mediumFontWithSize: 15)))
.foregroundColor(AppColors.blackWhite)
.padding(.horizontal,10)
Spacer()
Button {
} label: {
Text(MaggnetLocalizedString(key: "viewAll", comment: ""))
.font(Font(AppFont.regularFont(regularFontWithSize: 12)))
.foregroundColor(AppColors.blackWhite)
.padding()
.frame(height: 27, alignment: .center)
.background(AppColors.fadeBackground)
.cornerRadius(8)
}
}
.padding()
// MAIN SHOP LIST
VStack{
ScrollView(.horizontal,showsIndicators: false){
HStack{
ForEach(0..<walltetVM.finalsCouponList[index].couopons.count,id: \.self){
indeX in
Shops(coupons: walltetVM.finalsCouponList[index].couopons[indeX])
.onTapGesture {
selectedShopId = walltetVM.finalsCouponList[index].couopons[indeX].businessId?.description ?? ""
print(selectedShopId)
selectedCouponDetail = walltetVM.finalsCouponList[index].couopons[indeX]
showShopDetails = true
}
}
}
.padding(.horizontal)
}
}
.padding(.top,-5)
}
.padding(.top,-5)
}
}
}
.blur(radius: showShopDetails ? 3 : 0)
.popup(isPresented: $showShopDetails, autohideIn: 15, dismissCallback: {
showShopDetails = false
}) {
ShopDetailPopUp(couponDeatil: self.selectedCouponDetail)
.frame(width: 300, height: 400)
}
.navigationBarTitle(Text("Wallet"),displayMode: .inline)
.navigationBarItems(trailing: HStack{
Button {
} label: {
Image("wishIcon").frame(width: 20, height: 20, alignment: .center)
}
Button {
} label: {
Image("notifIcon").frame(width: 20, height: 20, alignment: .center)
}
})
.resignKeyboardOnDragGesture()
}
.onAppear(){
walltetVM.getWallets()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.openShopDetails))
{ obj in
showShopDetails = false
openShowDetails.toggle()
}
.environment(\.layoutDirection,Preferences.chooseLanguage == AppConstants.arabic ? .rightToLeft : .leftToRight)
}
}
}
struct Wallet_Previews: PreviewProvider {
static var previews: some View {
Wallet()
}
}
bannerList is marked as #Published, API call working fine, but in same View class I have one search text field , when I tap on it all the data I was rendering from API get lost and disappears from list.
You are holding a reference to your view model using ObservedObject:
#ObservedObject var walltetVM = ShopViewModel()
However, because you are creating the view model within the view, the view might tear down and recreate this at any moment, which might be why you are losing your data periodically.
If you use StateObject, this ensures that SwiftUI will retain the object for as long as the view lives, so the object won't be recreated.
#StateObject var walltetVM = ShopViewModel()
Alternatively, you could create the view model outside of the view and inject it into the view and keep using ObservedObject (but you'll still need to make sure the object lives as long as the view).
I found something working but still not optimised solution
instead of calling it on onAppear, I called API function in init method.
init(){
walltetVM.getWallets()
}

how to appear a list (using animation) once the button is pressed?

I want once I press the button search
VStack{
Text("Enter friends first name")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(Color("Color"))
TextField("firstname", text: $firstname)
.padding()
.keyboardType(.default)
.background(Color.white)
.autocapitalization(.none)
.textFieldStyle(.roundedBorder)
.shadow(color: Color.gray.opacity(0.1), radius: 5, x: 0, y: 2)
Text("Enter friends last Name")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(Color("Color"))
TextField("lastname", text: $lastname)
.padding()
.keyboardType(.default)
.background(Color.white)
.autocapitalization(.none)
.textFieldStyle(.roundedBorder)
.shadow(color: Color.gray.opacity(0.1), radius: 5, x: 0, y: 2)
Button (action:{
searchUser()
},label:{
Text("Search")
})
}
the list that is in searchUser()that shows the names of friends with this first name and last name and their details appears on the this view under the search button and once the button is pressed but with animation ? thanks
I tried to do the animation but it didn't work. does anyone know how can I do it ?
You can show/hide views conditionally by putting them inside if block.
struct ContentView: View {
#State var shouldShowList = false
var body: some View {
VStack {
if shouldShowList {
VStack {
ForEach(0 ..< 5) { item in
Text("Hello, world!")
.padding()
}
}
}
Button( shouldShowList ? "Hide" : "Show") {
shouldShowList.toggle()
}
}
.animation(.easeInOut, value: shouldShowList) // animation
}
}
Instead,
You can use a view modifier to show/hide.
1. create your own ViewModifire
struct Show: ViewModifier {
let isVisible: Bool
#ViewBuilder
func body(content: Content) -> some View {
if isVisible {
EmptyView()
} else {
content
}
}
}
extension View {
func show(isVisible: Bool) -> some View {
ModifiedContent(content: self, modifier: Show(isVisible: isVisible))
}
}
Usage
struct ContentView: View {
#State var shouldShowList = false
var body: some View {
VStack {
VStack {
ForEach(0 ..< 5) { item in
Text("Hello, world!")
.padding()
}
}
.show(isVisible: shouldShowList) //<= here
Button( shouldShowList ? "Hide" : "Show") {
shouldShowList.toggle()
}
}
.animation(.easeInOut, value: shouldShowList) // animation
}
}

How to add more padding bellow a TextView when the keyboard is shown

When i have TextField inside ScrollView and tap on it the keyboard is shown as expected. But it seems that the TextField is moved up just enough to show the input area but i want to be moved enough so that is visible in its whole. Otherwise it looks cropped. I couldn't find a way to change this behaviour.
struct ContentView: View {
#State var text:String = ""
var body: some View {
ScrollView {
VStack(spacing: 10) {
ForEach(1...12, id: \.self) {
Text("\($0)…")
.frame(height:50)
}
TextField("Label..", text: self.$text)
.padding(10)
.background(.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 1)
)
}
.padding()
.background(.red)
}
}
}
Use a .safeAreaInset modifier.
#State var text:String = ""
var body: some View {
ScrollView {
VStack(spacing: 10) {
ForEach(1...12, id: \.self) {
Text("\($0)…")
.frame(height:50)
}
TextField("Label..", text: self.$text)
.padding(10)
.background(.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 1)
)
}
.padding()
.background(.red)
}.safeAreaInset(edge: .bottom) { //this will push the view when the keyboad is shown
Color.clear.frame(height: 30)
}
}
You can provide additional padding to the view (and it works even in iOS 13 and 14):
struct ContentView: View {
#State var text:String = ""
var body: some View {
ScrollView {
VStack(spacing: 10) {
ForEach(1...12, id: \.self) {
Text("\($0)…")
.frame(height:50)
}
TextField("Label..", text: self.$text)
.padding(10)
.background(.white)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 1)
)
.padding(.bottom, 32) //here, set as much pasding as you want
}
.padding()
.background(.red)
}
}
}

Buttons inside list item don't work properly

I have 2 views: PollCard and PollList (like list of polls)
In the PollCard view I have 2 buttons(images), that calls "answer" function:
HStack{
Button(action: {
self.answer()
print("Pressed first image")
}){
Image(poll.v1img)
.resizable()
.renderingMode(.original)
.scaledToFill()
.frame(width: 150, height: 200)
}.frame(width: 150, height: 200)
Button(action: { self.answer()}){
Image(poll.v2img )
.resizable()
.renderingMode(.original)
.frame(width: 150, height: 200)
}.frame(width: 150, height: 200).zIndex(4)
}
In the PollList view I have this simple list:
var body: some View {
HStack{
List(pollData) { poll in
PollCard(poll: poll)
}.padding()
}
}
But when I click the images in the list, it selects like all images and presses it
It is also very easy to check - terminal prints Pressed first image even if I've pressed only second image
What should I do to fix this?
As I mentioned in the comment section the workaround would be to substitute the HStack around the List with a ScrollView and the List with a ForEach:
struct ContentView: View {
struct Data: Identifiable {
var id: Int
}
#State var data = [Data(id: 0), Data(id: 1), Data(id: 2), Data(id: 3), Data(id: 4), Data(id: 5)]
var body: some View {
ScrollView {
ForEach(self.data) { data in
HStack {
Button(action: {
print("Pressed blue...")
}, label: {
Rectangle()
.foregroundColor(Color.blue)
.frame(width: 150, height: 200)
})
Button(action: {
print("Pressed red...")
}, label: {
Rectangle()
.foregroundColor(Color.red)
.frame(width: 150, height: 200)
})
}
}
}
}
}
I hope this helps!
this is an unexpected behavior of Button (or it is a bug?) in current SwiftUI (either on macOS or iOS).
The workaround in your case is simple, try to apply PlainButtonStyle for your buttons
import SwiftUI
struct ContentView: View {
var body: some View {
List {
Text("Hello, World!")
HStack {
Button(action: {
print("button1")
}) {
Color.yellow
}.buttonStyle(PlainButtonStyle())
Button(action: {
print("button2")
}) {
Color.green
}.buttonStyle(PlainButtonStyle())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The funny thing is that it is enough to apply the style on one button only ... or apply it to parent HStack :-)
changing ContentView ...
struct ContentView: View {
var body: some View {
List {
Text("Hello, World!")
HStack {
Button(action: {
print("button1")
}) {
Color.yellow
}
Button(action: {
print("button2")
}) {
Color.green
}
}
.buttonStyle(PlainButtonStyle())
.frame(height: 100)
Text("By by, World!")
}
}
}
you get
where each of buttons works as expected

Resources