SwiftUI Charts cutting off last value with centered AxisValueLabel - swift

I am messing with the new swift charts library and its alignment is a little weird for the x values.
When i specify AxisValueLabel(centered: true) the last value gets cut off(December in this case).
If I dont specify center=true, then the x value has leading alignment and it just looks off.
code for graph
struct graphOne: View {
#ObservedObject var vm: CVVM
#State private var position = 30
var frameWidth = UIScreen.screenWidth
var body: some View {
Chart {
ForEach(vm.currentUsage) { empRes in
BarMark(
x: .value("Month", empRes.date, unit: vm.filter),
y: .value("energy", empRes.energyUsage)
)
//.foregroundStyle(.green)
.annotation(position: .top) {
Text("\(empRes.energyUsage)")
.font(.caption)
}
.cornerRadius(5)
}
}
.chartXAxis {
AxisMarks(preset: .aligned, values: vm.currentUsage.map{ $0.date}) { date in
AxisValueLabel(format: vm.chartType == "months" ? .dateTime.month() : .dateTime.day(), centerd: true)
}
}
.chartYAxis {
AxisMarks(position: .leading)
}
.padding()
.frame(width: frameWidth )
}
}

Related

How to set columns of "ForEach" next to eachOthers

I'm trying to put some data from "ForEach" side by side but I don't know how to do it in a right way...
This is what've got
import SwiftUI
struct ContentView: View {
#StateObject private var vm = notaViewModel()
#State var puntajeMaximo: String
#State var notaMaxima: String
#State var notaMinima: String
#State var notaAprobacion: String
#State var notaExigencia: String
var body: some View {
ScrollView {
// puntajeMaximo = Maximun score you can get
ForEach(0...vm.puntajeMaximo, id: \.self) { score in
HStack {
if vm.puntajeMaximo / 2 >= score {
Text("\(score) -> \(vm.getAverage(puntos: Float(score)))")
.frame(width: 100, height: 50)
.background(Color("textFieldBackground"))
.cornerRadius(5)
.foregroundColor(.red)
}
if vm.puntajeMaximo / 2 < score {
Text("\(score) -> \(vm.getAverage(puntos: Float(score)))")
.frame(width: 100, height: 50)
.background(Color("textFieldBackground"))
.cornerRadius(5)
}
}
}
}
.onAppear {
vm.setParameters(pMax: puntajeMaximo, nMaxima: notaMaxima, nMinima: notaMinima, nAprobacion: notaAprobacion, nExigencia: notaExigencia)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(puntajeMaximo: "30", notaMaxima: "70", notaMinima: "10", notaAprobacion: "40", notaExigencia: "60")
}
}
As you can see, I have a column with numbers (score) from 0 to 30 and a Float next to it, the first 15 are red and I need them to be at the left and the others, from 15 to 30 to be at the right. I've been trying with HStack, Vstack, Grid and cannot get the answer
Please help, this is driving me crazy
1. Why you have taken String instead of Number when you need to compare the value.
2. We have two different way which I have shared below from which 2 one is appropriate answer according to me still you can choose any one as per your requirement.
ANSWER 1
var body: some View {
HStack{
VStack{
ScrollView(showsIndicators: false) {
ForEach(0...puntajeMaximo, id: \.self) { score in
HStack {
if puntajeMaximo / 2 >= score {
Text("\(score) -> \(score).\(score)")
.frame(width: 100, height: 50)
.background(.white)
.cornerRadius(10)
.foregroundColor(.red)
}
}
}
}
}.frame(width: UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.height - 100)
.background(Color.orange)
VStack{
ScrollView {
ForEach(0...puntajeMaximo, id: \.self) { score in
HStack {
if puntajeMaximo / 2 < score {
Text("\(score) -> \(score).\(score)")
.frame(width: 100, height: 50)
.background(.white)
.cornerRadius(12)
}
}
}
}
}.frame(width: UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.height)
.background(Color.yellow)
}
}
Answer 1 Output Image
Here you can check scroll hidden and some other difference in two scrollview
ANSWER 2 (Preferred)
// Need to create Item in List which requires to conform Identifiable protocol
struct ListItem: Identifiable {
let id = UUID()
let score: Int
}
struct ContentViews: View {
// #StateObject private var vm = notaViewModel()
#State var puntajeMaximo: Int
init(puntajeMaximo: Int) {
self.puntajeMaximo = puntajeMaximo
getData()
}
var leftData: [ListItem] = []
var rightData: [ListItem] = []
var body: some View {
HStack{
VStack{
List(leftData) { data in
Text("\(data.score) -> \(data.score).\(data.score)")
.frame(width: 100, height: 50)
.background(.gray.opacity(0.3))
.cornerRadius(10)
.foregroundColor(.red)
.listRowBackground(Color.clear)
}
}.frame(width: UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.height - 100)
.background(Color.orange)
VStack{
List(rightData) { data in
Text("\(data.score) -> \(data.score).\(data.score)")
.frame(width: 100, height: 50)
.background(.gray.opacity(0.3))
.cornerRadius(10)
.foregroundColor(.black)
}
}.frame(width: UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.height)
.background(Color.yellow)
}
}
// Must be called in init or before rendering the view
mutating func getData() {
for score in 0...puntajeMaximo {
if (puntajeMaximo / 2 >= score) {
leftData.append(ListItem(score: score))
} else if puntajeMaximo / 2 < score {
rightData.append(ListItem(score: score))
}
}
}
}
struct ContentViews_Previews: PreviewProvider {
static var previews: some View {
ContentViews(puntajeMaximo: 30)
}
}
Answer 2 Output Image
Required
A mutating method that separates the list for left and right view
A struct that conforms Identifiable protocol to pass in ListView as a row.
You can make changes to design as per you requirement.
As this method runs loop only 1 time and separates the list instead of Answer 1 which runs loop twice for both view

SwiftUI - Dynamic LazyHGrid row height

I'm creating vertical layout which has scrollable horizontal LazyHGrid in it. The problem is that views in LazyHGrid can have different heights (primarly because of dynamic text lines) but the grid always calculates height of itself based on first element in grid:
What I want is changing size of that light red rectangle based on visible items, so when there are smaller items visible it should look like this:
and when there are bigger items it should look like this:
This is code which results in state on the first image:
struct TestView: PreviewProvider {
static var previews: some View {
ScrollView {
VStack {
Color.blue
.frame(height: 100)
ScrollView(.horizontal) {
LazyHGrid(
rows: [GridItem()],
alignment: .top,
spacing: 16
) {
Color.red
.frame(width: 64, height: 24)
ForEach(Array(0...10), id: \.self) { value in
Color.red
.frame(width: 64, height: CGFloat.random(in: 32...92))
}
}.padding()
}.background(Color.red.opacity(0.3))
Color.green
.frame(height: 100)
}
}
}
}
Something similar what I want can be achieved by this:
extension View {
func readSize(edgesIgnoringSafeArea: Edge.Set = [], onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
SwiftUI.Color.clear
.preference(key: ReadSizePreferenceKey.self, value: geometryProxy.size)
}.edgesIgnoringSafeArea(edgesIgnoringSafeArea)
)
.onPreferenceChange(ReadSizePreferenceKey.self) { size in
DispatchQueue.main.async { onChange(size) }
}
}
}
struct ReadSizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
struct Size: Equatable {
var height: CGFloat
var isValid: Bool
}
struct TestView: View {
#State private var sizes = [Int: Size]()
#State private var height: CGFloat = 32
static let values: [(Int, CGFloat)] =
(0...3).map { ($0, CGFloat(32)) }
+ (4...10).map { ($0, CGFloat(92)) }
var body: some View {
ScrollView {
VStack {
Color.blue
.frame(height: 100)
ScrollView(.horizontal) {
LazyHGrid(
rows: [GridItem(.fixed(height))],
alignment: .top,
spacing: 16
) {
ForEach(Array(Self.values), id: \.0) { value in
Color.red
.frame(width: 300, height: value.1)
.readSize { sizes[value.0]?.height = $0.height }
.onAppear {
if sizes[value.0] == nil {
sizes[value.0] = Size(height: .zero, isValid: true)
} else {
sizes[value.0]?.isValid = true
}
}
.onDisappear { sizes[value.0]?.isValid = false }
}
}.padding()
}.background(Color.red.opacity(0.3))
Color.green
.frame(height: 100)
}
}.onChange(of: sizes) { sizes in
height = sizes.filter { $0.1.isValid }.map { $0.1.height }.max() ?? 32
}
}
}
... but as you see its kind of laggy and a little bit complicated, isn't there better solution? Thank you everyone!
The height of a row in a LazyHGrid is driven by the height of the tallest cell. According to the example you provided, the data source will only show a smaller height if it has only a small size at the beginning.
Unless the first rendering will know that there are different heights, use the larger value as the height.
Is your expected UI behaviour that the height will automatically switch? Or use the highest height from the start.

