SwiftUI ActionSheet does not dismiss when timer is running - ios

I have the following simple SwiftUI setup. A timer that is running and updating a Text. If the timer is not running (stopped or paused) I can easily show an ActionSheet (by tapping on Actions) and dismiss it by choosing either "Cancel" or "Action 1" option. But if the timer is running, I have a really hard time dismissing the ActionSheet by choosing one of the "Cancel" or "Action 1" options. Do you know what's going on?
I am using Xcode 11.5.
import SwiftUI
struct ContentView: View {
#ObservedObject var stopWatch = StopWatch()
#State private var showActionSheet: Bool = false
var body: some View {
VStack {
Text("\(stopWatch.secondsElapsed)")
HStack {
if stopWatch.mode == .stopped {
Button(action: { self.stopWatch.start() }) {
Text("Start")
}
} else if stopWatch.mode == .paused {
Button(action: { self.stopWatch.start() }) {
Text("Resume")
}
} else if stopWatch.mode == .running {
Button(action: { self.stopWatch.pause() }) {
Text("Pause")
}
}
Button(action: { self.stopWatch.stop() }) {
Text("Reset")
}
}
Button(action: { self.showActionSheet = true }) {
Text("Actions")
}
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("Actions"), message: nil, buttons: [.default(Text("Action 1")), .cancel()])
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
class StopWatch: ObservableObject {
#Published var secondsElapsed: TimeInterval = 0.0
#Published var mode: stopWatchMode = .stopped
var timer = Timer()
func start() {
mode = .running
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
self.secondsElapsed += 0.1
}
}
func stop() {
timer.invalidate()
secondsElapsed = 0
mode = .stopped
}
func pause() {
timer.invalidate()
mode = .paused
}
enum stopWatchMode {
case running
case stopped
case paused
}
}

Works fine with Xcode 12 / iOS 14, but try to separate button with sheet into another subview to avoid recreate it on timer counter refresh.
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#ObservedObject var stopWatch = StopWatch()
// #StateObject var stopWatch = StopWatch() // << used for SwiftUI 2.0
#State private var showActionSheet: Bool = false
var body: some View {
VStack {
Text("\(stopWatch.secondsElapsed)")
HStack {
if stopWatch.mode == .stopped {
Button(action: { self.stopWatch.start() }) {
Text("Start")
}
} else if stopWatch.mode == .paused {
Button(action: { self.stopWatch.start() }) {
Text("Resume")
}
} else if stopWatch.mode == .running {
Button(action: { self.stopWatch.pause() }) {
Text("Pause")
}
}
Button(action: { self.stopWatch.stop() }) {
Text("Reset")
}
}
ActionsSubView(showActionSheet: $showActionSheet)
}
}
}
struct ActionsSubView: View {
#Binding var showActionSheet: Bool
var body: some View {
Button(action: { self.showActionSheet = true }) {
Text("Actions")
}
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("Actions"), message: nil, buttons: [.default(Text("Action 1")), .cancel()])
}
}
}

Related

Swiftui view doesn't refresh when navigated to from a different view

