SwiftUI Button: Preserve highlighted state without ugly animation - swift

I have a quiz view with buttons per answer. After the user chooses an answer, the respective button should remain highlighted. The problem is that I get an unwanted transition animation, it seems that the following happens
User holds button: isPressed = true
User taps button: isPressed = false
Button action is triggered: answerState == .highlighted
This results in an animation where the button is unhighlighted in step 2 and highlighted again in step 3, which is quite ugly. Do you have any idea how to solve that issue?
import Foundation
import SwiftUI
struct QuizView: View {
#State var quizQuestion = QuizManager.shared.generateQuestion()
#State var selectedAnswer: Insignia? = nil
#State var isSolutionPresented = false
#Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
#Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
var body: some View {
QuizGrid(self.quizQuestion.elements) { quizElement in
if quizElement.type == .question {
VStack {
Text("Group name")
.font(.subheadline)
.fontWeight(.light)
.padding(.horizontal)
Text(quizElement.insignia.wrappedName)
.font(.title)
// .font(.system(size: 30.0, weight: .heavy, design: .default))
//.fontWeight(.heavy)
//.lineLimit(2)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal)
.padding(.top, 5)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
Button(action: {
self.selectedAnswer = quizElement.insignia
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.presentSolution()
}
}) {
Image("Image")
.renderingMode(.original)
}
.buttonStyle(
QuizAnswerButtonStyle(answerState: self.answerState(answer: quizElement.insignia))
)
.disabled(self.selectedAnswer != nil)
}
}
.gridStyle(
(horizontalSizeClass == .compact && verticalSizeClass == .regular) ? QuizGridStyle(columns: 2, rows: 2, questionPosition: .top, relativeQuestionSize: CGSize(width:1, height: 0.3)) : QuizGridStyle(columns: 4, rows: 1, questionPosition: .top, relativeQuestionSize: CGSize(width:1, height: 0.3))
)
}
struct QuizAnswerButtonStyle: ButtonStyle {
var answerState: QuizAnswerState
func makeBody(configuration: Self.Configuration) -> some View {
ZStack(alignment: .init(horizontal: .center, vertical: .center)) {
Rectangle()
.foregroundColor(.white)
.colorMultiply(self.fillColor(answerState: (configuration.isPressed || self.answerState == .highlighted) ? .highlighted : self.answerState))
configuration.label
}
.overlay(
RoundedRectangle(cornerRadius: 10)
.strokeBorder(Color("quizItemBorderColor").opacity(0.2), lineWidth: 1)
)
.cornerRadius(10)
}
func fillColor(answerState: QuizAnswerState) -> Color {
switch(answerState) {
case .neutral:
return Color("quizItemNeutralColor")
case .highlighted:
return Color("quizItemHighlightedColor")
case .correct:
return Color("quizItemCorrectColor")
case .incorrect:
return Color("quizItemIncorrectColor")
}
}
}
func answerState(answer: Insignia) -> QuizAnswerState {
if self.isSolutionPresented {
if (self.selectedAnswer?.objectID == answer.objectID && answer.objectID == self.quizQuestion.question.objectID) || (self.selectedAnswer?.objectID != answer.objectID && answer.objectID == self.quizQuestion.question.objectID) {
return .correct
} else if self.selectedAnswer?.objectID == answer.objectID && answer.objectID != self.quizQuestion.question.objectID {
return .incorrect
}else {
return .neutral
}
} else {
return self.selectedAnswer?.objectID == answer.objectID ? .highlighted : .neutral
}
}
func presentSolution() {
withAnimation(.easeInOut(duration: 0.15)) {
self.isSolutionPresented = true
}
let delay = self.selectedAnswer?.objectID == self.quizQuestion.question.objectID ? UserDefaults.standard.double(forKey: "QuizDelayAfterCorrectAnswer") : UserDefaults.standard.double(forKey: "QuizDelayAfterIncorrectAnswer")
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.loadQuestion()
}
}
func loadQuestion() {
self.isSolutionPresented = false
self.selectedAnswer = nil
self.quizQuestion = QuizManager.shared.generateQuestion()
}
}
public enum QuizAnswerState {
case neutral
case highlighted
case correct
case incorrect
}
For the following demo, I added a scale animation for better visibility of the issue. The first interactions are just pressing the button without actually selecting it. The animation looks good. The last interaction is selecting the button, and there is this strange animation.

