How can I display tabular data with SwiftUI on iPhone? - swiftui

I'm working on an app for my local sports league.
One view will be the current standings, with several fields on each row: Name of team, Games Played, Points, etc. I want the Teams column to be left-aligned, the other columns to be right-aligned.
It seems the best answer is SwiftUI's Table(), but on iPhone, it only displays the first column.
I've tried using an HStack{} with Text():
List {
ForEach(selectedStats()) { stats in
HStack {
Text (stats.name)
Spacer()
Text ("\(stats.totalMatches)")
Text ("\(stats.standingsPoints)")
}
}
}
but I'm having difficulty getting the columns to align left or right across multiple lines in the table.
Is there a way to get Tables to work? Or how can I align my columns?
(P. S. I used the tag TableView, because there is no tag specific to Table or SwiftUI-Table. It would be nice if that could be added.)

SwiftUI's Grid and GridRow are your friends for laying out simple content like this:
struct Stat: Identifiable, Equatable {
var id: String { name }
let name: String
let totalMatches: Int
let standingsPoints: Int
}
struct ContentView: View {
let stats = [
Stat(name: "Fred", totalMatches: 12, standingsPoints: 87),
Stat(name: "Jim", totalMatches: 4, standingsPoints: 12),
Stat(name: "Dave", totalMatches: 9, standingsPoints: 91)]
var body: some View {
List {
Grid {
GridRow {
Text("Name")
Text("Matches")
Text("Points")
}
.bold()
Divider()
ForEach(stats) { stat in
GridRow {
Text(stat.name)
Text(stat.totalMatches, format: .number)
Text(stat.standingsPoints, format: .number)
}
if stat != stats.last {
Divider()
}
}
}
}
}
}
You can tweak the layout by adding Spacer()s .gridCellAnchor modifiers, e.g.
var body: some View {
List {
Grid {
GridRow {
Text("Name"). // UnitPoint(x: 1, y: 0.5) = align right
.gridCellAnchor(UnitPoint(x: 1, y: 0.5))
Spacer()
Text("Matches") // UnitPoint(x: 0, y: 0.5) = align left
.gridCellAnchor(UnitPoint(x: 0, y: 0.5))
Text("Points")
.gridCellAnchor(UnitPoint(x: 0, y: 0.5))
}
.bold()
Divider()
ForEach(stats) { stat in
GridRow {
Text(stat.name)
.gridCellAnchor(UnitPoint(x: 1, y: 0.5))
Spacer()
Text(stat.totalMatches, format: .number)
.gridCellAnchor(UnitPoint(x: 0, y: 0.5))
Text(stat.standingsPoints, format: .number)
.gridCellAnchor(UnitPoint(x: 0, y: 0.5))
}
if stat != stats.last {
Divider()
}
}
}
}
}

I kept researching after I posted and came across some stuff by Paul Hudson of Hacking with Swift fame.
Alignment and alignment guides
How to create a custom alignment guide
Based on those two, I came up with this:
extension HorizontalAlignment {
enum GP: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[.trailing]
}
}
static let gp = HorizontalAlignment(GP.self)
}
and then I apply
.alignmentGuide(.gp) { d in d[HorizontalAlignment.trailing] }
to each element I want to right-align.
Seems to do the trick, as well.

Related

Swift Charts: Prevent horizontal bar marks from overlapping Y axis label

Pretty basic code for a horizontal bar chart using Apple's Swift Charts:
private var testData: [(String, Int)] = []
init() {
for i in (0 ..< 10).reversed() {
testData.append(
("Item\(i)", i + 3)
)
}
}
var body: some View {
Chart(testData, id: \.0) { item in
BarMark(
x: .value("x", item.1),
y: .value("y", item.0)
)
.foregroundStyle(Color.red)
.annotation(
position: .overlay,
alignment: .trailing
) {
Text("\(item.1)")
.foregroundColor(.white)
.fontWeight(.bold)
.padding(.trailing)
}
}
.chartXAxis(.hidden)
.chartYAxis {
AxisMarks(
position: .trailing
) { value in
AxisValueLabel(centered: true) {
if let stringValue = value.as(String.self) {
Text(stringValue)
.font(.title2)
}
}
}
}
}
With this result but I can't seem to prevent the chart from overlapping the Y axis label:
Even if I align the label with position: .leading it's still hidden under the chart. Any help would be appreciated.