How to get bounce animation in SwiftUI?

How do I achieve the bounce animation similar to the one on macOS dock when an application needs attention and it bounces on the dock. SwiftUI seems to only have ease-curves and spring, which don't really emphasize the way a bounce does.
I've attempted various spring animations and combinations of ease curves and timingCurves to try and get the bounce animation working but nothing really did the job.
The closest to a bounce animation is interpolating spring but the main problem with these animations is that they overshoot during the animation, whereas bounce animations don't.
struct ContentView: View {
#State var bounce = false
#State private var initialVelocity:Double = 1
#State private var damping:Double = 1
#State private var stiffness:Double = 1
var body: some View {
VStack {
Circle().fill(Color.red).frame(width:50,height:50)
.offset(y: bounce ? 0 : -80)
.animation(.interpolatingSpring(stiffness: self.stiffness, damping: self.damping, initialVelocity: self.initialVelocity))
HStack(){
Text("stiffness")
Slider(value: $stiffness, in: 0...100)
}
HStack(){
Text("damping")
Slider(value: $damping, in: 0...100)
}
HStack(){
Text("initialVelocity")
Slider(value: $initialVelocity, in: 0...100)
}
Button("Animate" ){
self.bounce.toggle()
}
}.padding(.horizontal)
}
}
What I'm looking for is a bounce animation that replicates gravity, it's a pretty common animation that is available in a lot of games and software
What's about .interpolatingSpring(...)?
Consider the following example but keep in mind, that you might have to play around with the values for stiffness, etc.:
struct ContentView: View {
#State var test = false
var body: some View {
VStack {
Text("Animatable")
.offset(y: test ? 0 : -80)
.animation(.interpolatingSpring(stiffness: 350, damping: 5, initialVelocity: 10))
Button(action: {self.test.toggle()}) {
Text("Animate")
}
}
}
}
Using multiple offsets, delays and easing I was able to get fairly close to replicating that specific animation.
struct ContentView: View {
#State var bounceHeight: BounceHeight? = nil
func bounceAnimation() {
withAnimation(Animation.easeOut(duration: 0.3).delay(0)) {
bounceHeight = .up100
}
withAnimation(Animation.easeInOut(duration: 0.04).delay(0)) {
bounceHeight = .up100
}
withAnimation(Animation.easeIn(duration: 0.3).delay(0.34)) {
bounceHeight = .base
}
withAnimation(Animation.easeOut(duration: 0.2).delay(0.64)) {
bounceHeight = .up40
}
withAnimation(Animation.easeIn(duration: 0.2).delay(0.84)) {
bounceHeight = .base
}
withAnimation(Animation.easeOut(duration: 0.1).delay(1.04)) {
bounceHeight = .up10
}
withAnimation(Animation.easeIn(duration: 0.1).delay(1.14)) {
bounceHeight = .none
}
}
var body: some View {
VStack {
Text("☝️")
.font(.system(size: 200))
.multilineTextAlignment(.center)
.minimumScaleFactor(0.2)
.lineLimit(1)
}
.padding(8)
.frame(width: 72, height: 72)
.background(.purple.opacity(0.4))
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.shadow(radius: 3)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(.purple, lineWidth: 2)
)
.offset(y: bounceHeight?.associatedOffset ?? 0)
.onTapGesture {
bounceAnimation()
}
}
}
enum BounceHeight {
case up100, up40, up10, base
var associatedOffset: Double {
switch self {
case .up100:
return -100
case .up40:
return -40
case .up10:
return -10
case .base:
return 0
}
}
}