I have, what is probably, a beginner question here. I'm hoping there is something simple I'm missing or I have done wrong.
I essentially have a view which holds a struct containing an array of id strings. I then have a #FirestoreQuery which accesses a collection which holds objects with these id's. My view then displays a list with two sections. One for the id's in the original struct, and one for the remaining ones in the collection which don't appear in the array.
Each listitem is a separate view which displays the details of that item and also includes a button. When this button is pressed it adds/removes that object from the parent list and the view should update to show that object in the opposite section of the list from before.
My issue is that this works fine in the 'preview' in xcode when I look at this view on it's own. However if I run the app in the simulator, or even preview a parent view and navigate to this one, the refreshing of the view doesn't seem to work. I can press the buttons, and nothing happens. If i leave the view and come back, everything appears where it should.
I'll include all the files below. Is there something I'm missing here?
Thanks
Main view displaying the list with two sections
import SwiftUI
import FirebaseFirestoreSwift
struct SessionInvitesView: View {
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#Binding var sessionViewModel : TrainingSessionViewModel
#State private var searchText: String = ""
#State var refresh : Bool = false
var enrolledClients : [Client] {
return clients.filter { sessionViewModel.session.invites.contains($0.id!) }
}
var availableClients : [Client] {
return clients.filter { !sessionViewModel.session.invites.contains($0.id!) }
}
var searchFilteredClients : [Client] {
if searchText.isEmpty {
return availableClients
} else {
return availableClients.filter {
$0.dogName.localizedCaseInsensitiveContains(searchText) ||
$0.name.localizedCaseInsensitiveContains(searchText) ||
$0.dogBreed.localizedCaseInsensitiveContains(searchText) }
}
}
var backButton: some View {
Button(action: { self.onCancel() }) {
Text("Back")
}
}
var body: some View {
NavigationView {
List {
Section(header: Text("Enrolled")) {
ForEach(enrolledClients) { client in
SessionInviteListItem(client: client, isEnrolled: true, onTap: removeClient)
}
}
Section(header: Text("Others")) {
ForEach(searchFilteredClients) { client in
SessionInviteListItem(client: client, isEnrolled: false, onTap: addClient)
}
}
}
.listStyle(.insetGrouped)
.searchable(text: $searchText)
.navigationTitle("Invites")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: backButton)
}
}
func removeClient(clientId: String) {
self.sessionViewModel.session.invites.removeAll(where: { $0 == clientId })
refresh.toggle()
}
func addClient(clientId: String) {
self.sessionViewModel.session.invites.append(clientId)
refresh.toggle()
}
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
}
struct SessionInvitesView_Previews: PreviewProvider {
#State static var model = TrainingSessionViewModel()
static var previews: some View {
SessionInvitesView(sessionViewModel: $model)
}
}
List item view
import SwiftUI
struct SessionInviteListItem: View {
var client : Client
#State var isEnrolled : Bool
var onTap : (String) -> ()
var body: some View {
HStack {
VStack(alignment: .leading) {
HStack {
Text(client.dogName.uppercased())
.bold()
Text("(\(client.dogBreed))")
}
Text(client.name)
.font(.subheadline)
}
Spacer()
Button(action: { onTap(client.id!) }) {
Image(systemName: self.isEnrolled ? "xmark.circle.fill" : "plus.circle.fill")
}
.buttonStyle(.borderless)
.foregroundColor(self.isEnrolled ? .red : .green)
}
}
}
struct SessionInviteListItem_Previews: PreviewProvider {
static func doNothing(_ : String) {}
static var previews: some View {
SessionInviteListItem(client: buildSampleClient(), isEnrolled: false, onTap: doNothing)
}
}
Higher level view used to navigate to this list view
import SwiftUI
import FirebaseFirestoreSwift
struct TrainingSessionEditView: View {
// MARK: - Member Variables
#Environment(\.presentationMode) private var presentationMode
#FirestoreQuery(collectionPath: "clients") var clients : [Client]
#StateObject var sheetManager = SheetManager()
var mode: Mode = .new
var dateManager = DateManager()
#State var viewModel = TrainingSessionViewModel()
#State var sessionDate = Date.now
#State var startTime = Date.now
#State var endTime = Date.now.addingTimeInterval(3600)
var completionHandler: ((Result<Action, Error>) -> Void)?
// MARK: - Local Views
var cancelButton: some View {
Button(action: { self.onCancel() }) {
Text("Cancel")
}
}
var saveButton: some View {
Button(action: { self.onSave() }) {
Text("Save")
}
}
var addInviteButton : some View {
Button(action: { sheetManager.showInvitesSheet.toggle() }) {
HStack {
Text("Add")
Image(systemName: "plus")
}
}
}
// MARK: - Main View
var body: some View {
NavigationView {
List {
Section(header: Text("Details")) {
TextField("Session Name", text: $viewModel.session.title)
TextField("Location", text: $viewModel.session.location)
}
Section {
DatePicker(selection: $sessionDate, displayedComponents: .date) {
Text("Date")
}
.onChange(of: sessionDate, perform: { _ in
viewModel.session.date = dateManager.dateToStr(date: sessionDate)
})
DatePicker(selection: $startTime, displayedComponents: .hourAndMinute) {
Text("Start Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: startTime, perform: { _ in
viewModel.session.startTime = dateManager.timeToStr(date: startTime)
})
DatePicker(selection: $endTime, displayedComponents: .hourAndMinute) {
Text("End Time")
}
.onAppear() { UIDatePicker.appearance().minuteInterval = 15 }
.onChange(of: endTime, perform: { _ in
viewModel.session.endTime = dateManager.timeToStr(date: endTime)
})
}
Section {
HStack {
Text("Clients")
Spacer()
Button(action: { self.sheetManager.showInvitesSheet.toggle() }) {
Text("Edit").foregroundColor(.blue)
}
}
ForEach(viewModel.session.invites, id: \.self) { clientID in
self.createClientListElement(id: clientID)
}
.onDelete(perform: deleteInvite)
}
Section(header: Text("Notes")) {
TextField("Add notes here...", text: $viewModel.session.notes)
}
if mode == .edit {
Section {
HStack {
Spacer()
Button("Delete Session") {
sheetManager.showActionSheet.toggle()
}
.foregroundColor(.red)
Spacer()
}
}
}
}
.navigationTitle(mode == .new ? "New Training Session" : "Edit Training Session")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
leading: cancelButton,
trailing: saveButton)
.actionSheet(isPresented: $sheetManager.showActionSheet) {
ActionSheet(title: Text("Are you sure?"),
buttons: [
.destructive(Text("Delete Session"), action: { self.onDelete() }),
.cancel()
])
}
.sheet(isPresented: $sheetManager.showInvitesSheet) {
SessionInvitesView(sessionViewModel: $viewModel)
}
}
}
func createClientListElement(id: String) -> some View {
let client = clients.first(where: { $0.id == id })
if let client = client {
return AnyView(ClientListItem(client: client))
}
else {
return AnyView(Text("Invalid Client ID: \(id)"))
}
}
func deleteInvite(indexSet: IndexSet) {
viewModel.session.invites.remove(atOffsets: indexSet)
}
// MARK: - Local Event Handlers
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func onCancel() {
self.dismiss()
}
func onSave() {
self.viewModel.onDone()
self.dismiss()
}
func onDelete() {
self.viewModel.onDelete()
self.dismiss()
self.completionHandler?(.success(.delete))
}
// MARK: - Sheet Management
class SheetManager : ObservableObject {
#Published var showActionSheet = false
#Published var showInvitesSheet = false
}
}
struct TrainingSessionEditView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionEditView(viewModel: TrainingSessionViewModel(session: buildSampleTrainingSession()))
}
}
I'm happy to include any of the other files if you think it would help. Thanks in advance!