SwiftUI - offset wrong or weird padding?

I'm creating a schedule app and im trying to render the activities on a calendar sort of thing. Im doing this by having a set spacing of 20 between every hour mark. This means that to position the activities I can take the hour the activity start times this set height of 20 to position it correctly. This does indeed work but when I add multiple activities the offset is wrong? But if I render every activity on its own the offset is correct.
This is the code for displaying the activities.
struct Event: Hashable {
var name: String
var start: Int
var end: Int
var y: CGFloat
var eHeight: CGFloat
var color: Color
}
struct EventIndicatorView: View {
var day = [Event]()
init() {
day.append(createEvent(name: "Matematik", start: 8, end: 9, color: Color.blue))
day.append(createEvent(name: "Svenska", start: 10, end: 11, color: Color.green))
day.append(createEvent(name: "Fysik", start: 12, end: 14, color: Color.red))
day.append(createEvent(name: "Idrott", start: 15, end: 16, color: Color.pink))
}
func createEvent(name: String, start: Int, end: Int, color: Color) -> Event {
let eHeight = CGFloat((end - start) * height * 2)
//let y = CGFloat((start*height*2) + height)
let y = CGFloat((start*2*height)+height*(end-start))
let e = Event(name: name, start: start, end: end, y: y, eHeight: eHeight, color: color)
return e
}
var body: some View {
VStack(spacing: 0) {
ForEach(day, id: \.self) { d in
HStack() {
VStack(spacing: 0) {
HStack(spacing: 0) {
Rectangle()
.fill(d.color)
.frame(width:5, height: d.eHeight)
ZStack(alignment: .topLeading) {
Rectangle()
.fill(d.color.opacity(0.5))
.frame(height: d.eHeight)
Text(d.name)
.padding(EdgeInsets(top: 4, leading: 4, bottom: 0, trailing: 0))
.foregroundColor(d.color)
.font(.subheadline.bold())
}
}
.cornerRadius(4)
.offset(x: 60, y: d.y)
}
}
.frame(height: CGFloat(height))
}
Spacer()
}
}
}
This is the code for showing the hours.
var hours = [String]()
struct CalenderView: View {
init() {
for hour in 0...24 {
if hour == 24 {
hours.append("00:00")
return
}
if hour <= 9 {
hours.append("0\(hour):00")
} else {
hours.append("\(hour):00")
}
}
}
var body: some View {
VStack(spacing: CGFloat(height)) {
ForEach(hours.indices, id:\.self) {index in
HStack(spacing: 10) {
Text(hours[index])
.frame(width: 40, height: CGFloat(height))
.font(.caption.bold())
.foregroundColor(Color(.systemGray))
Rectangle()
.fill(Color(.systemGray4))
.frame(height: 1)
}
.padding(.leading, 10)
}
}
}
}
This code results in this:
Display of multiple acctiviys
The green one should be positioned on 10 but isn't.
Here is a picture where I only display one activity:
Only one activity
But if I only display the green one its position is correct

Positioning Views in ForEach SwiftUI