Having trouble creating a star animation in swiftUI

I'm trying to create a star animation using blur, but my stars end up disappearing and I can't figure out why.
Through debugging a bit I'm pretty sure this has to do with how I'm using onAppear. I'm just trying to make sure the stars blur and unblur on the screen forever - I always want the stars to be visible though.
Could anyone help me fix this problem (attached code below) and any design tips would be appreciated haha.
struct ContentView: View {
#State private var radius = 2
private var opacity = 0.25
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
VStack {
ForEach(0..<8) {_ in
HStack {
ForEach(0..<5) { _ in
Circle().fill(Color.white)
.frame(width: 3, height: 3)
.blur(radius: CGFloat(self.radius))
.animation(Animation.easeInOut(duration: 6).
repeatForever(autoreverses: true))
.padding(EdgeInsets(top: self.calculateRandom(), leading: 0,
bottom: 0, trailing: self.calculateRandom()))
.onAppear() {
self.radius += 2
}
}
}
}
}
}
}
func calculateRandom() -> CGFloat {
return CGFloat(Int.random(in: 30..<150))
}
}
To have animation activated you need to toggle some values, so animator has range to animate in between.
Here is fixed code. Tested with Xcode 12 / iOS 14.
struct ContentView: View {
#State private var run = false // << here !!
private var opacity = 0.25
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
VStack {
ForEach(0..<8) {_ in
HStack {
ForEach(0..<5) { _ in
Circle().fill(Color.white)
.frame(width: 3, height: 3)
.blur(radius: run ? 4 : 2) // << here !!
.animation(Animation.easeInOut(duration: 6).repeatForever(autoreverses: true))
.padding(EdgeInsets(top: self.calculateRandom(), leading: 0,
bottom: 0, trailing: self.calculateRandom()))
.onAppear() {
self.run = true // << here !!
}
}
}
}
}
}
}
func calculateRandom() -> CGFloat {
return CGFloat(Int.random(in: 30..<150))
}
}
Update: variant with static star positions (movement animation is caused by layout in V/H/Stacks as soon as new elements added, so to avoid this it needs to remove those inner stacks and layout manually in ZStack with .position modifier)
struct BlurContentView: View {
#State private var run = false
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
GeometryReader { gp in
ForEach(0..<8) {_ in
ForEach(0..<5) { _ in
Circle().fill(Color.white)
.frame(width: 3, height: 3)
.position(x: calculateRandom(in: gp.size.width),
y: calculateRandom(in: gp.size.height))
.animation(nil) // << no animation for above modifiers
.blur(radius: run ? 4 : 2)
}
}
}
.animation(Animation.easeInOut(duration: 6)
.repeatForever(autoreverses: true), value: run) // animate one value
.onAppear() {
self.run = true
}
}
}
func calculateRandom(in value: CGFloat) -> CGFloat {
return CGFloat(Int.random(in: 10..<Int(value) - 10))
}
}