Related

Changing translation of a DragGesture swift

I am working with a slider and dealing with translation. Once the slider performs an action, I mock an api call and then on completion the image is unlocked but now I would like to return the slider to the original position once the api call is completed. Here is what the slider looks like in code.
struct DraggingComponent: View {
#Binding var isLocked: Bool
#Binding var isLoading: Bool
let maxWidth: CGFloat
#State private var width = CGFloat(50)
private let minWidth = CGFloat(50)
init(isLocked: Binding<Bool>, isLoading: Binding<Bool>, maxWidth: CGFloat) {
_isLocked = isLocked
self._isLoading = isLoading
self.maxWidth = maxWidth
}
var body: some View {
RoundedRectangle(cornerRadius: 16)
.fill(Color.black)
.opacity(width / maxWidth)
.frame(width: width)
.overlay(
Button(action: { }) {
ZStack {
image(name: "lock", isShown: isLocked)
progressView(isShown: isLoading)
image(name: "lock.open", isShown: !isLocked && !isLoading)
}
.animation(.easeIn(duration: 0.35).delay(0.55), value: !isLocked && !isLoading)
}
.buttonStyle(BaseButtonStyle())
.disabled(!isLocked || isLoading),
alignment: .trailing
)
.simultaneousGesture(
DragGesture()
.onChanged { value in
guard isLocked else { return }
if value.translation.width > 0 {
width = min(max(value.translation.width + minWidth, minWidth), maxWidth)
}
}
.onEnded { value in
guard isLocked else { return }
if width < maxWidth {
width = minWidth
UINotificationFeedbackGenerator().notificationOccurred(.warning)
} else {
UINotificationFeedbackGenerator().notificationOccurred(.success)
withAnimation(.spring().delay(0.5)) {
isLocked = false
}
}
}
)
.animation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 0), value: width)
}
private func image(name: String, isShown: Bool) -> some View {
Image(systemName: name)
.font(.system(size: 20, weight: .regular, design: .rounded))
.foregroundColor(Color.black)
.frame(width: 42, height: 42)
.background(RoundedRectangle(cornerRadius: 14).fill(.white))
.padding(4)
.opacity(isShown ? 1 : 0)
.scaleEffect(isShown ? 1 : 0.01)
}
private func progressView(isShown: Bool) -> some View {
ProgressView()
.progressViewStyle(.circular)
.tint(.white)
.opacity(isShown ? 1 : 0)
.scaleEffect(isShown ? 1 : 0.01)
}
}
Where it is used:
#State private var isLocked = true
#State private var isLoading = false
GeometryReader { geometry in
ZStack(alignment: .leading) {
BackgroundComponent()
DraggingComponent(isLocked: $isLocked, isLoading: $isLoading, maxWidth: geometry.size.width)
}
}
.frame(height: 50)
.padding()
.padding(.bottom, 20)
.onChange(of: isLocked) { isLocked in
guard !isLocked else { return }
simulateRequest()
}
private func simulateRequest() {
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
isLoading = false
}
}
How can I get the translation back to the initial position.
Pull your width #State up into the containing view and pass it on as #Binding. After simulateRequest set it to its initial state.
In your DraggingComponent use
struct DraggingComponent: View {
#Binding var width: CGFloat
.....
in the View that contains DraggingComponent:
#State private var width = CGFloat(50)
and:
.onChange(of: isLocked) { isLocked in
guard !isLocked else { return }
simulateRequest()
self.width = CGFloat(50) //reset it here probably with animation
}

#State Property not accumulating

Users can swipe left or right on a stack of cards to simulate "true" or "false" to a series of quiz questions. I have a #State var called userScore initialized to 0. When dragGesture .onEnded, I compare the "correctAnswer" with the "userAnswer." If they match, add 1 point to the userScore.
The problem: The console prints 1 or 0. The score does not ACCUMULATE.
Please help me calculate the user's final score? Thanks in advance. . .
import SwiftUI
struct CardView: View {
#State var offset: CGSize = .zero
#State var userScore: Int = 0
#State var userAnswer: Bool = false
private var currentQuestion: Question
private var onRemove: (_ user: Question) -> Void
init(user: Question, onRemove: #escaping (_ user: Question) -> Void) {
self.currentQuestion = user
self.onRemove = onRemove
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25, style: .continuous)
.fill(
Color.black
.opacity(1 - Double(abs(offset.width / 50)))
)
.background(
RoundedRectangle(cornerRadius: 25, style: .continuous)
.fill(offset.width > 0 ? Color.green : Color.red)
//.overlay(offset.width > 0 ? likeGraphics() : dislikeGraphics(), alignment: .topLeading)
)
.shadow(color: .red, radius: 5, x: 0.0, y: 0.0)
VStack {
Image(currentQuestion.imageIcon)
.resizable()
.scaledToFit()
.foregroundColor(Color.white)
.frame(width: 75, height: 75)
ScrollView {
Text(currentQuestion.questionText)
.font(.largeTitle)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
}
}
.padding()
}
.frame(width: 300, height: 375) //check: www.ios-resolution.com
.offset(offset)
.rotationEffect(.degrees(offset.width / 400.0 ) * 15, anchor: .bottom)
//.opacity(2 - Double(abs(offset.width / 50.0))) //fade on drag too?
.gesture(
DragGesture()
.onChanged { gesture in
withAnimation(.spring()) {
offset = gesture.translation
}
}
.onEnded { value in
withAnimation(.interactiveSpring()) {
if abs(offset.width) > 100 {
onRemove(currentQuestion)
if (currentQuestion.correctAnswer == determineSwipeStatus()) {
userScore += 1
}
print(userScore)
} else {
offset = .zero
}
}
}
)
}
func determineSwipeStatus() -> Bool {
if(offset.width > 0) {
userAnswer = true
} else if (offset.width < 0) {
userAnswer = false
}
return userAnswer
}
func getAlert() -> Alert {
return Alert(
title: Text("Your group is \(userScore)% cult-like!"),
message: Text("Would you like to test another group?"),
primaryButton: .default(Text("Yes, test another group"),
action: {
//transition back to screen one
//self.showQuizPageScreen.toggle()
}),
secondaryButton: .cancel())
}
}
struct CardView_Previews: PreviewProvider {
static var previews: some View {
CardView(user: Question(id: 1,
imageIcon: "icon1",
questionText: "Dummy placeholder text",
correctAnswer: true),
onRemove: { _ in
//leave blank for preview purposes
})
}
}

