I have a Rectangle()
SwiftUI view. I am trying to animate the fill inside it based on a numeric value. For example the height of the rectangle would be totalHeight
* ratio
. Here is the code:
struct SquareOptionView: View {
let title: String
@Binding var voteRatio: Double
let color: Color
let tapAction: ()->()
var body: some View {
VStack(spacing: 10.0){
Text(title)
Text(
"\(voteRatio * 100, specifier: "%.2f")%"
)
}
.padding(25.0)
.frame(maxWidth: .infinity, alignment: .bottom)
.background{
color
.containerRelativeFrame(.vertical, alignment: .bottom) { length, _ in
return length * voteRatio
}
.animation(.bouncy, value: voteRatio)
.frame(alignment: .bottom)
}
.onTapGesture {
tapAction()
}
}
}
It works well but the animation ends up going to the center rather than top to bottom. As you can see I tried spamming the bottom
alignment but the views still animate to the center of their size.
Code snippets:
import Foundation
class SquareOptionsContainerViewModel: ObservableObject{
let firstOptionTitle: String
let secondOptionTitle: String
@Published var firstVoteRatio: Double
@Published var secondVoteRatio: Double
let firstOptionClickListener: ()->()
let secondOptionClickListener: ()->()
init(firstOptionTitle: String, secondOptionTitle: String, firstVoteRatio: Double, secondVoteRatio: Double, firstOptionClickListener: @escaping () -> Void, secondOptionClickListener: @escaping () -> Void) {
self.firstOptionTitle = firstOptionTitle
self.secondOptionTitle = secondOptionTitle
self.firstVoteRatio = firstVoteRatio
self.secondVoteRatio = secondVoteRatio
self.firstOptionClickListener = firstOptionClickListener
self.secondOptionClickListener = secondOptionClickListener
}
}
ParentView:
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel: SquareOptionsContainerViewModel
var body: some View {
HStack(spacing: 0.0){
SquareOptionView(title: viewModel.firstOptionTitle, voteRatio: $viewModel.firstVoteRatio, color: .green){
viewModel.firstVoteRatio = random(min: 0.0, max: 1.0)
}
Rectangle()
.frame(width: 2.0)
.foregroundColor(
.black
)
SquareOptionView(title: viewModel.secondOptionTitle, voteRatio: $viewModel.secondVoteRatio, color: .red){
viewModel.secondVoteRatio = random(min: 0.0, max: 1.0)
}
}
.clipShape(
RoundedRectangle(cornerSize: .init(width: 10.0, height: 10.0))
)
.overlay(
RoundedRectangle(cornerSize: .init(width: 10.0, height: 10.0))
.stroke(lineWidth: 2.0)
)
.padding(.vertical, 10.0)
}
func random(min: Double, max: Double) -> Double {
return random * (max - min) + min
}
var random: Double {
return Double(arc4random()) / 0xFFFFFFFF
}
}
Sample Repo: https://github.com/mrikh/test
I think you misunderstood what "container" means in containerRelativeFrame
. Check out the documentation to see what a "container" means in this context.
If I understand correctly, you don't want the frame to be relative to a "container", but just relative to the parent view.
To set a frame that fills all the available space in the parent view, you should use .frame(maxWidth: .infinity, maxHeight: .infinity)
. In fact, I think if you replace the .frame
modifier just above the .background
modifier in swiftPunk's answer, it would work as expected:
.frame(maxWidth: .infinity, maxHeight: .infinity) // <------
.background{
GeometryReader { proxy in
Color.white.opacity(0.01)
color
.offset(x: .zero, y: proxy.size.height*(1 - voteRatio))
}
}
Note that this makes the whole VStack
's height match its parent, not just the view in the background
.
I personally would use a scaleEffect
that scales the background color vertically. Use anchor: .bottom
so that it scales from the bottom.
struct SquareOptionView: View {
let title: String
// voteRatio doesn't need to be a Binding because you never change it in SqaureOptionView
let voteRatio: Double
let color: Color
let tapAction: ()->()
var body: some View {
VStack(spacing: 10.0){
Text(title)
Text(
"\(voteRatio * 100, specifier: "%.2f")%"
)
}
.padding(25.0)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(.rect)
.background {
color
.scaleEffect(x: 1, y: voteRatio, anchor: .bottom)
.animation(.bouncy, value: voteRatio)
}
.onTapGesture {
tapAction()
}
}
}