How to avoid animation of HStack size changes in SwiftUI? - swiftui

I am trying to figure out how to suppress the animation of HStack size changes. Here is a stand-alone example which is a simplified version of my answer to another question.
This date picker uses 2 menus to allow for the selection of month and year. Just tap either to pop up a menu and select the new value.
I have added .animation(nil) to suppress the animations resulting from the size changes of the HStack when the month changes. Unfortunately this is now deprecated in IOS 15.
If you remove the .animation(nil) you will see the undesired animation.
Things I have tried:
I tried adding a value to the animation line:
animation(nil, value: pickedMonth)
I tried wrapping the setting of the pickedMonth in withAnimation(nil) { }
Neither of these stopped the animation.
How should this be done in iOS 15 and later?
import SwiftUI
struct ContentView: View {
let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
let years = 1990..<2025
#State private var pickedMonth = "May"
#State private var pickedYear = 2022
var body: some View {
HStack(spacing: 5) {
Menu {
ForEach(months, id: \.self) { month in
Button(month) {
// withAnimation(nil) {
pickedMonth = month
// }
}
}
} label: {
Text("\(pickedMonth),")
.font(.system(size: 24, weight: .semibold, design: .rounded))
.foregroundColor(.blue)
.fixedSize()
}
Menu {
ForEach(years, id: \.self) { year in
Button(String(year)) {
pickedYear = year
}
}
} label: {
Text(String(pickedYear))
.font(.system(size: 24, weight: .semibold, design: .rounded))
.foregroundColor(.blue)
.fixedSize()
}
}
.fixedSize()
.animation(nil)
//.animation(nil, value: pickedMonth)
}
}

Let me introduce you to my friend Transaction. Transactions allow you to intercept an already set animation and change it. The traditional way of using it is similar to withAnimation in which you enclose the animated value with withTransaction() closure. However, because you want the animation to be nil in a different view every time, you can use the .transaction view modifier form like this:
struct ContentView: View {
let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
let years = 1990..<2025
#State private var pickedMonth = "May"
#State private var pickedYear = 2022
var body: some View {
HStack(spacing: 5) {
Menu {
ForEach(months, id: \.self) { month in
Button(month) {
pickedMonth = month
}
}
} label: {
Text("\(pickedMonth),")
.font(.system(size: 24, weight: .semibold, design: .rounded))
.foregroundColor(.blue)
.fixedSize()
}
Menu {
ForEach(years, id: \.self) { year in
Button(String(year)) {
pickedYear = year
}
}
} label: {
Text(String(pickedYear))
.font(.system(size: 24, weight: .semibold, design: .rounded))
.foregroundColor(.blue)
.fixedSize()
}
}
.fixedSize()
// place it here
.transaction { hstackTransaction in // get the transaction
hstackTransaction.animation = nil // modify the transaction's animation
}
}
}
Regardless of the fact that .animation() compiles, it has serious issues which is why it is deprecated. I am not sure if .animation(nil) shares those issues, but you are right to want to purge it from your code.

Related

Cannot convert value of type '(Store).Type' to expected argument type 'Binding<C>'