Creating an iOS passcode view with SwiftUI, how to hide a TextView?

I am trying to imitate a lock screen of iOS in my own way with some basic code. However I do not understand how to properly hide an input textview. Now I am using an opacity modifier, but it does not seem to be the right solution. Could you please recommend me better options?
import SwiftUI
public struct PasscodeView: View {
#Environment(\.dismiss) var dismiss
#ObservedObject var viewModel: ContentView.ViewModel
private let maxDigits: Int = 4
private let userPasscode = "1234"
#State var enteredPasscode: String = ""
#FocusState var keyboardFocused: Bool
#State private var showAlert = false
#State private var alertMessage = "Passcode is wrong, try again!"
public var body: some View {
VStack {
HStack {
ForEach(0 ..< maxDigits) {
($0 + 1) > enteredPasscode.count ?
Image(systemName: "circle") :
Image(systemName: "circle.fill")
}
}
.alert("Wrong Passcode", isPresented: $showAlert) {
Button("OK", role: .cancel) { }
}
TextField("Enter your passcode", text: $enteredPasscode)
.opacity(0)
.keyboardType(.decimalPad)
.focused($keyboardFocused)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
keyboardFocused = true
}
}
}
.padding()
.onChange(of: enteredPasscode) { _ in
guard enteredPasscode.count == maxDigits else { return }
passcodeValidation()
}
}
func passcodeValidation() {
if enteredPasscode == userPasscode {
viewModel.isUnlocked = true
dismiss()
} else {
enteredPasscode = ""
showAlert = true
}
}
}

SwiftUI: stuck on infinite page view implementation

