How to make LongPressGesture and scrolling in ScrollView work together at the same time in SwiftUI? - ios

Let's imagine, here is a ScrollView with some elements and I want to make some actions (e.g. changing of color) on long tap on these elements. But also I want to make possible to scroll this view.
Here is an example:
import SwiftUI
struct TextBox: View {
var text: String
var color: Color
#GestureState private var isLongPressure: Bool = false
var body: some View {
let longTap = LongPressGesture(minimumDuration: 0.3)
.updating($isLongPressure) { state, newState, transaction in
newState = state
transaction.animation = .easeOut(duration: 0.2)
}
Text(text)
.frame(width: 400, height: 200)
.background(isLongPressure ? .white : color)
.simultaneousGesture(longTap)
}
}
struct TestGestures: View {
var body: some View {
ScrollView {
TextBox(text: "Test 1", color: .red)
TextBox(text: "Test 2", color: .green)
TextBox(text: "Test 3", color: .blue)
TextBox(text: "Test 4", color: .red)
TextBox(text: "Test 5", color: .green)
TextBox(text: "Test 6", color: .blue)
}
}
}
struct TestGestures_Previews: PreviewProvider {
static var previews: some View {
TestGestures()
}
}
So, if I comment .simultaneousGesture(longTap) – scrolling works, but if I uncomment it – scrolling stopped work.
P.S.: I've tried to add onTapGesture before adding longTap and it doesn't help.
Thanks in advance!
Update:
Thanks for the solution by #nickreps:
import SwiftUI
struct TextBox: View {
var text: String
var color: Color
#GestureState private var isLongPressure: Bool = false
var body: some View {
let longTap = LongPressGesture(minimumDuration: 0.3)
.updating($isLongPressure) { value, state, transaction in
state = value
transaction.animation = .easeOut(duration: 0.2)
}
Text(text)
.frame(width: 400, height: 200)
.background(isLongPressure ? .white : color)
.delaysTouches(for: 0.01) {
//some code here, if needed
}
.gesture(longTap)
}
}
struct TestGestures: View {
var body: some View {
ScrollView {
TextBox(text: "Test 1", color: .red)
TextBox(text: "Test 2", color: .green)
TextBox(text: "Test 3", color: .blue)
TextBox(text: "Test 4", color: .red)
TextBox(text: "Test 5", color: .green)
TextBox(text: "Test 6", color: .blue)
}
}
}
extension View {
func delaysTouches(for duration: TimeInterval = 0.25, onTap action: #escaping () -> Void = {}) -> some View {
modifier(DelaysTouches(duration: duration, action: action))
}
}
fileprivate struct DelaysTouches: ViewModifier {
#State private var disabled = false
#State private var touchDownDate: Date? = nil
var duration: TimeInterval
var action: () -> Void
func body(content: Content) -> some View {
Button(action: action) {
content
}
.buttonStyle(DelaysTouchesButtonStyle(disabled: $disabled, duration: duration, touchDownDate: $touchDownDate))
.disabled(disabled)
}
}
fileprivate struct DelaysTouchesButtonStyle: ButtonStyle {
#Binding var disabled: Bool
var duration: TimeInterval
#Binding var touchDownDate: Date?
func makeBody(configuration: Configuration) -> some View {
configuration.label
.onChange(of: configuration.isPressed, perform: handleIsPressed)
}
private func handleIsPressed(isPressed: Bool) {
if isPressed {
let date = Date()
touchDownDate = date
DispatchQueue.main.asyncAfter(deadline: .now() + max(duration, 0)) {
if date == touchDownDate {
disabled = true
DispatchQueue.main.async {
disabled = false
}
}
}
} else {
touchDownDate = nil
disabled = false
}
}
}
struct TestGestures_Previews: PreviewProvider {
static var previews: some View {
TestGestures()
}
}