I would like to add animating views to a parent view. I know that the parent view needs to position the children but I'm having trouble coming up with the formula to implement. I have the first couple of views right but once I get to 4 and up its a problem! I would like the views to appear in a grid with 3 columns.
Here is some reproducible code ready to be copy and pasted.
import SwiftUI
struct CustomView: View, Identifiable {
#State private var startAnimation = false
let id = UUID()
var body: some View {
Circle()
.frame(width: 50, height: 50)
.scaleEffect(x: startAnimation ? 2 : 1,
y: startAnimation ? 2 : 1)
.animation(Animation.interpolatingSpring(mass: 2, stiffness: 20, damping: 1, initialVelocity: 1))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.startAnimation = true
}
}
}
}
struct StartView: View {
#State private var userSelection: [CustomView] = []
var body: some View {
VStack(spacing: -20) {
Button("Add View") {
self.userSelection.append(CustomView())
}
LazyVGrid(columns: gridStyle) {
ForEach(Array(userSelection.enumerated()), id: \.0 ){ index, equip in
CustomView()
.position(x: widthBasedOn(index: index), y: heightBasedOn(index: index))
}
.padding([])
}
.frame(width: UIScreen.main.bounds.width * 0.5,
height: UIScreen.main.bounds.height * 0.8)
}
}
let gridStyle = [
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50),
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50),
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50)
]
private func widthBasedOn(index: Int) -> CGFloat {
if index % 3 != 0 {
if index > 3 {
let difference = index - 4
return CGFloat(index * difference * 100)
}
let answer = CGFloat(index * 100)
print("\(index) width should be: \(answer)")
return answer
}
return 0
}
private func heightBasedOn(index: Int) -> CGFloat {
if index > 3 && index < 6 {
return 100
}
return 200
}
}
struct EquipmentSelectionView_Previews: PreviewProvider {
static var previews: some View {
StartView()
}
}
Since most of your question is somewhat vague, and I am not sure about the specifics, this is my solution. Feel free to respond, and I will be glad to answer your question further with more tailored solution.
I removed many of your code that was unnecessary or overly-complicated. For example, I removed the widthBasedOn and heightBasedOn methods. I also changed the array property var userSelection: [CustomView] to var numberOfViews = 0.
Note: Both your original code and my solution cause all the circles to wiggle up and down, whenever a new circle is added.
I suggest that you copy paste this code snippet, run it in Xcode, and see if this is what you want.
struct CustomView: View, Identifiable {
#State private var startAnimation = false
let id = UUID()
var body: some View {
Circle()
//Changing the frame size of the circle, making it bigger or smaller
.frame(width: startAnimation ? 100 : 50, height: startAnimation ? 100 : 50)
.animation(Animation.interpolatingSpring(mass: 2, stiffness: 20, damping: 1, initialVelocity: 1))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.startAnimation = true
}
}
}
}
struct StartView: View {
//View will display this number of circles
#State private var numberOfViews = 0
var body: some View {
VStack() {
Button("Add View") {
self.numberOfViews += 1
}
.padding(.top, 100)
Spacer()
LazyVGrid(columns: gridStyle) {
//Add a new circle CustomView() to the LazyVGrid for each number of views
ForEach(0..<numberOfViews, id: \.self ){view in
CustomView()
}
}
}
}
//3 columns, flexible spacing for elments. In this case, equal amount of spacing.
let gridStyle = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
]
}
struct EquipmentSelectionView_Previews: PreviewProvider {
static var previews: some View {
StartView()
}
}
Limiting number of circles
To limit the number of circles:
if numberOfViews < 9 {
self.numberOfViews += 1
}
Positioning the button
To position the button, you can add padding:
Button("Add View") {
if numberOfViews < 9 {
self.numberOfViews += 1
}
}
.padding(.top, 100)
Overlap vs. No Overlap
Using there .frame modifier will not have any overlap:
.frame(width: startAnimation ? 100 : 50, height: startAnimation ? 100 : 50)
But if you do want overlap, use .scaleEffect:
.scaleEffect(x: startAnimation ? 2 : 1,
y: startAnimation ? 2 : 1)
P.S. Unfortunately, I can't show you the results with GIF images because Stackoverflow keep giving me upload errors.

Positioning View using anchor point