I'm trying to create a PageView in pure SwiftUI. There's my test code below. And everything works as expected but the DragGesture. It just doesn't call 'onEnded' function. Never. How can I fix it?
struct PageView<V: View>: Identifiable {
let id = UUID()
var content: V
}
struct InfinitePageView: View {
#State private var pages: [PageView] = [
PageView(content: Text("Page")),
PageView(content: Text("Page")),
PageView(content: Text("Page"))
]
#State private var selectedIndex: Int = 1
#State private var isDragging: Bool = false
private var drag: some Gesture {
DragGesture()
.onChanged { _ in
self.isDragging = true
}
.onEnded { _ in
self.isDragging = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
resolvePages()
}
}
}
var body: some View {
NavigationView {
TabView(selection: $selectedIndex) {
ForEach(pages) { page in
page.content
.tag(pages.firstIndex(where: { $0.id == page.id })!)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.gesture(drag)
.onChange(of: selectedIndex, perform: { value in
guard !isDragging else { return }
DispatchQueue.main.async {
resolvePages()
}
})
}
}
private func resolvePages() {
if selectedIndex > 1 {
addNextPage()
}
if selectedIndex < 1 {
addPreviousPage()
}
}
private func addNextPage() {
pages.append(PageView(content: Text("Page")))
pages.removeFirst()
selectedIndex = 1
}
private func addPreviousPage() {
pages.insert(PageView(content: Text("Page")), at: 0)
pages.removeLast()
selectedIndex = 1
}
}
This is a known issue with SwiftUI
the DragGesture that you setup likely gets overridden by the DragGesture within the TabView.
Can detect onEnded with the setup in this post Detect DragGesture cancelation in SwiftUI but that deactivates the TabView/Page gestures.
Your onChange code for selectedIndex runs once the new page is selected and would act should act the same as onEnded.

Im trying to make a an app that calculates the WPM

Im trying to make a an app that calculates the WPM. In the end of the game I would like to use a timer to stop the app after 60 seconds. I can't figure out how to stop it. I'm trying to stop it with conditional statement. But I don't know how to implement it with SwiftUI. If anyone had any other ideas that would be great.
import SwiftUI
struct ContentView: View {
#State var userInput = ""
#State var modalview = false
#State var getstarted = false
#EnvironmentObject var timerHolder : TimerHolder
var body: some View {
ZStack() {
modalView(modalview: $modalview, userInput: userInput)
}.sheet(isPresented: $modalview) {
modalView(modalview: self.$modalview)
}
}
}
struct modalView : View {
#ObservedObject var durationTimer = TimerHolder()
#Binding var modalview : Bool
#State var userInput: String = ""
var body: some View {
VStack{
Button(action: {
self.modalview = true
}) {
TextField("Get Started", text:$userInput)
.background(Color.gray)
.foregroundColor(.white)
// .frame(width: 300, height: 250).cornerRadius(20)
}
Text("\(userInput.count)")
if durationTimer == 60 {
.alert(isPresented: $showAlert) {
Alert(title: Text("Reminder"), message: Text("You wrote"), primaryButton: .default(Text("Yes"), action: { self.presentationMode.wrappedValue.dismiss() })
, secondaryButton: .cancel(Text("No")))
}; else {
}
}
}
}
class TimerHolder : ObservableObject {
var timer : Timer!
#Published var count = 0
func start() {
self.timer?.invalidate()
self.count = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
_ in
self.count += 1
print(self.count)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
I changed some codes to give you a clue.
struct ContentView: View {
#State var userInput = ""
#State var modalview = false
#State var getstarted = false
#EnvironmentObject var timerHolder : TimerHolder
var body: some View {
ZStack() {
modalView(modalview: $modalview, userInput: userInput)
}.sheet(isPresented: $modalview) {
modalView(modalview: self.$modalview)
}
}
}
struct modalView : View {
#ObservedObject var durationTimer = TimerHolder()
#Binding var modalview : Bool
#State var userInput: String = ""
var body: some View {
VStack{
Button(action: {
self.modalview = true
}) {
TextField("Get Started", text:$userInput)
.background(Color.gray)
.foregroundColor(.white)
// .frame(width: 300, height: 250).cornerRadius(20)
}
Text("\(userInput.count)").alert(isPresented: self.$durationTimer.count) {
Alert(title: Text("Reminder"),
message: Text("You wrote"),
primaryButton: .default(Text("Yes"), action: { self.presentationMode.wrappedValue.dismiss()
print(123)
})
,
secondaryButton: .cancel(Text("No")))
}
}
}
}
class TimerHolder : ObservableObject {
var timer : Timer!
#Published var count = false
init(){
self.start()
}
func start() {
self.timer?.invalidate()
self.count = false
self.timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) {
_ in
self.count = true
print(self.count)
}
}
}

How could I simply have a View transition to SwiftUI View?

I'd like to implement a simple view transition through SwiftUI and Timer.
I have a primary View, it's content View. If I call func FireTimer() from in the View, the function fires timer. Then after 5 seconds, I would have a View transition.
I tried NavigationLink, but it has a button. Timer can't push the button so now I'm confused.
I'll show my code below.
TimerFire.swift
import Foundation
import UIKit
import SwiftUI
let TIME_MOVENEXT = 5
var timerCount : Int = 0
class TimerFire : ObservableObject{
var workingTimer = Timer()
#objc func FireTimer() {
print("FireTimer")
workingTimer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(TimerFire.timerUpdate),
userInfo: nil,
repeats: true)
}
#objc func timerUpdate(timeCount: Int) {
timerCount += 1
let timerText = "timerCount:\(timerCount)"
print(timerText)
if timerCount == TIME_MOVENEXT {
print("timerCount == TIME_MOVENEXT")
workingTimer.invalidate()
print("workingTimer.invalidate()")
timerCount = 0
//
//want to have a transition to SecondView here
//
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
Button(action: {
// What to perform
let timerFire = TimerFire()
timerFire.FireTimer()
}) {
// How the button looks like
Text("Fire timer")
}
}
}
SecondView.swift
import Foundation
import SwiftUI
struct SecondView: View {
var body: some View {
Text("Second World")
}
}
How could I simply show this SecondView?
Ok, if you want to do this w/o NavigationView on first screen (for any reason) here is a possible approach based on transition between two views.
Note: Preview has limited support for transitions, so please test on Simulator & real device
Here is a demo how it looks (initial white screen is just Simulator launch)
Here is single testing module:
import SwiftUI
import UIKit
let TIME_MOVENEXT = 5
var timerCount : Int = 0
class TimerFire : ObservableObject{
var workingTimer = Timer()
#Published var completed = false
#objc func FireTimer() {
print("FireTimer")
workingTimer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(TimerFire.timerUpdate),
userInfo: nil,
repeats: true)
}
#objc func timerUpdate(timeCount: Int) {
timerCount += 1
let timerText = "timerCount:\(timerCount)"
print(timerText)
if timerCount == TIME_MOVENEXT {
print("timerCount == TIME_MOVENEXT")
workingTimer.invalidate()
print("workingTimer.invalidate()")
timerCount = 0
//
//want to have a transition to SecondView here
//
self.completed = true
}
}
}
struct SecondView: View {
var body: some View {
Text("Second World")
}
}
struct TestTransitionByTimer: View {
#ObservedObject var timer = TimerFire()
#State var showDefault = true
var body: some View {
ZStack {
Rectangle().fill(Color.clear) // << to make ZStack full-screen
if showDefault {
Rectangle().fill(Color.blue) // << just for demo
.overlay(Text("Hello, World!"))
.transition(.move(edge: .leading))
}
if !showDefault {
Rectangle().fill(Color.red) // << just for demo
.overlay(SecondView())
.transition(.move(edge: .trailing))
}
}
.onAppear {
self.timer.FireTimer()
}
.onReceive(timer.$completed, perform: { completed in
withAnimation {
self.showDefault = !completed
}
})
}
}
struct TestTransitionByTimer_Previews: PreviewProvider {
static var previews: some View {
TestTransitionByTimer()
}
}
There is no code snippet for ContentView, so I tried to build simple example by myself. You can use NavigationLink(destination: _, isActive: Binding<Bool>, label: () -> _) in your case. Change some State var while receiving changes from Timer.publish and you'll go to SecondView immediately:
struct TransitionWithTimer: View {
#State var goToSecondWorld = false
let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: SecondWorld(), isActive: self.$goToSecondWorld) {
Text("First World")
.onReceive(timer) { _ in
self.goToSecondWorld = true
}
}
}
}
}
}
// you can use ZStack and opacity/offset of view's:
struct TransitionWithTimerAndOffset: View {
#State var goToSecondWorld = false
let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Text("First world") // here can be your first View
.opacity(self.goToSecondWorld ? 0 : 1)
.offset(x: goToSecondWorld ? 1000 : 0)
Text("Second world") // and here second world View
.opacity(self.goToSecondWorld ? 1 : 0)
.offset(x: goToSecondWorld ? 0 : -1000)
}
.onReceive(timer) { _ in
withAnimation(.spring()) {
self.goToSecondWorld = true
}
}
}
}
struct SecondWorld: View {
var body: some View {
Text("Second World")
}
}

Resources