I want to dim the phone screen when my app is running and if there are no touch events during a certain period of time (say 10 sec) and then make the screen brighter as soon anywhere on the screen is touched again.
After searching SO, It seems like I need to create a custom UIApplication in order to process all the touches. Below is my code so far:
import UIKit
#objc(MyApplication)
class MyApplication: UIApplication {
override func sendEvent(_ event: UIEvent) {
var screenUnTouchedTimer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(self.makeScreenDim), userInfo: nil, repeats: true);
// Ignore .Motion and .RemoteControl event simply everything else then .Touches
if event.type != .touches {
super.sendEvent(event)
return
}
// .Touches only
var restartTimer = true
if let touches = event.allTouches {
// At least one touch in progress? Do not restart timer, just invalidate it
self.makeScreenBright()
for touch in touches.enumerated() {
if touch.element.phase != .cancelled && touch.element.phase != .ended {
restartTimer = false
break
}
}
}
if restartTimer {
// Touches ended || cancelled, restart timer
print("Touches ended. Restart timer")
} else {
// Touches in progress - !ended, !cancelled, just invalidate it
print("Touches in progress. Invalidate timer")
}
super.sendEvent(event)
}
func makeScreenDim() {
UIScreen.main.brightness = CGFloat(0.1)
print("makeScreenDim")
}
func makeScreenBright() {
UIScreen.main.brightness = CGFloat(0.5)
print("makeScreenBright")
}
}
The print out looks something like this:
makeScreenBright
Touches in progress. Invalidate timer
makeScreenBright
Touches ended. Restart timer
makeScreenDim
makeScreenDim
makeScreenDim
makeScreenDim
makeScreenDim
...
As you can see above there is big issue with the code, it seems like I am creating a new Timer for every touch event. I don't know how to create a static (only one) Timer in the UIApplication.
How should I be implementing only one timer in the correct way?
(I am using an Iphone7, latest version of swift and xcode)
You have to invalidate the previously created timer somewhere, otherwise you will get your described behaviour.
Store it in a property on each call to sendEvent, so you can access it the next time the method is called.
class MyApplication: UIApplication {
var screenUnTouchedTimer : Timer?
override func sendEvent(_ event: UIEvent) {
screenUnTouchedTimer?.invalidate()
screenUnTouchedTimer = Timer ......
Related
I am trying to cancel a delayed execution of a function running on the main queue, in a tap gesture, I found a way to create a cancellable DispatchWorkItem, but the issue I have is that it's getting created every time while tapping, and then when I cancel the execution, I actually cancel the new delayed execution and not the first one.
Here is a simpler example with a Timer instead of a DispatchQueue.main.asyncAfter:
.onTapGesture {
isDeleting.toggle()
let timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { timer in
completeTask()
}
if !isDeleting {
timer.invalidate()
}
}
completeTask:
private func completeTask() {
tasksViewModel.deleteTask(task: task) // task is declared above this func at the top level of the struct and so is tasksViewModel, etc.
guard let userID = userViewModel.id?.uuidString else { return }
Task {
//do some async stuff
}
}
As you can see if I click it once the timer fires, but if I click it again, another timer fires and straight away invalidates, but the first timer is still running.
So I have to find a way to create only one instance of that timer.
I tried putting it in the top level of the struct and not inside the var body but the issue now is that I can't use completeTask() because it uses variables that are declared at the same scope.
Also, can't use a lazy initialization because it is an immutable struct.
My goal is to eventually let the user cancel a timed task and reactivate it at will on tapping a button/view. Also, the timed task should use variables that are declared at the top level of the struct.
First of all you need to create a strong reference of timer on local context like so:
var timer: Timer?
and then, set the timer value on onTapGesture closure:
.onTapGesture {
isDeleting.toggle()
self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { timer in
completeTask()
}
if !isDeleting {
timer.invalidate()
}
}
and after that you can invalidate this Timer whenever you need by accessing the local variable timer like this:
func doSomething() {
timer?.invalidate()
}
that is my solution mb can help you
var timer: Timer?
private func produceWorkItem(withDelay: Double = 3) {
scrollItem?.cancel()
timer?.invalidate()
scrollItem = DispatchWorkItem.init { [weak self] in
self?.timer = Timer.scheduledTimer(withTimeInterval: withDelay, repeats: false) { [weak self] _ in
self?.goToNextPage(animated: true, completion: { [weak self] _ in self?.produceWorkItem() })
guard let currentVC = self?.viewControllers?.first,
let index = self?.pages.firstIndex(of: currentVC) else {
return
}
self?.pageControl.currentPage = index
}
}
scrollItem?.perform()
}
for stop use scrollItem?.cancel()
for start call func
Apologies in advance for any tacky code (I'm still learning Swift and SwiftUI).
I have a project where I'd like to have multiple timers in an Array count down to zero, one at a time. When the user clicks start, the first timer in the Array counts down and finishes, and a completion handler is called which shifts the array to the left with removeFirst() and starts the next timer (now the first timer in the list) and does this until all timers are done.
I also have a custom Shape called DissolvingCircle, which like Apple's native iOS countdown timer, erases itself as the timer counts down, and stops dissolving when the user clicks pause.
The problem I'm running into is that the animation only works for the first timer in the list. After it dissolves, it does not come back when the second timer starts. Well, not exactly at least: if I click pause while the second timer is running, the appropriate shape is drawn. Then when I click start again, the second timer animation shows up appropriately until that timer ends.
I'm suspecting the issue has to do with the state check I'm making. The animation only starts if timer.status == .running. In that moment when the first timer ends, its status gets set to .stopped, then it falls off during the shift, and then the new timer starts and is set to .running, so my ContentView doesn't appear to see any change in state, even though there is a new timer running. I tried researching some basic principles of Shape animations in SwiftUI and tried re-thinking how my timer's status is being set to get the desired behavior, but I can't come up with a working solution.
How can I best restart the animation for the next timer in my list?
Here is my problematic code below:
MyTimer Class - each individual timer. I set the timer status here, as well as call the completion handler passed as a closure when the timer is finished.
//MyTimer.swift
import SwiftUI
class MyTimer: ObservableObject {
var timeLimit: Int
var timeRemaining: Int
var timer = Timer()
var onTick: (Int) -> ()
var completionHandler: () -> ()
enum TimerStatus {
case stopped
case paused
case running
}
#Published var status: TimerStatus = .stopped
init(duration timeLimit: Int, onTick: #escaping (Int) -> (), completionHandler: #escaping () -> () ) {
self.timeLimit = timeLimit
self.timeRemaining = timeLimit
self.onTick = onTick //will call each time the timer fires
self.completionHandler = completionHandler
}
func start() {
status = .running
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
if (self.timeRemaining > 0) {
self.timeRemaining -= 1
print("Timer status: \(self.status) : \(self.timeRemaining)" )
self.onTick(self.timeRemaining)
if (self.timeRemaining == 0) { //time's up!
self.stop()
}
}
}
}
func stop() {
timer.invalidate()
status = .stopped
completionHandler()
print("Timer status: \(self.status)")
}
func pause() {
timer.invalidate()
status = .paused
print("Timer status: \(self.status)")
}
}
The list of timers is managed by a class I created called MyTimerManager, here:
//MyTimerManager.swift
import SwiftUI
class MyTimerManager: ObservableObject {
var timerList = [MyTimer]()
#Published var timeRemaining: Int = 0
var timeLimit: Int = 0
init() {
//for testing purposes, let's create 3 timers, each with different durations.
timerList.append(MyTimer(duration: 10, onTick: self.updateTime, completionHandler: self.myTimerDidFinish))
timerList.append(MyTimer(duration: 7, onTick: self.updateTime, completionHandler: self.myTimerDidFinish))
timerList.append(MyTimer(duration: 11, onTick: self.updateTime, completionHandler: self.myTimerDidFinish))
self.timeLimit = timerList[0].timeLimit
self.timeRemaining = timerList[0].timeRemaining
}
func updateTime(_ timeRemaining: Int) {
self.timeRemaining = timeRemaining
}
//the completion handler - where the timer gets shifted off and the new timer starts
func myTimerDidFinish() {
timerList.removeFirst()
if timerList.isEmpty {
print("All timers finished")
} else {
self.timeLimit = timerList[0].timeLimit
self.timeRemaining = timerList[0].timeRemaining
timerList[0].start()
}
print("myTimerDidFinish() complete")
}
}
Finally, the ContentView:
import SwiftUI
struct ContentView: View {
#ObservedObject var timerManager: MyTimerManager
//The following var and function take the time remaining, express it as a fraction for the first
//state of the animation, and the second state of the animation will be set to zero.
#State private var animatedTimeRemaining: Double = 0
private func startTimerAnimation() {
let timer = timerManager.timerList.isEmpty ? nil : timerManager.timerList[0]
animatedTimeRemaining = Double(timer!.timeRemaining) / Double(timer!.timeLimit)
withAnimation(.linear(duration: Double(timer!.timeRemaining))) {
animatedTimeRemaining = 0
}
}
var body: some View {
VStack {
let timer = timerManager.timerList.isEmpty ? nil : timerManager.timerList[0]
let displayText = String(timerManager.timeRemaining)
ZStack {
Text(displayText)
.font(.largeTitle)
.foregroundColor(.black)
//This is where the problem is occurring. When the first timer starts, it gets set to
//.running, and so the animation runs approp, however, after the first timer ends, and
//the second timer begins, there appears to be no state change detected and nothing happens.
if timer?.status == .running {
DissolvingCircle(startAngle: Angle.degrees(-90), endAngle: Angle.degrees(animatedTimeRemaining*360-90))
.onAppear {
self.startTimerAnimation()
}
//this code is mostly working approp when I click pause.
} else if timer?.status == .paused || timer?.status == .stopped {
DissolvingCircle(startAngle: Angle.degrees(-90), endAngle: Angle.degrees(Double(timer!.timeRemaining) / Double(timer!.timeLimit)*360-90))
}
}
HStack {
Button(action: {
print("Cancel button clicked")
timerManager.objectWillChange.send()
timerManager.stop()
}) {
Text("Cancel")
}
switch (timer?.status) {
case .stopped, .paused:
Button(action: {
print("Start button clicked")
timerManager.objectWillChange.send()
timer?.start()
}) {
Text("Start")
}
case .running:
Button(action: {
print("Pause button clicked")
timerManager.objectWillChange.send()
timer?.pause()
}){
Text("Pause")
}
case .none:
EmptyView()
}
}
}
}
}
Screenshots:
First timer running, animating correctly.
Second timer running, animation now gone.
I clicked pause on the third timer, ContentView noticed state change. If I click start from here, the animation will work again until the end of the timer.
Please let me know if I can provide any additional code or discussion. I'm glad to also receive recommendations to make other parts of my code more elegant.
Thank you in advance for any suggestions or assistance!
I may have found one appropriate answer, similar to one of the answers in How can I get data from ObservedObject with onReceive in SwiftUI? describing the use of .onReceive(_:perform:) with an ObservableObject.
Instead of presenting the timer's status in a conditional to the ContentView, e.g. if timer?.status == .running and then executing the timer animation function during .onAppear, instead I passed the timer's status to .onReceive like this:
if timer?.status == .paused || timer?.status == .stopped {
DissolvingCircle(startAngle: Angle.degrees(-90), endAngle: Angle.degrees(Double(timer!.timeRemaining) / Double(timer!.timeLimit)*360-90))
} else {
if let t = timer {
DissolvingCircle(startAngle: Angle.degrees(-90), endAngle: Angle.degrees(animatedTimeRemaining*360-90))
.onReceive(t.$status) { _ in
self.startTimerAnimation()
}
}
The view receives the new timer's status, and plays the animation when the new timer starts.
I have a sliderValueChange function which updates a UILabel's text. I want for it to have a time limit until it clears the label's text, but I also want this "timed clear" action to be cancelled & restarted or delayed whenever the UISlider is moved within the time limit before the "timed clear" action takes place.
So far this is what I have:
let task = DispatchWorkItem {
consoleLabel.text = ""
}
func volumeSliderValueChange(sender: UISlider) {
task.cancel()
let senderValue = String(format: "%.2f", sender.value)
consoleLabel.text = "Volume: \(senderValue)"
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: task)
}
Obviously, this approach does not work, since cancel() apparently cannot be reversed.. (or at least I don't know how). I also don't know how to start a new task at the end of this function which will be cancelled if the function is recalled..
Am I going about this the wrong way? Is there something I am overlooking to make this work?
Use a timer:
weak var clearTimer: Timer?
And:
override func viewDidLoad() {
super.viewDidLoad()
startClearTimer()
}
func startClearTimer() {
clearTimer = Timer.scheduledTimer(
timeInterval: 3.0,
target: self,
selector: #selector(clearLabel(_:)),
userInfo: nil,
repeats: false)
}
func clearLabel(_ timer: Timer) {
label.text = ""
}
func volumeSliderValueChange(sender: UISlider) {
clearTimer?.invalidate() //Kill the timer
//do whatever you need to do with the slider value
startClearTimer() //Start a new timer
}
The problem is that you are cancelling the wrong thing. You don't want to cancel the task; you want to cancel the countdown which you got going when you said asyncAfter.
So use a DispatchTimer or an NSTimer (now called a Timer in Swift). Those are counters-down that can be cancelled. And then you can start counting again.
I'm developing a Sprite Kit Game, which I have a node named hero who dodges approaching villains. I have an issue with applyImpulse for a jump action and, in this case, the hero can jump repeatedly and fly instead of dodging the villains. I'm used a boolean variable to change the status with a timer value to jump only once in 3 seconds but that is not working. Here is my code
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
if isGameOver == true {
self.restart()
}
if isstarted {
self.runAction(SKAction.playSoundFileNamed("jump.wav", waitForCompletion: false))
for touch: AnyObject in touches{
if jumpon == false { //hero has not jumped
let location = touch.locationInNode(self)
heroAction()
jumpon = true //hero jumped
}
}
} else {
isstarted = true
hero.stop()
hero.armMove()
hero.rightLegMove()
hero.leftLegMove()
treeMove()
villanMove()
let clickToStartLable = childNodeWithName("clickTostartLable")
clickToStartLable?.removeFromParent()
addGrass()
// star.generateStarWithSpawnTime()
}
}
func changeJump(){
jumpon = false // hero has landed
}
and function update is called in every second and then changeJump must be called
override func update(currentTime: CFTimeInterval) {
if isstarted == true {
let pointsLabel = childNodeWithName("pointsLabel") as MLpoints
pointsLabel.increment()
}
if jumpon == true { // checked everyframe that hero jumped
jumpingTimer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: "changeJump", userInfo: nil, repeats: true) // changing jump status
}
}
How should I update my code to make the hero jump only once in three seconds. Thanks in advance.
Some thoughts...
I suggest you use an SKAction or two instead of an NSTimer in Sprite Kit games. SKActions pause/resume appropriately when you pause/resume the scene and/or view
The update method is called ~60 times a second not every second
Checking the status of a jump in update is not need
Alternatively, you can check if the hero is on the ground before starting another jump sequence instead of using a timer. The game may frustrate the user if the hero is on the ground (and ready to jump) but the timer is still counting down
and some code...
if (!jumping) {
jumping = true
let wait = SKAction.waitForDuration(3)
let leap = SKAction.runBlock({
// Play sound and apply impulse to hero
self.jump()
})
let action = SKAction.group([leap, wait])
runAction(action) {
// This runs only after the action has completed
self.jumping = false
}
}
I search to make vibrate twice my iphone when I click on a button (like a sms alert vibration)
With AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))
I obtain just one normal vibration but I want two shorts :/.
Update for iOS 10
With iOS 10, there are a few new ways to do this with minimal code.
Method 1 - UIImpactFeedbackGenerator:
let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
feedbackGenerator.impactOccurred()
Method 2 - UINotificationFeedbackGenerator:
let feedbackGenerator = UINotificationFeedbackGenerator()
feedbackGenerator.notificationOccurred(.error)
Method 3 - UISelectionFeedbackGenerator:
let feedbackGenerator = UISelectionFeedbackGenerator()
feedbackGenerator.selectionChanged()
#import <AudioToolbox/AudioServices.h>
AudioServicesPlayAlertSound(UInt32(kSystemSoundID_Vibrate))
This is the swift function...See this article for detailed description.
This is what I came up with:
import UIKit
import AudioToolbox
class ViewController: UIViewController {
var counter = 0
var timer : NSTimer?
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func vibratePhone() {
counter++
switch counter {
case 1, 2:
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
default:
timer?.invalidate()
}
}
#IBAction func vibrate(sender: UIButton) {
counter = 0
timer = NSTimer.scheduledTimerWithTimeInterval(0.6, target: self, selector: "vibratePhone", userInfo: nil, repeats: true)
}
}
When you press the button, the timer starts and repeats at desired time interval. The NSTimer calls the vibratePhone(Void) function and from there I can control how many times the phone will vibrate. I used a switch in this case, but you could use a if else, too. Simply set a counter to count each time the function is called.
If you want to vibrate only two times. You can just..
func vibrate() {
AudioServicesPlaySystemSoundWithCompletion(kSystemSoundID_Vibrate) {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
}
}
And vibrating multiple times can be possible by using recursion and AudioServicesPlaySystemSoundWithCompletion.
You can pass a count number to vibrate function like vibrate(count: 10). Then it vibrates 10 times.
func vibrate(count: Int) {
if count == 0 {
return
}
AudioServicesPlaySystemSoundWithCompletion(kSystemSoundID_Vibrate) { [weak self] in
self?.vibrate(count: count - 1)
}
}
In case of using UIFeedbackGenerator, There is a great library Haptica
Hope it helps.