I was able to get it working by utilizing a button rather than a TextView. Although this does directly utilize the code you provided, you should be able to modify some pieces to have it meet your needs (I can help with this, if needed!)
import SwiftUI
struct ScrollTest: View {
let testData = [1]
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
AnimatedButtonView(color: .red, text: "Test 1")
AnimatedButtonView(color: .green, text: "Test 2")
AnimatedButtonView(color: .blue, text: "Test 3")
}
}
}
struct AnimatedButtonView: View {
#GestureState var isDetectingLongPress = false
let color: Color
let text: String
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
.fill(color)
.frame(width: UIScreen.main.bounds.width, height: 200)
.padding(25)
.scaleEffect(!isDetectingLongPress ? 1.0 : 0.875)
.brightness(!isDetectingLongPress ? 0.0 : -0.125)
.animation(.easeInOut(duration: 0.125), value: isDetectingLongPress)
Text(text)
}
.delaysTouches(for: 0.01) {
//some code here, if needed
}
.gesture(
LongPressGesture(minimumDuration: 3)
.updating($isDetectingLongPress) { currentState, gestureState,
transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
print("gesture ended")
})
}
}
extension View {
func delaysTouches(for duration: TimeInterval = 0.25, onTap action: #escaping () -> Void = {}) -> some View {
modifier(DelaysTouches(duration: duration, action: action))
}
}
fileprivate struct DelaysTouches: ViewModifier {
#State private var disabled = false
#State private var touchDownDate: Date? = nil
var duration: TimeInterval
var action: () -> Void
func body(content: Content) -> some View {
Button(action: action) {
content
}
.buttonStyle(DelaysTouchesButtonStyle(disabled: $disabled, duration: duration, touchDownDate: $touchDownDate))
.disabled(disabled)
}
}
fileprivate struct DelaysTouchesButtonStyle: ButtonStyle {
#Binding var disabled: Bool
var duration: TimeInterval
#Binding var touchDownDate: Date?
func makeBody(configuration: Configuration) -> some View {
configuration.label
.onChange(of: configuration.isPressed, perform: handleIsPressed)
}
private func handleIsPressed(isPressed: Bool) {
if isPressed {
let date = Date()
touchDownDate = date
DispatchQueue.main.asyncAfter(deadline: .now() + max(duration, 0)) {
if date == touchDownDate {
disabled = true
DispatchQueue.main.async {
disabled = false
}
}
}
} else {
touchDownDate = nil
disabled = false
}
}
}

I'm not sure I understand the exact context, but you could add a condition so your LongPressGesture only triggers an action when gesture is not being used for scrolling.
let longTap = LongPressGesture(minimumDuration: 0.3)
.updating($isLongPressure) { value, state, transaction in
if value {
state = true
transaction.animation = .easeOut(duration: 0.2)
}
}

Related

SwiftUI sheet reappears after dismissed on Apple Watch