SwiftUI Text animation on opacity does not work

Question is simple: how in the world do i get a Text to animate properly?
struct ContentView: View {
#State var foozle: String = ""
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.frame(maxWidth: .infinity)
.transition(.opacity)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.foozle = "uuuuuuuuu"
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
The problem: the view insists on doing some dumb animation where the text is replaced with the new text, but truncated with ellipses, and it slowly expands widthwise until the entirety of the new text is shown.
Naturally, this is not an animation on opacity. It's not a frame width problem, as I've verified with drawing the borders.
Is this just another dumb bug in SwiftUI that i'm going to have to deal with, and pray that someone fixes it?
EDIT: ok, so thanks to #Mac3n, i got this inspiration, which works correctly, even if it's a little ugly:
Text(self.foozle)
.frame(maxWidth: .infinity)
.opacity(op)
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.op = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.foozle += "omo"
withAnimation(.easeIn(duration: 0.3)) {
self.op = 1
}
}
}
}) {
Text("ugh")
}
The problem is that SwiftUI sees Text view as the same view. You can use the .id() method on the view to set it. In this case I've just set the value to a hash of the text itself, so if you change the text, the entire view will get replaced.
struct ContentView: View {
#State var foozle: String = ""
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.id(self.foozle.hashValue)
.frame(maxWidth: .infinity)
.transition(.opacity)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.foozle = "uuuuuuuuu"
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
Transition works when view appeared/disappeared. In your use-case there is no such workflow.
Here is a demo of possible approach to hide/unhide text with opacity animation:
struct DemoTextOpacity: View {
var foozle: String = "uuuuuuuuu"
#State private var hidden = true
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.frame(maxWidth: .infinity)
.opacity(hidden ? 0 : 1)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.hidden.toggle()
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
If you want to animate on opacity you need to change opacity value on your text element.
code example:
#State private var textValue: String = "Sample Data"
#State private var opacity: Double = 1
var body: some View {
VStack{
Text("\(textValue)")
.opacity(opacity)
Button("Next") {
withAnimation(.easeInOut(duration: 0.5), {
self.opacity = 0
})
self.textValue = "uuuuuuuuuuuuuuu"
withAnimation(.easeInOut(duration: 1), {
self.opacity = 1
})
}
}
}