I have several dozen Texts that I would like to position such that their leading baseline (lastTextBaseline) is at a specific coordinate. position can only set the center. For example:
import SwiftUI
import PlaygroundSupport
struct Location: Identifiable {
let id = UUID()
let point: CGPoint
let angle: Double
let string: String
}
let locations = [
Location(point: CGPoint(x: 54.48386479999999, y: 296.4645408), angle: -0.6605166885682314, string: "Y"),
Location(point: CGPoint(x: 74.99159120000002, y: 281.6336352), angle: -0.589411952788817, string: "o"),
]
struct ContentView: View {
var body: some View {
ZStack {
ForEach(locations) { run in
Text(verbatim: run.string)
.font(.system(size: 48))
.border(Color.green)
.rotationEffect(.radians(run.angle))
.position(run.point)
Circle() // Added to show where `position` is
.frame(maxWidth: 5)
.foregroundColor(.red)
.position(run.point)
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
This locates the strings such that their center is at the desired point (marked as a red circle):
I would like to adjust this so that the leading baseline is at this red dot. In this example, a correct layout would move the glyphs up and to the right.
I have tried adding .topLeading alignment to the ZStack, and then using offset rather than position. This will let me align based on the top-leading corner, but that's not the corner I want to layout. For example:
ZStack(alignment: .topLeading) { // add alignment
Rectangle().foregroundColor(.clear) // to force ZStack to full size
ForEach(locations) { run in
Text(verbatim: run.string)
.font(.system(size: 48))
.border(Color.green)
.rotationEffect(.radians(run.angle), anchor: .topLeading) // rotate on top-leading
.offset(x: run.point.x, y: run.point.y)
}
}
I've also tried changing the "top" alignment guide for the Texts:
.alignmentGuide(.top) { d in d[.lastTextBaseline]}
This moves the red dots rather than the text, so I don't believe this is on the right path.
I am considering trying to adjust the locations themselves to take into account the size of the Text (which I can predict using Core Text), but I am hoping to avoid calculating a lot of extra bounding boxes.
So, as far as I can tell, alignment guides can't be used in this way – yet. Hopefully this will be coming soon, but in the meantime we can do a little padding and overlay trickery to get the desired effect.
Caveats
You will need to have some way of retrieving the font metrics – I'm using CTFont to initialise my Font instances and retrieving metrics that way.
As far as I can tell, Playgrounds aren't always representative of how a SwiftUI layout will be laid out on the device, and certain inconsistencies arise. One that I've identified is that the displayScale environment value (and the derived pixelLength value) is not set correctly by default in playgrounds and even previews. Therefore, you have to set this manually in these environments if you want a representative layout (FB7280058).
Overview
We're going to combine a number of SwiftUI features to get the outcome we want here. Specifically, transforms, overlays and the GeometryReader view.
First, we'll align the baseline of our glyph to the baseline of our view. If we have the font's metrics we can use the font's 'descent' to shift our glyph down a little so it sits flush with the baseline – we can use the padding view modifier to help us with this.
Next, we're going to overlay our glyph view with a duplicate view. Why? Because within an overlay we're able to grab the exact metrics of the view underneath. In fact, our overlay will be the only view the user sees, the original view will only be utilised for its metrics.
A couple of simple transforms will position our overlay where we want it, and we'll then hide the view that sits underneath to complete the effect.
Step 1: Set up
First, we're going to need some additional properties to help with our calculations. In a proper project you could organise this into a view modifier or similar, but for conciseness we'll add them to our existing view.
#Environment(\.pixelLength) var pixelLength: CGFloat
#Environment(\.displayScale) var displayScale: CGFloat
We'll also need a our font initialised as a CTFont so we can grab its metrics:
let baseFont: CTFont = {
let desc = CTFontDescriptorCreateWithNameAndSize("SFProDisplay-Medium" as CFString, 0)
return CTFontCreateWithFontDescriptor(desc, 48, nil)
}()
Then some calculations. This calculates some EdgeInsets for a text view that will have the effect of moving the text view's baseline to the bottom edge of the enclosing padding view:
var textPadding: EdgeInsets {
let baselineShift = (displayScale * baseFont.descent).rounded(.down) / displayScale
let baselineOffsetInsets = EdgeInsets(top: baselineShift, leading: 0, bottom: -baselineShift, trailing: 0)
return baselineOffsetInsets
}
We'll also add a couple of helper properties to CTFont:
extension CTFont {
var ascent: CGFloat { CTFontGetAscent(self) }
var descent: CGFloat { CTFontGetDescent(self) }
}
And finally we create a new helper function to generate our Text views that uses the CTFont we defined above:
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
}
Step 2: Adopt our glyphView(_:) in our main body call
This step is simple and has us adopt the glyphView(_:) helper function we define above:
var body: some View {
ZStack {
ForEach(locations) { run in
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.position(run.point)
Circle() // Added to show where `position` is
.frame(maxWidth: 5)
.foregroundColor(.red)
.position(run.point)
}
}
}
This gets us here:
Step 3: Baseline shift
Next we shift the baseline of our text view so that it sits flush with the bottom of our enclosing padding view. This is just a case of adding a padding modifier to our new glyphView(_:)function that utilises the padding calculation we define above.
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
.padding(textPadding) // Added padding modifier
}
Notice how the glyphs are now sitting flush with the bottom of their enclosing views.
Step 4: Add an overlay
We need to get the metrics of our glyph so that we are able to accurately place it. However, we can't get those metrics until we've laid out our view. One way around this is to duplicate our view and use one view as a source of metrics that is otherwise hidden, and then present a duplicate view that we position using the metrics we've gathered.
We can do this with the overlay modifier together with a GeometryReader view. And we'll also add a purple border and make our overlay text blue to differentiate it from the previous step.
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.blue)
.border(Color.purple, width: self.pixelLength)
})
.position(run.point)
Step 5: Translate
Making use of the metrics we now have available for us to use, we can shift our overlay up and to the right so that the bottom left corner of the glyph view sits on our red positioning spot.
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.blue)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
})
.position(run.point)
Step 6: Rotate
Now we have our view in position we can finally rotate.
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.blue)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
.rotationEffect(.radians(run.angle))
})
.position(run.point)
Step 7: Hide our workings out
Last step is to hide our source view and set our overlay glyph to its proper colour:
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.hidden()
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.black)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
.rotationEffect(.radians(run.angle))
})
.position(run.point)
The final code
//: A Cocoa based Playground to present user interface
import SwiftUI
import PlaygroundSupport
struct Location: Identifiable {
let id = UUID()
let point: CGPoint
let angle: Double
let string: String
}
let locations = [
Location(point: CGPoint(x: 54.48386479999999, y: 296.4645408), angle: -0.6605166885682314, string: "Y"),
Location(point: CGPoint(x: 74.99159120000002, y: 281.6336352), angle: -0.589411952788817, string: "o"),
]
struct ContentView: View {
#Environment(\.pixelLength) var pixelLength: CGFloat
#Environment(\.displayScale) var displayScale: CGFloat
let baseFont: CTFont = {
let desc = CTFontDescriptorCreateWithNameAndSize("SFProDisplay-Medium" as CFString, 0)
return CTFontCreateWithFontDescriptor(desc, 48, nil)
}()
var textPadding: EdgeInsets {
let baselineShift = (displayScale * baseFont.descent).rounded(.down) / displayScale
let baselineOffsetInsets = EdgeInsets(top: baselineShift, leading: 0, bottom: -baselineShift, trailing: 0)
return baselineOffsetInsets
}
var body: some View {
ZStack {
ForEach(locations) { run in
self.glyphView(for: run.string)
.border(Color.green, width: self.pixelLength)
.hidden()
.overlay(GeometryReader { geometry in
self.glyphView(for: run.string)
.foregroundColor(.black)
.border(Color.purple, width: self.pixelLength)
.transformEffect(.init(translationX: geometry.size.width / 2, y: -geometry.size.height / 2))
.rotationEffect(.radians(run.angle))
})
.position(run.point)
Circle() // Added to show where `position` is
.frame(maxWidth: 5)
.foregroundColor(.red)
.position(run.point)
}
}
}
private func glyphView(for text: String) -> some View {
Text(verbatim: text)
.font(Font(baseFont))
.padding(textPadding)
}
}
private extension CTFont {
var ascent: CGFloat { CTFontGetAscent(self) }
var descent: CGFloat { CTFontGetDescent(self) }
}
PlaygroundPage.current.setLiveView(
ContentView()
.environment(\.displayScale, NSScreen.main?.backingScaleFactor ?? 1.0)
.frame(width: 640, height: 480)
.background(Color.white)
)
And that's it. It's not perfect, but until SwiftUI gives us an API that allows us to use alignment anchors to anchor our transforms, it might get us by!
this code takes care of the font metrics, and position text as you asked
(If I properly understood your requirements :-))
import SwiftUI
import PlaygroundSupport
struct BaseLine: ViewModifier {
let alignment: HorizontalAlignment
#State private var ref = CGSize.zero
private var align: CGFloat {
switch alignment {
case .leading:
return 1
case .center:
return 0
case .trailing:
return -1
default:
return 0
}
}
func body(content: Content) -> some View {
ZStack {
Circle().frame(width: 0, height: 0, alignment: .center)
content.alignmentGuide(VerticalAlignment.center) { (d) -> CGFloat in
DispatchQueue.main.async {
self.ref.height = d[VerticalAlignment.center] - d[.lastTextBaseline]
self.ref.width = d.width / 2
}
return d[VerticalAlignment.center]
}
.offset(x: align * ref.width, y: ref.height)
}
}
}
struct ContentView: View {
var body: some View {
ZStack {
Cross(size: 20, color: Color.red).position(x: 200, y: 200)
Cross(size: 20, color: Color.red).position(x: 200, y: 250)
Cross(size: 20, color: Color.red).position(x: 200, y: 300)
Cross(size: 20, color: Color.red).position(x: 200, y: 350)
Text("WORLD").font(.title).border(Color.gray).modifier(BaseLine(alignment: .trailing))
.rotationEffect(.degrees(45))
.position(x: 200, y: 200)
Text("Y").font(.system(size: 150)).border(Color.gray).modifier(BaseLine(alignment: .center))
.rotationEffect(.degrees(45))
.position(x: 200, y: 250)
Text("Y").font(.system(size: 150)).border(Color.gray).modifier(BaseLine(alignment: .leading))
.rotationEffect(.degrees(45))
.position(x: 200, y: 350)
Text("WORLD").font(.title).border(Color.gray).modifier(BaseLine(alignment: .leading))
.rotationEffect(.degrees(225))
.position(x: 200, y: 300)
}
}
}
struct Cross: View {
let size: CGFloat
var color = Color.clear
var body: some View {
Path { p in
p.move(to: CGPoint(x: size / 2, y: 0))
p.addLine(to: CGPoint(x: size / 2, y: size))
p.move(to: CGPoint(x: 0, y: size / 2))
p.addLine(to: CGPoint(x: size, y: size / 2))
}
.stroke().foregroundColor(color)
.frame(width: size, height: size, alignment: .center)
}
}
PlaygroundPage.current.setLiveView(ContentView())
Updated: you could try the following variants
let font = UIFont.systemFont(ofSize: 48)
var body: some View {
ZStack {
ForEach(locations) { run in
Text(verbatim: run.string)
.font(Font(self.font))
.border(Color.green)
.offset(x: 0, y: -self.font.lineHeight / 2.0)
.rotationEffect(.radians(run.angle))
.position(run.point)
Circle() // Added to show where `position` is
.frame(maxWidth: 5)
.foregroundColor(.red)
.position(run.point)
}
}
}
there is also next interesting variant, use ascender instead of above lineHeight
.offset(x: 0, y: -self.font.ascender / 2.0)