I'm developing a fitness app for the Apple Watch that lets you choose an intensity before starting a workout. I want to present the intensity picker before the workout starts, so I tried presenting it as a sheet before navigating to the actual workout view. The problem is that when I try to dismiss the sheet, it is dismissed but it comes right back. I'm using Xcode 14.2 and watchOS 9.1.
This is the main view and also the view that presents the said sheet (the first one, controlled by showingZonePickerView):
import SwiftUI
#main
struct Wise_Watch_AppApp: App {
#StateObject private var workoutManager = WorkoutManager()
#SceneBuilder var body: some Scene {
WindowGroup {
NavigationView {
WorkoutView()
}
.sheet(isPresented: $workoutManager.showingZonePickerView, onDismiss: {
workoutManager.showingZonePickerView = false
}) {
WorkoutLevelPickerView(total: 5, completed: 1)
.toolbar(.hidden)
}
.sheet(isPresented: $workoutManager.showingSummaryView) {
SummaryView()
}
.environmentObject(workoutManager)
}
}
}
This is the view where the user can pick the preferred workout:
import SwiftUI
import HealthKit
struct WorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#State var linkActive: Bool = false
var workoutTypes: [HKWorkoutActivityType] = [.cycling, .running, .walking]
var workoutDictionary: Dictionary<String, HKWorkoutActivityType> = [
"figure.outdoor.cycle" : .cycling,
"figure.run" : .running,
"figure.walk" : .walking
]
var body: some View {
List(Array(workoutDictionary.keys), id: \.self) { workoutType in
NavigationLink(
destination: SessionPagingView(),
tag: workoutDictionary[workoutType]!,
selection: $workoutManager.selectedWorkout
) {
Label("\(workoutDictionary[workoutType]!.name)", systemImage: workoutType)
.font(.subheadline)
.fontWeight(.semibold)
.padding()
}
.padding(EdgeInsets(top: 15, leading: 5, bottom: 15, trailing: 5))
}
.listStyle(.carousel)
.navigationBarTitle("Workouts")
.onAppear {
workoutManager.requestAuthorization()
}
}
}
This is the view to which the app navigates when a NavigationLink is pressed:
import SwiftUI
import WatchKit
import ConfettiSwiftUI
struct SessionPagingView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#State private var selection: Tab = .metrics
#State private var counter: Int = 0
#State private var isViewHidden: Bool = true
enum Tab {
case metrics, nowPlaying, milestone
}
var body: some View {
if(isViewHidden) {
sessionView.hidden()
} else {
sessionView
}
}
var sessionView: some View {
TabView(selection: $selection) {
ForEach(workoutManager.tabItems) { item in
VStack {
Text("Congrats!")
.font(.title2)
.fontWeight(.bold)
.confettiCannon(counter: $counter, num: 40, radius: 200)
Text("You just reached")
.font(.subheadline)
.foregroundColor(.cyan)
.tag(Tab.milestone)
Text("\(item.value) km")
.font(.subheadline)
.foregroundColor(.cyan)
.tag(Tab.milestone)
}
}
MetricsView().tag(Tab.metrics)
NowPlayingView().tag(Tab.nowPlaying)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.onAppear {
workoutManager.showingZonePickerView = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isViewHidden = false
}
}
.onChange(of: workoutManager.running) { _ in
displayMetricsView()
}
.onChange(of: workoutManager.tabItems) { _ in
if(workoutManager.tabItems.count > 0) {
displayMilestoneView()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: {
displayMetricsView()
})
}
}
.ignoresSafeArea()
.animation(.easeInOut, value: self.selection)
}
private func displayMetricsView() {
withAnimation {
selection = .metrics
}
}
private func displayMilestoneView() {
withAnimation {
selection = .milestone
WKInterfaceDevice.current().play(.notification)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
counter += 1
})
}
}
}
This is the actual view that I'm presenting inside the sheet:
import SwiftUI
struct WorkoutLevelPickerView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#Environment(\.dismiss) private var dismiss
let total: Int
#State var lineWidth: CGFloat = 16
#State var color: Color = .green
#State var completed: Double = 1.0
#State var currentZone: HeartRateZone = zones[0]
#State var isScrolling: Bool = false
var body: some View {
VStack {
ZStack {
CircleLabelView(
radius: 30,
tracking: 0,
size: .init(width: 120, height: 120),
text: currentZone.intensity.uppercased()
)
.font(.headline)
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: Double(currentZone.labelRotationAngleModifier * currentZone.intensity.count / 2)))
.opacity(isScrolling ? 0 : 1)
.animation(.easeInOut, value: isScrolling)
WorkoutLevelPickerBackgroundView(total: total, lineWidth: lineWidth)
withAnimation(.spring()) {
WorkoutLevelPickerProgressView(total: total, completed: Int(completed), lineWidth: lineWidth, zone: currentZone)
}
VStack {
Button {
dismiss()
} label: {
Image(systemName: currentZone.iconName)
.padding()
.font(.title2)
}
.frame(width: 70, height: 70)
}
}
.frame(width: 150, height: 150)
.focusable()
.digitalCrownRotation(
detent: $completed.animation(.spring()),
from: 1.0,
through: 5.0,
by: 1.0,
sensitivity: .low,
isContinuous: false,
isHapticFeedbackEnabled: true,
onChange: { _ in
isScrolling = true
},
onIdle: {
isScrolling = false
}
)
.digitalCrownAccessory(.hidden)
.onChange(of: completed) {_ in
if(Int(completed) != currentZone.id) {
currentZone = zones[Int(completed) - 1]
print(currentZone.tint.description)
}
}
}
}
}
This is a video of the flow that generates my problem:
I tried presenting the sheet from other views. I also tried dismissing the sheet through a binding, not through the dismiss action. On all these changes, the outcome was the same as before.
Edit:
Forgot to mention that, when the sheet is presented, pressing the side button will dismiss it and the app will work as intended until the user tries to start a new workout.
As Paulw11 mentioned in his comment, the problem was that I was constantly setting showingZonePickerView on true when the sheet was dismissed and the view reappeared.