I am trying to add items to an array in 1 view and then show that array in another view using a foreach loop but I keep getting these errors
Cannot convert value of type '(Store).Type' to expected argument type 'Binding<C>'
and Generic parameter 'C' could not be inferred
Here is my class - import SwiftUI
class StoreViewModel: ObservableObject{
#Published var item: [String] = []
#Published var amount: [String] = []
}
and here is where I try to show the items added
import SwiftUI
struct Store: View {
#State var presented: Bool = false
#State var showingAlert:Bool = false
#StateObject var store = StoreViewModel()
var body: some View {
ScrollView{
VStack{
HStack{
Button {
showingAlert = true
} label: {
Image(systemName: "trash")
.foregroundColor(Color.red)
.font(.system(size: 20))
}
.alert("Are you sure you want to remove all items", isPresented: $showingAlert) {
Button("OK", role: .destructive) {
withAnimation(.easeOut(duration: 0.3)){
}
}
}
Text("Your Cupboard")
.font(.system(size: 30))
.fontWeight(.semibold)
.padding()
Spacer()
Button {
presented.toggle()
} label: {
Image(systemName: "plus")
.font(.system(size: 30))
.foregroundColor(Color(hex: "FF0044"))
}
.fullScreenCover(isPresented: $presented, content: AddItem.init)
}
.padding(.horizontal)
Text("Here you can see what you have in your cupboard")
.padding()
.multilineTextAlignment(.center)
Text("Click the + to add an item")
.padding()
ForEach((Store), id: \.self){item in
HStack{
Spacer()
Text(item)
.font(.system(size: 20))
Text("30g")
Spacer()
Button {
} label: {
Image(systemName: "trash")
.foregroundColor(.red)
.font(.system(size: 15))
}
.padding(.trailing)
}
.padding()
}
}
}
.environmentObject(store)
}
}
What am I doing wrong and how can I overcome my errors?
Many Thanks for your time and help
Case sensitivity matters. You mean store, the StoreViewModel instance and not the type of self. Further the purpose of a ForEach expression is to iterate an array (presumably item(s)).
First of all to avoid more confusion rename item in StoreViewModel as its plural form
class StoreViewModel: ObservableObject{
#Published var items: [String] = []
#Published var amount: [String] = []
}
Then replace
ForEach((Store), id: \.self){ item in
with
ForEach(store.items, id: \.self){ item in

SwiftUI: scrolling on 3 wheel pickers inside a HStack is inaccurate

I have 3 wheel pickers inside a HStack to allow for selecting hours, minutes, and seconds.
Here's the playground code to help illustrate the issue:
import SwiftUI
import PlaygroundSupport
struct DurationPickers: View {
#State private var hourSelection = 0
#State private var minuteSelection = 0
#State private var secondSelection = 0
private let hoursArray = [Int](0..<24)
private let minutesArray = [Int](0..<61)
private let secondsArray = [Int](0..<61)
private let fullHeight: CGFloat = 256
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
HStack(spacing: 0) {
Text("Hours")
.frame(maxWidth: .infinity)
Text("Minutes")
.frame(maxWidth: .infinity)
Text("Seconds")
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity)
HStack(spacing: 0) {
Picker(selection: self.$hourSelection, label: Text("")) {
ForEach(0 ..< self.hoursArray.count, id:\.self) { index in
Text("\( self.hoursArray[index])").tag(index)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: geometry.size.width/3, alignment: .center)
Picker(selection: self.$minuteSelection, label: Text("")) {
ForEach(0 ..< self.minutesArray.count, id:\.self) { index in
Text("\( self.minutesArray[index])").tag(index)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: geometry.size.width/3, alignment: .center)
Picker(selection: self.self.$secondSelection, label: Text("")) {
ForEach(0 ..< self.secondsArray.count, id:\.self) { index in
Text("\( self.secondsArray[index])").tag(index)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: geometry.size.width/3, alignment: .center)
}
.frame(maxWidth: .infinity)
}
}
.frame(maxHeight: fullHeight)
.background(Color.gray)
}
}
struct ContentView: View {
var body: some View {
VStack {
Spacer()
DurationPickers()
}
.frame(width: 480, height: 600)
}
}
PlaygroundPage.current.setLiveView(ContentView())
The result of the code is this:
This looks exactly as I expect and want. However, when I try to select the hours for example, I have to scroll from the left half of the red area, otherwise minutes would start scrolling. The same happens if you scroll on minutes from the right half of the orange rectangle.
Any idea why it's behaving like this? Any suggestions to fix this?
In case someone else is having a similar issue, I just found a solution posted on Apple's forums two weeks ago.
Previously, this behaviour was fixed by adding:
.compositingGroup().clipped()
As suggested in some of Asperi's answers. However, this solution have stopped working (probably since the release of iOS 15.1, but maybe later).
The solution that works now on iOS 15.4 is adding the following extension:
extension UIPickerView {
open override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: super.intrinsicContentSize.height)
}
}
Not really sure what exactly is this doing and whether it might introduce other issues, but I'll go with it for now and keep testing!

SwiftUI Custom textfield with onEditingChanged closure