SwiftUI: Add animation to Picker without adding it to the values

I'm trying to animate the hiding/showing of a picker based on a toggle. If true or false, I would like the picker to easeInOut.
I've tried adding the .animation(Animation.easeInOut(duration: 0.5)) to the picker itself or the HStack the picker is in, but both add the animation to values inside the picker and when scrolling through the values the application to crashes.
HStack {
if showPicker {
Picker(selection: $selected.value, label: Text(selected.type)) {
ForEach(digits, id: \.self) { d in
Text("\(d)")
}
}
.frame(width: 40)
}
}
.animation(Animation.easeInOut(duration: 2))
if showPicker {
Picker(selection: $selected.value, label: Text(selected.type)) {
ForEach(digits, id: \.self) { d in
Text("\(d)")
}
}
.frame(width: 40)
.animation(Animation.easeInOut(duration: 0.5))
}
Both options do animate the hiding/showing the picker, but it also animates scrolling through the values in the picker, which causes it to crash.
Any help would be appreciated.
Thank you
About your first approach, putting animation on HStack. Never do that. According to the comments in the declaration file:
Use this modifier on leaf views rather than container views. The
animation applies to all child views within this view; calling
animation(_:) on a container view can lead to unbounded scope.
I tried your second approach (filling the missing bits from your post), and it won't crash. Maybe you can update your question with a fully reproducible example.
Changed animation to explicit, so other parameters are not affected:
struct PickerStackOverflow: View {
#State private var showPicker = true
#State private var value: Int = 1
let digits: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
var body: some View {
VStack {
Text("Selected \(value)")
HStack {
if showPicker {
Picker(selection: $value, label: Text("Label")) {
ForEach(digits, id: \.self) { d in
Text("\(d)")
}
}
.frame(width: 40)
}
}
Button("Tap Me") {
withAnimation(Animation.easeInOut(duration: 2)) {
self.showPicker.toggle()
}
}
}
}
}