SwiftUI. Subview animation is not working if subview's View #State was changed while parent View is animating. iOS 16

I have a structure, where the parent view is a hidden pop-up with content. On some user action parent View starts to slide up. I need all its content to slide up with the parent View. It was working perfectly before iOS 16, but now it's broken. If the child's View subview #State is changed during the animation, then this View appears instantly on a screen, i.e. not sliding. As I understand, because View's #State was changed SwiftUI redraws this particular View and disables its animation, so it appears in its final position without animation. I was trying to force the animation of this particular View using .animation or withAnimation. Nothing of it helped. How to fix this bug in iOS 16?
Minimal reproducible example:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import SwiftUI
struct ContentView: View {
#State var shouldShow: SlideCardPosition = .bottom
#State var text1: String = ""
var body: some View {
VStack {
Button {
shouldShow = .top
} label: {
Text("Show PopUp")
.padding(.top, 100)
}
PopUpUIView(shouldShow: $shouldShow)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct SlideOverCard<Content: View>: View {
#GestureState private var dragState = SlideDragState.inactive
#Binding private var position: SlideCardPosition
#State private var highlightedBackground = false
private var contentHeight: CGFloat
private var backgroundColor: Color?
private var withCorners: Bool
private var isHandleHidden: Bool
private var overlayOpacity: CGFloat
init(position: Binding<SlideCardPosition>,
contentHeight: CGFloat,
backgroundColor: Color? = nil,
withCorners: Bool = true,
isHandleHidden: Bool = false,
overlayOpacity: CGFloat = 0.75,
content: #escaping () -> Content) {
_position = position
self.content = content
self.contentHeight = contentHeight
self.backgroundColor = backgroundColor
self.withCorners = withCorners
self.isHandleHidden = isHandleHidden
self.overlayOpacity = overlayOpacity
}
var content: () -> Content
var body: some View {
return Rectangle()
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
.foregroundColor(Color.black.opacity(highlightedBackground ? overlayOpacity : 0))
.position(x: UIScreen.screenWidth / 2, y: (UIScreen.screenHeight) / 2)
.edgesIgnoringSafeArea([.top, .bottom])
.overlay(
Group {
VStack(spacing: 0) {
if !isHandleHidden {
Handle()
}
self.content()
Spacer()
}
}
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
.background(backgroundColor != nil ? backgroundColor! : Color.black)
.cornerRadius(withCorners ? 40.0 : 0)
.shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0)
.offset(y: position(from: position) + dragState.translation.height)
.animation(dragState.isDragging ? nil : .interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0), value: UUID())
.edgesIgnoringSafeArea([.top, .bottom])
.onTapGesture {}
.onChange(of: position) { _ in
withAnimation(.easeInOut) {
highlightedBackground.toggle()
}
}
)
.onTapGesture {
position = position == .bottom ? .top : .bottom
}
}
private func position(from cardPosition: SlideCardPosition) -> CGFloat {
switch cardPosition {
case .top: return UIScreen.screenHeight - contentHeight - UIScreen.topSafeAreaHeight
case .bottom: return 1000
}
}
}
enum SlideCardPosition {
case top
case bottom
}
private enum SlideDragState {
case inactive
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive:
return .zero
case .dragging(let translation):
return translation
}
}
var isDragging: Bool {
switch self {
case .inactive:
return false
case .dragging:
return true
}
}
}
private struct Handle: View {
private let handleThickness: CGFloat = 5
var body: some View {
RoundedRectangle(cornerRadius: handleThickness / 2.0)
.frame(width: 34, height: handleThickness)
.foregroundColor(.white)
.padding(.top, 8)
}
}
import UIKit
extension UIScreen {
static let screenWidth = UIScreen.main.bounds.size.width
static let screenHeight = UIScreen.main.bounds.size.height
static let screenSize = UIScreen.main.bounds.size
private static let window = UIApplication.shared.windows[0]
private static let safeFrame = window.safeAreaLayoutGuide.layoutFrame
static var topSafeAreaHeight: CGFloat {
safeFrame.minY
}
static var bottomSafeAreaHeight: CGFloat {
window.frame.maxY - safeFrame.maxY
}
}
import SwiftUI
struct PopUpUIView: View {
#Binding var shouldShow: SlideCardPosition
#State var text1 = "some random text"
var body: some View {
SlideOverCard(position: $shouldShow,
contentHeight: 300) {
VStack(spacing: 10) {
Text(text1)
.foregroundColor(.white)
.padding(.top, 80)
}
}.onChange(of: shouldShow) { _ in
if shouldShow == .top {
text1 = UUID().uuidString
}
}
}
}
struct PopUpUIView_Previews: PreviewProvider {
static var previews: some View {
PopUpUIView(shouldShow: .constant(.bottom))
}
}
Example of incorrect animation with a dynamic text.
Example of what I want to achieve. It is working fine if text is static.