News to SwiftUI so please bare with me :) I have a custom textfield setup as a struct in a separate file.
I would like to use onEditingChanged in my content view (the closure would be perfect), is this possible? I have tried with a binding but it isn't a great solution.
import Foundation
import SwiftUI
struct EntryField: View {
var sfSymbolName: String
var placeHolder: String
var prompt: String
#Binding var field: String
var uptodate:Bool = false
var showActivityIndicator:Bool = false
#State var checkMarkOpacity = 0.9
#Binding var editingChanged: Bool
var body: some View {
VStack(alignment: .leading) {
HStack {
Image(systemName: sfSymbolName)
.foregroundColor(.gray)
.font(.custom("SF Pro Light", size: 60))
TextField(placeHolder, text: $field, onEditingChanged: { editing in
editingChanged = editing
}).autocapitalization(.none)
.keyboardType(.decimalPad)
.font(.custom("SF Pro Light", size: 60))
.placeholder(when: field.isEmpty) {
Text("0,00")
.foregroundColor(.white)
.font(.custom("SF Pro Light", size: 60))
}
Image(systemName: "checkmark").opacity(uptodate ? 1 : 0)
.opacity(checkMarkOpacity)
.foregroundColor(.white)
.font(.custom("SF Pro Light", size: 40))
.onAppear(perform: {
withAnimation(.easeIn(duration: 3.0).delay(2.0) ) {
checkMarkOpacity = 0
}
})
.overlay(
ProgressView()
.opacity(showActivityIndicator ? 1 : 0)
//.progressViewStyle(ShadowProgressViewStyle())
.progressViewStyle(RingProgressViewStyle(
configuration: .init(
trackColor: .blue,
fillColor: .white,
lineWidth: 6)))
)
}
.padding(8)
.background(Color(UIColor.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius:10))
Text(prompt)
.fixedSize(horizontal: false, vertical: true)
.font(.caption)
}
}
}
and in my content view I have:
#State private var editingChanged: Bool = false
EntryField(sfSymbolName: "eurosign.square", placeHolder: "", prompt: "Litre Price", field: $closestLitrePrice, uptodate: uptodate, showActivityIndicator: showActivityIndicator, editingChanged: $editingChanged)
thank you
If you don't need it internally then you can pass closure directly from construction to TextField, like
let editingChanged: (Bool) -> Void // << here !!
var body: some View {
VStack(alignment: .leading) {
HStack {
Image(systemName: sfSymbolName)
.foregroundColor(.gray)
.font(.custom("SF Pro Light", size: 60))
TextField(placeHolder, text: $field,
onEditingChanged: editingChanged) // << here !!
.autocapitalization(.none)

SwiftUI App crashes with a VStack around two pickers

I already found the solution, but still like to understand what the issue was to be able to transfer it to similar problems.
Take this example code:
import SwiftUI
struct ContentView: View {
private var days = Array(1...31)
#State private var selectedDay = 1
private var months = [ "January", "February", "March", "April", "May", "June" ]
#State private var selectedMonth = "January"
var body: some View {
NavigationView {
Form {
VStack {
Picker("Select day", selection: $selectedDay) {
ForEach(self.days, id: \.self) { day in
Text(String(day))
}
}
Picker("Select month", selection: $selectedMonth) {
ForEach(self.months, id: \.self) { month in
Text(month)
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you then tap on any of the pickers the application will crash after a few seconds with Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffeed371fd8).
The solution was to remove the VStack.
But I still like to understand why the application crashes if there is a VStack?
What’s wrong about adding a VStack?
Form is actually a List and every View in Form's view builder is put into row, so combining two picker into VStack result it putting two pickers into one row and when you tap on that row which picker list should be shown? ... unknown - thus this is a reason of crash.
If you want to combine such views in form use Section, like
Form {
Section {
Picker("Select day", selection: $selectedDay) {
ForEach(self.days, id: \.self) { day in
Text(String(day))
}
}
Picker("Select month", selection: $selectedMonth) {
ForEach(self.months, id: \.self) { month in
Text(month)
}
}
}
}

SwiftUI Multiple Pickers Unclickable

I have multiple Pickers in an HStack-->VStack. I want to create an hour, minute, seconds picker.
The way I have it setup now is what I am looking for however the 2nd and 3rd picker's do not allow for user interaction. Any ideas as to why?
struct TimePicker: View {
#State private var selectedHour = 0
#State private var selectedMin = 0
#State private var selectedSecond = 0
var body: some View {
HStack {
VStack {
Picker(selection: self.$selectedHour, label: Text("Hour")) {
ForEach(0..<24) { hour in
Text("\(hour) Hour")
}
}
}
.frame(minWidth: 100, maxWidth: .infinity)
.clipped()
.border(Color.blue)
VStack {
Picker(selection: self.$selectedMin, label: Text("Min")) {
ForEach(0..<61) { min in
Text("\(min) Min")
}
}
}
.frame(minWidth: 100, maxWidth: .infinity)
.clipped()
.border(Color.yellow)
VStack {
Picker(selection: self.$selectedSecond, label: Text("Sec")) {
ForEach(0..<61) { sec in
Text("\(sec) Sec")
}
}
}
.frame(minWidth: 100, maxWidth: .infinity)
.clipped()
.border(Color.purple)
}
}
}
Pickers are very buggy in Xcode 11.1 / iOS 13.1. Try upgrading and it should be resolved.