SwiftUI For Each Button in list - not working... sometimes

I have made a custom selection button view in SwiftUI for an app that is being developed. I cant for the life of me work out why sometimes the buttons don't do anything - It is always the last x number of buttons that don't work (which made me think it was related to the 10 view limitation of swift ui however, I've been told this isn't an issue when using a for each loop).
Sometimes it works as expected and others it cuts off the last x number of buttons. Although when it is cutting off buttons it is consistent between different simulators and physical devices. Can anybody see anything wrong here?
I am new to SwiftUI and so could be something simple...
#EnvironmentObject var QuestionManager: questionManager
var listItems: [String]
#State var selectedItem: String = ""
var body: some View {
GeometryReader {geom in
ScrollView{
VStack{
ForEach(Array(listItems.enumerated()), id: \.offset){ item in
Button(action: {
if (selectedItem != item.element) {
selectedItem = item.element
} else {
selectedItem = ""
QuestionManager.tmpAnswer = ""
}
}, label: {
GeometryReader { g in
Text("\(item.element)")
.font(.system(size: g.size.width/22))
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.black)
.lineLimit(2)
.frame(width: g.size.width, height: g.size.height)
.minimumScaleFactor(0.5)
.background(
Rectangle()
.fill((item.element == selectedItem) ? Color(.green) : .white)
.frame(width: g.size.width, height: g.size.height)
.border(Color.gray)
).scaledToFit()
}
.frame(width: geom.size.width*0.92, height: 45)
}).disabled((Int(QuestionManager.answers.year) == Calendar.current.component(.year, from: Date())) ? validateMonth(month: item.offset) : false)
}
}
.frame(width: geom.size.width)
}
}
}
} ```
As #Yrb mentioned, using enumerated() is not a great option in a ForEach loop.
Your issue could be compounded by listItems having duplicate elements.
You may want to restructure your code, something like this approach using a dedicated
item structure, works very well in my tests:
struct MyItem: Identifiable, Equatable {
let id = UUID()
var name = ""
init(_ str: String) {
self.name = str
}
static func == (lhs: MyItem, rhs: MyItem) -> Bool {
lhs.id == rhs.id
}
}
struct ContentView: View {
#EnvironmentObject var QuestionManager: questionManager
// for testing
var listItems: [MyItem] = [MyItem("1"),MyItem("2"),MyItem("3"),MyItem("4"),MyItem("6"),MyItem("7"),MyItem("8"),MyItem("9")]
#State var selectedItem: MyItem? = nil
var body: some View {
GeometryReader {geom in
ScrollView{
VStack{
ForEach(listItems){ item in
Button(action: {
if (selectedItem != item) {
selectedItem = item
} else {
selectedItem = nil
QuestionManager.tmpAnswer = ""
}
}, label: {
GeometryReader { g in
Text(item.name)
.font(.system(size: g.size.width/22))
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.black)
.lineLimit(2)
.frame(width: g.size.width, height: g.size.height)
.minimumScaleFactor(0.5)
.background(
Rectangle()
.fill((item == selectedItem) ? Color(.green) : .white)
.frame(width: g.size.width, height: g.size.height)
.border(Color.gray)
).scaledToFit()
}
.frame(width: geom.size.width*0.92, height: 45)
})
.disabled((Int(QuestionManager.answers.year) == Calendar.current.component(.year, from: Date())) ? validateMonth(item: item) : false)
}
}
.frame(width: geom.size.width)
}
}
}
func validateMonth(item: MyItem) -> Bool {
if let itemOffset = listItems.firstIndex(where: {$0.id == item.id}) {
// ... do your validation
return true
}
return false
}
}

Disable animation when a view appears in SwiftUI

I got a problem while trying to display a custom loading view in SwiftUI.
I created a custom struct view OrangeActivityIndicator:
struct OrangeActivityIndicator: View {
var style = StrokeStyle(lineWidth: 6, lineCap: .round)
#State var animate = false
let orangeColor = Color.orOrangeColor
let orangeColorOpaque = Color.orOrangeColor.opacity(0.5)
init(lineWidth: CGFloat = 6) {
style.lineWidth = lineWidth
}
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false))
}.onAppear() {
self.animate.toggle()
}
}
}
I use it inside different screens or views, my problem is that it appears weirdly, for example in CampaignsView of the app I display it when the server call is in progress.
struct CampaignsView: View {
#ObservedObject var viewModel: CampaignsViewModel
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 0) {
CustomNavigationBar(campaignsNumber: viewModel.cardCampaigns.count)
.padding([.leading, .trailing], 24)
.frame(height: 25)
CarouselView(x: $viewModel.x, screen: viewModel.screen, op: $viewModel.op, count: $viewModel.index, cardCampaigns: $viewModel.cardCampaigns).frame(height: 240)
CampaignDescriptionView(idx: viewModel.index, cardCampaigns: viewModel.cardCampaigns)
.padding([.leading, .trailing], 24)
Spacer()
}
.onAppear {
self.viewModel.getCombineCampaigns()
}
if viewModel.isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
}
}
.padding(.top, 34)
.background(Color.orBackgroundGrayColor.edgesIgnoringSafeArea(.all))
.navigationBarHidden(true)
}
}
}
The Indicator itself is correctly spinning, the problem is when it appears, it appears as a translation animation coming from the bottom to the middle of the screen. This is my viewModel with the server call and isLoading property:
class CampaignsViewModel: ObservableObject {
#Published var index: Int = 0
#Published var cardCampaigns: [CardCampaign] = [CardCampaign]()
#Published var isLoading: Bool = false
var cancellable: AnyCancellable?
func getCombineCampaigns() {
self.isLoading = true
let campaignLoader = CampaignLoader()
cancellable = campaignLoader.getCampaigns()
.receive(on: DispatchQueue.main)
//Handle Events operator is used for debugging.
.handleEvents(receiveSubscription: { print("Receive subscription: \($0)") },
receiveOutput: { print("Receive output: \($0)") },
receiveCompletion: { print("Receive completion: \($0)") },
receiveCancel: { print("Receive cancel") },
receiveRequest: { print("Receive request: \($0)") })
.sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { campaignResult in
self.isLoading = false
guard let campaignsList = campaignResult.content else {
return
}
self.cardCampaigns = campaignsList.map { campaign in
return CardCampaign(campaign: campaign)
}
self.moveToFirstCard()
}
}
}
Use animation with linked related state
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7)
.repeatForever(autoreverses: false),
value: animate) // << here !!
}.onAppear() {
self.animate.toggle()
}
}
SwiftUI 3
(iOS 15)
use the new API
.animation(.default, value: isAppeared)
SwiftUI 1, 2
Like below, when I first load a view, the button was animated.
So, I used the GeometryReader.
The animation is set to nil while the coordinates of the dummy are changing.
RecordView.swift
struct RecordView: View {
var body: some View {
Button(action: { }) {
Color(.red)
.frame(width: 38, height: 38)
.cornerRadius(6)
.padding(34)
.overlay(Circle().stroke(Color(.red), lineWidth: 8))
}
.buttonStyle(ButtonStyle1()) // 👈 customize the button.
}
}
ButtonStyle.swift
import SwiftUI
struct ButtonStyle1: ButtonStyle {
// In my case, the #State isn't worked, so I used class.
#ObservedObject private var data = ButtonStyle1Data()
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
// Dummy for tracking the view status
GeometryReader {
Color.clear
.preference(key: FramePreferenceKey.self, value: $0.frame(in: .global))
}
.frame(width: 0, height: 0)
.onPreferenceChange(FramePreferenceKey.self) { frame in
guard !data.isAppeared else { return }
// ⬇️ This is the key
data.isLoaded = 0 <= frame.origin.x
}
// Content View
configuration.label
.opacity(configuration.isPressed ? 0.5 : 1)
.scaleEffect(configuration.isPressed ? 0.92 : 1)
.animation(data.isAppeared ? .easeInOut(duration: 0.18) : nil)
}
}
}
ButtonStyleData.swift
final class ButtonStyle1Data: ObservableObject {
#Published var isAppeared = false
}
FramePreferenceKey.swift
struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
}
Result

SwiftUI list animations

I am following Apple's Swift UI Animating Views And Transitions and I noticed a bug in the Hike Graph View. When I click on the graph it does not allow me to switch from Elevation to Heart Rate or Pace. It does not let me and just exits the view. I think this has something to do with the List here:
VStack(alignment: .leading) {
Text("Recent Hikes")
.font(.headline)
HikeView(hike: hikeData[0])
}
Hike View Contains:
import SwiftUI
struct HikeView: View {
var hike: Hike
#State private var showDetail = false
var transition: AnyTransition {
let insertion = AnyTransition.move(edge: .trailing)
.combined(with: .opacity)
let removal = AnyTransition.scale
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
var body: some View {
VStack {
HStack {
HikeGraph(hike: hike, path: \.elevation)
.frame(width: 50, height: 30)
.animation(nil)
VStack(alignment: .leading) {
Text(hike.name)
.font(.headline)
Text(hike.distanceText)
}
Spacer()
Button(action: {
withAnimation {
self.showDetail.toggle()
}
}) {
Image(systemName: "chevron.right.circle")
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.scaleEffect(showDetail ? 1.5 : 1)
.padding()
}
}
if showDetail {
HikeDetail(hike: hike)
.transition(transition)
}
}
}
}
Hike Detail Contains:
struct HikeDetail: View {
let hike: Hike
#State var dataToShow = \Hike.Observation.elevation
var buttons = [
("Elevation", \Hike.Observation.elevation),
("Heart Rate", \Hike.Observation.heartRate),
("Pace", \Hike.Observation.pace),
]
var body: some View {
return VStack {
HikeGraph(hike: hike, path: dataToShow)
.frame(height: 200)
HStack(spacing: 25) {
ForEach(buttons, id: \.0) { value in
Button(action: {
self.dataToShow = value.1
}) {
Text(value.0)
.font(.system(size: 15))
.foregroundColor(value.1 == self.dataToShow
? Color.gray
: Color.accentColor)
.animation(nil)
}
}
}
}
}
}
Hike Graoh Contains:
import SwiftUI
func rangeOfRanges<C: Collection>(_ ranges: C) -> Range<Double>
where C.Element == Range<Double> {
guard !ranges.isEmpty else { return 0..<0 }
let low = ranges.lazy.map { $0.lowerBound }.min()!
let high = ranges.lazy.map { $0.upperBound }.max()!
return low..<high
}
func magnitude(of range: Range<Double>) -> Double {
return range.upperBound - range.lowerBound
}
extension Animation {
static func ripple(index: Int) -> Animation {
Animation.spring(dampingFraction: 0.5)
.speed(2)
.delay(0.03 * Double(index))
}
}
struct HikeGraph: View {
var hike: Hike
var path: KeyPath<Hike.Observation, Range<Double>>
var color: Color {
switch path {
case \.elevation:
return .gray
case \.heartRate:
return Color(hue: 0, saturation: 0.5, brightness: 0.7)
case \.pace:
return Color(hue: 0.7, saturation: 0.4, brightness: 0.7)
default:
return .black
}
}
var body: some View {
let data = hike.observations
let overallRange = rangeOfRanges(data.lazy.map { $0[keyPath: self.path] })
let maxMagnitude = data.map { magnitude(of: $0[keyPath: path]) }.max()!
let heightRatio = (1 - CGFloat(maxMagnitude / magnitude(of: overallRange))) / 2
return GeometryReader { proxy in
HStack(alignment: .bottom, spacing: proxy.size.width / 120) {
ForEach(data.indices) { index in
GraphCapsule(
index: index,
height: proxy.size.height,
range: data[index][keyPath: self.path],
overallRange: overallRange)
.colorMultiply(self.color)
.transition(.slide)
.animation(.ripple(index: index))
}
.offset(x: 0, y: proxy.size.height * heightRatio)
}
}
}
}
Graph Capsule Contains:
import SwiftUI
struct GraphCapsule: View {
var index: Int
var height: CGFloat
var range: Range<Double>
var overallRange: Range<Double>
var heightRatio: CGFloat {
max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
}
var offsetRatio: CGFloat {
CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
}
var body: some View {
Capsule()
.fill(Color.white)
.frame(height: height * heightRatio)
.offset(x: 0, y: height * -offsetRatio)
}
}
Is there any way to fix this? Thanks
The problem might be deeper in SwiftUI - if you comment out transition(.slide) in HikeGraph (and restart the XCODE), it will start working