SwiftUI Focus State API environment variable not working

Environment value isFocused doesn't seem to work when we want to observe focus state of SwiftUI textfield. Is there any other way to do this, besides passing the value to TextFieldStyle's init (which we would have to do for every Textfield)? Doesn't work on device either.
What is the preferred way of changing Textfield's appearance when its focus state changes?
Example:
SwiftUI TextFieldStyle defined as follows:
struct MyTextFieldStyle: TextFieldStyle {
#Environment(\.isFocused) var isFocused: Bool
func _body(configuration: TextField<_Label>) -> some View {
configuration
.padding()
.overlay(
RoundedRectangle(
cornerRadius: 10.0, style: .continuous
)
.stroke(isFocused ? .green : .gray, lineWidth: 3)
)
.accentColor(Color(uiColor: .white))
}
}
#if DEBUG
private struct TestView: View {
#FocusState private var focusedTextfield: FocusField?
enum FocusField: Hashable {
case textfield1, textfield2
}
var body: some View {
VStack(spacing: 16) {
TextField("hello", text: .constant("Hi"))
.textFieldStyle(MyTextFieldStyle())
.focused($focusedTextfield, equals: .textfield1)
TextField("hello", text: .constant("Hi"))
.textFieldStyle(MyTextFieldStyle())
.focused($focusedTextfield, equals: .textfield2)
}.onAppear {
focusedTextfield = .textfield1
}
}
}
struct MyTextfieldStyle_Previews: PreviewProvider {
static var previews: some View {
ZStack {
TestView()
}
}
}
#endif
// EDIT: Working solution based on https://stackoverflow.com/a/72092987/7828383
struct MyTextFieldStyle: TextFieldStyle {
#FocusState var isFocused: Bool
func _body(configuration: TextField<_Label>) -> some View {
configuration
.padding()
.focused($isFocused)
.overlay(
RoundedRectangle(
cornerRadius: 10.0, style: .continuous
)
.stroke(isFocused ? .green : .gray, lineWidth: 3)
)
.accentColor(Color(uiColor: .white))
}
}
#if DEBUG
private struct TestView: View {
#FocusState private var focusedTextfield: FocusField?
enum FocusField: Hashable {
case textfield1, textfield2
}
var body: some View {
VStack(spacing: 16) {
TextField("hello", text: .constant("Hi"))
.textFieldStyle(MyTextFieldStyle())
.focused($focusedTextfield, equals: .textfield1)
TextField("hello", text: .constant("Hi"))
.textFieldStyle(MyTextFieldStyle())
.focused($focusedTextfield, equals: .textfield2)
}.onAppear {
DispatchQueue.main.async {
focusedTextfield = .textfield1
}
}
}
}
struct MyTextFieldStyle_Previews: PreviewProvider {
static var previews: some View {
ZStack {
TestView()
}
}
}
#endif
You have met a couple of different issues:
As far as I know there is no public protocol for custom TextFieldStyles. But you can do your own TextField struct with the same behavior.
In this struct you can use another local #FocusState var. I didn't get the environment var working, but this does.
To set the initial focus in your main view you have to wait some time using asyncAfter
struct MyTextField: View {
#FocusState private var isFocused: Bool
let title: String
#Binding var text: String
init(_ title: String, text: Binding<String>) {
self.title = title
self._text = text
}
var body: some View {
TextField(title, text: $text)
.focused($isFocused) // important !
.padding()
.overlay(
RoundedRectangle(
cornerRadius: 10.0, style: .continuous
)
.stroke(isFocused ? .green : .gray, lineWidth: 3)
)
.accentColor(Color(uiColor: .red))
}
}
struct ContentView: View {
#FocusState private var focusedTextfield: FocusField?
enum FocusField: Hashable {
case textfield1, textfield2
}
#State private var input1 = "Hi"
#State private var input2 = "Hi2"
var body: some View {
VStack(spacing: 16) {
MyTextField("hello", text: $input1)
.focused($focusedTextfield, equals: .textfield1)
MyTextField("hello", text: $input2)
.focused($focusedTextfield, equals: .textfield2)
// test for changing focus
Button("Field 1") { focusedTextfield = .textfield1}
Button("Field 2") { focusedTextfield = .textfield2}
}
.padding()
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
focusedTextfield = .textfield1
}
}
}
}

Cannot convert value of type 'Int?' to expected argument type 'Binding<Int>' SwiftUI

I created a circularprogress view to be able to show a progress bar according to the steps data. But for some reason I can not reach to the step.count inside my stepView file.
This is my StepView
struct StepView: View {
private var healthStore: HealthStore?
#State private var presentClipboardView = true
#State private var steps: [Step] = [Step]()
init() {
healthStore = HealthStore()
}
private func updateUIFromStatistics(_ statisticsCollection: HKStatisticsCollection) {
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
statisticsCollection.enumerateStatistics(from: startOfDay, to: now) { (statistics, stop) in
let count = statistics.sumQuantity()?.doubleValue(for: .count())
let step = Step(count: Int(count ?? 0), date: statistics.startDate, wc: Double(count ?? 0 / 1000 ))
steps.append(step)
}
}
var body: some View {
VStack {
ForEach(steps, id: \.id) { step in
VStack {
HStack{
Text("WC")
Text("\(step.wc)")
}
HStack {
Text("\(step.count ?? 0)")
Text("Total Steps")
}
Text(step.date, style: .date)
.opacity(0.5)
CircularProgress(steps: step.count) //ERROR
Spacer()
}
}
.navigationBarBackButtonHidden(true)
}
.onAppear() {
if let healthStore = healthStore {
healthStore.requestAuthorization { (success) in
if success {
healthStore.calculateSteps { (statisticsCollection) in
if let statisticsCollection = statisticsCollection {
updateUIFromStatistics(statisticsCollection)
}
}
}
}
}
}
.onDisappear() {
self.presentClipboardView.toggle()
}
}
}
and this is my circularprogress view
struct CircularProgress: View {
var steps: Binding<Int>
var body: some View {
ZStack {
Color.progressBarColor
.edgesIgnoringSafeArea(.all)
VStack {
ZStack {
Label()
Outline(steps: steps)
}
}
}
}
}
struct Label: View {
var percentage: CGFloat = 20
var body : some View {
ZStack {
Text(String(format: "%.0f", percentage))
.font(Font.custom("SFCompactDisplay-Bold", size: 56))
}
}
}
struct Outline: View {
var steps: Binding<Int>
var percentage: CGFloat = 20
var colors : [Color] = [Color.trackProgressBarColor]
var body: some View {
ZStack {
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle()
.trim(from: 0, to: percentage * 0.01)
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.fill(AngularGradient(gradient: .init(colors: colors), center: .center, startAngle: .zero, endAngle: .init(degrees: 360)))
).animation(.spring(response: 2.0, dampingFraction: 1.0, blendDuration: 1.0))
}
}
}
I am getting this error at stepview WHILE CALLING CIRCULARPROGRESS inside the stepview. I guess I am trying to get the data in the wrong way.
I don't see necessity of binding here, so just replace corresponding places with simple Int:
struct CircularProgress: View {
var steps: Int
and
struct Outline: View {
var steps: Int

#Binding value change not being recognised in parent view

I have a connected a #Binding variable called showCodeMessage between my parent view LiveView and its child view NewTimer.
struct LiveView: View {
#ObservedObject var countdown: CountDown
#State var showCodeMessage: Bool = false {
didSet(val){
print("changed: \(val)") // doesn't print
}
}
var body: some View {
ZStack {
NewTimer(countdown: self.countdown, showCodeMessage: $showCodeMessage)
if self.showCodeMessage { // doesn't show
MessageWithButton()
.frame(minWidth: 0, maxWidth: .infinity, alignment: .bottom)
.padding(.top, 10)
.padding(20)
.opacity(self.messageOpacity2)
}
}
}
}
struct NewTimer: View {
#ObservedObject var countdown: CountDown
#Binding var showCodeMessage: Bool
#State var codeSent = false
#State var inBackground = false
var body: some View {
VStack{
Text("Timer")
.onAppear{
self.countdown.secondsLeft = 600
self.countdown.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ _ in
self.countdown.secondsLeft -= 1
// At the 9:00 minute mark, show code message
if self.countdown.secondsLeft <= 595 && !showCodeMessage && self.inBackground == false {
print("Show code message: \(self.countdown.secondsLeft)")
self.showCodeMessage = true
}
}
}
}
}
}
However, when I perform self.showCodeMessage = true - the change doesn't register in my parent view. It doesn't even detect that a change has been made.
Any idea why?
EDIT: Added other classes/structs below
class CountDown: ObservableObject {
init(secondsLeft: Int){
self.secondsLeft = secondsLeft
}
#Published var secondsLeft: Int
var timer: Timer?
func start(){
// Reset epoch
UserDefaults.standard.set(0, forKey: "epoch")
}
func stop(){
self.timer?.invalidate()
}
func performAtEnd(action: #escaping () -> Void){
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ _ in
self.secondsLeft -= 1
print("performAtEnd() \(self.secondsLeft)")
if (self.secondsLeft == 0){
print("action() \(self.secondsLeft)")
action()
self.timer?.invalidate()
}
}
}
}
struct MessageWithButton: View {
let text: String
let color: Color
let hasButton: Bool = false
#Binding var showCode: Bool
var body: some View {
HStack{
Text("\(self.text)")
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.font(.custom("AvenirNext-Bold", size: 16))
.foregroundColor(.white)
.padding(11)
MessageButton(showCode: $showCode).padding(5)
}
.background(
RoundedRectangle(cornerRadius: 5)
.foregroundColor(self.color)
)
}
}
struct MessageButton : View {
#Binding var showCode: Bool
// #AppStorage("codeResult") var codeResult = UserDefaults.standard.string(forKey: "codeResult") ?? ""
#AppStorage("codeResult") var codeResult = UserDefaults.standard.string(forKey: "codeResult") ?? ""
var body: some View {
Button(action: {self.clickedCode()}){
Text("\(buttonText())")
.font(.custom("AvenirNext-Bold", size: 18))
.fontWeight(.bold)
.foregroundColor(Color.white)
.padding(10)
}
.background(Color("LightBlack"))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.black, lineWidth: 1)
)
.padding(.trailing, 5)
}
func buttonText() -> String {
if self.showCode { return "HIDE" }
return "ENTER"
}
func clickedCode(){
// Remove 'incorrect' from codeResult so Code input can show again
self.codeResult = ""
print("clickedcode()")
self.showCode.toggle()
}
}

Resources