Swift - Using a Timer to detect user inactivity then display an Alert after 4 minutes to prompt the user if they are still there - ios

I have a basic application for iPads which consists of 4 views. Across the whole app, I want to be able to detect user inactivity and then display an Alert after 4 minutes which will ask the user if they are still there.
I have found some useful resources for the Timer and Alert functions. I have played around with these tutorials and can get the Timer working on it's own. However, this is my first time developing in Swift so I would like some guidance on the best way to connect the Timer to the Alert is? I would like the Timer to run for 4 minutes and then create the Alert.
I would also like to know where the best place is to put the code for these elements so that they work across all 4 of my views. E.g is there a single place I can put the code and reuse it rather than having it repeated in each of the 4 views?

First you can follow the instructions here to set up your timer: https://blog.gaelfoppolo.com/detecting-user-inactivity-in-ios-application-684b0eeeef5b
Then you can do something like this to handle the timeout notifications:
extension UIViewController {
func observeTimeout() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleTimeout),
name: .appTimeout,
object: nil)
}
#objc func handleTimeout() {
let alert = UIAlertController(title: "Timeout", message: "Oh no!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
}))
present(alert, animated: true, completion: nil)
}
}
And then in each of your view controllers do this to register for the timeout:
class SomeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
observeTimeout()
}
}

It's been a while since this question was asked however, since I came across the same problem I slightly changed the answer #rob-c provided using this guide:
Tested on Swift 5.5 and iOS 15.1
1- Create a subclass of UIApplication
import Foundation
import UIKit
class TimerApplication: UIApplication {
private var timeoutInSeconds: TimeInterval {
return 40.0
}
private var idleTimer: Timer?
override init() {
super.init()
resetIdleTimer()
}
private func resetIdleTimer() {
if let idleTimer = idleTimer {
idleTimer.invalidate()
}
idleTimer = Timer.scheduledTimer(timeInterval: timeoutInSeconds,
target: self,
selector: #selector(TimerApplication.timeHasExceeded),
userInfo: nil,
repeats: false
)
}
#objc private func timeHasExceeded() {
NotificationCenter.default.post(name: .appTimeout, object: nil)
}
override func sendEvent(_ event: UIEvent) {
super.sendEvent(event)
if idleTimer != nil {
self.resetIdleTimer()
}
if let touches = event.allTouches {
for touch in touches where touch.phase == UITouch.Phase.began {
self.resetIdleTimer()
}
}
}
}
2- Add notification name
import Foundation
extension Notification.Name {
static let appTimeout = Notification.Name("appTimeout")
}
3- Create main.swift file and add this
import Foundation
import UIKit
/// NOTE: comment out #UIApplicationMain in AppDelegate
UIApplicationMain(
CommandLine.argc,
CommandLine.unsafeArgv,
NSStringFromClass(TimerApplication.self),
NSStringFromClass(AppDelegate.self)
)
4- Remove #UIApplicationMain from AppDelegate
5- Add observer for the notification
Add the observer where appropriate for your case, in AppDelegate or any view controller:
func addObservers() {
NotificationCenter.default.addObserver(self,
selector: #selector(idleTimeLimitReached(_:)),
name: .appTimeout,
object: nil)
}
#objc func idleTimeLimitReached(_ notification: Notification) {
print("***** IDLE Time called")
}

You can run Timer in AppDelegate itself and display alert like below from AppDelegate
func showAlertFromAppDelegates(){
let alertVC = UIAlertController(title: "Oops" , message: "Presented Alert from AppDelegates", preferredStyle: UIAlertController.Style.alert)
let okAction = UIAlertAction(title: "Okay", style: UIAlertAction.Style.cancel) { (alert) in
}
alertVC.addAction(okAction)
var presentVC = self.window!.rootViewController
while let next = presentVC?.presentedViewController
{
presentVC = next
}
presentVC?.present(alertVC, animated: true, completion: nil)
}
Edit:
Timer related code added, call startTimer() when you want to start inactivity and call stopTimer() when inactivity ends
func startTimer()
{
let interval = 4*60
timer = Timer.scheduledTimer(timeInterval: TimeInterval(interval), target: self, selector: #selector(self.showAlertFromAppDelegates), userInfo: nil, repeats: true)
}
func stopTimer()
{
if let timer = timer
{
timer.invalidate()
}
timer = nil
}

Related

How to show a popup after 20 sec of an app open every time in swift

I need to show a popup every time when i open the app after 20 sec.
code: with this code i can show popup only when i open the app first time after 20 sec.. but i need to show the same when i close the app and open again.. how to do that? please guide me.
var timer = Timer()
override func viewDidLoad() {
timer = Timer.scheduledTimer(timeInterval: 20.0, target: self, selector: #selector(displayAlert), userInfo: nil, repeats: false)
}
#objc func displayAlert()
{
print("after 20 sec")
showPopup()
}
You want to register for notification when your app becomes active...
For example:
override func viewDidLoad() {
super.viewDidLoad()
// all your normal stuff...
// register to receive notification when App becomes active
NotificationCenter.default.addObserver(self, selector: #selector(startAlertTimer), name: UIApplication.didBecomeActiveNotification, object: nil)
}
#objc func startAlertTimer()
{
print("start 20 sec timer")
Timer.scheduledTimer(timeInterval: 20.0, target: self, selector: #selector(showPopup), userInfo: nil, repeats: false)
}
#objc func showPopup() {
print("after 20 seconds, popup")
}
And if you use a DispatchQueue:
DispatchQueue.main.asyncAfter(deadline: .now() + 20) {
// Do something here..
}

How to make a global timer for the whole app

I want to make a timer that start on app load and when it finishes to alert a text and restart the app when alert is closed(when click the ok button of the alert). Have tried with a class timer but I can't send alert and reload the app. I need it to be working in the whole app, and when move to other view not restart it countdown. Is there any easy way without using CocoaPods.
import Foundation
class GlobalTokenTimer {
static let sharedTimer: GlobalTokenTimer = {
let timer = GlobalTokenTimer()
return timer
}()
var internalTimer: Timer?
var jobs = [() -> Void]()
func startTimer(withInterval interval: Double, andJob job: #escaping () -> Void) {
if internalTimer != nil {
internalTimer?.invalidate()
}
jobs.append(job)
internalTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(doJob), userInfo: nil, repeats: true)
}
func stopTimer() {
guard internalTimer != nil else {
print("No timer active, start the timer before you stop it.")
return
}
jobs = [()->()]()
internalTimer?.invalidate()
}
#objc func doJob() {
guard jobs.count > 0 else{return}
}
}
Your timer is static and will work while app is alive. Implement some function that will show alert on topViewController of your application.
extension UIApplication {
// function that lets you get topViewController of the app
func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}
Inside your app delegate start your timer and show alert. In iOS you can't restart you app, but can set newly created MyStartViewController which is your home page for example and restart logic of your app.
UPD:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func startTimer() {
GlobalTokenTimer.sharedTimer.startTimer(withInterval: 10) {
let alertVC = UIAlertController(title: "Restart your app!", message: "Select ok!", preferredStyle: .alert)
alertVC.addAction(UIAlertAction(title: "Restart", style: .default) { [weak self] _ in
self?.window?.rootViewController = MyStartViewController()
startTimer()
})
application.topViewController()?.present(alertVC, animated: true)
}
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
startTimer()
}
}
Set your timer repeat: false and it would stop after finishing once.
internalTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(doJob), userInfo: nil, repeats: false)
UPD:
You do nothing in your doJob function, change it to this.
#objc func doJob() {
guard jobs.count > 0 else { return }
jobs.forEach { $0() }
}

NotificationCenter to pass data in Swift

I am working on a test project in Swift 3. I am trying to pass textField string from one class to another class using NotificationCenter. I am trying to workout the answer from this link: pass NSString variable to other class with NSNotification and how to pass multiple values with a notification in swift
I tried few answers from the above link but nothing worked.
My code:
//First VC
import UIKit
extension Notification.Name {
public static let myNotificationKey = Notification.Name(rawValue: "myNotificationKey")
}
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func sendData(_ sender: AnyObject) {
let userInfo = [ "text" : textView.text ]
NotificationCenter.default.post(name: .myNotificationKey, object: nil, userInfo: userInfo)
}
}
//SecondVC
import Foundation
import UIKit
class viewTwo: UIViewController {
#IBOutlet weak var result: UILabel!
override func viewDidLoad() {
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(self.notificationReceived(_:)), name: .myNotificationKey, object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self, name: .myNotificationKey, object: nil)
}
func notificationReceived(_ notification: Notification) {
guard let text = notification.userInfo?["text"] as? String else { return }
print ("text: \(text)")
result.text = text
}
}
I am not sure whats wrong with the code. Above, code originally marked as a answered which, I found from the first link. Code been converted to Swift.
Don't use object parameter to pass data. It is meant to filter notifications with the same name, but from a particular object. So if you pass some object when you post a notification and another object when you addObserver, you won't receive it. If you pass nil, you basically turn off this filter.
You should use userInfo parameter instead.
First, it is better to define notification's name as extension for Notification.Name. This approach is much safer and more readable:
extension Notification.Name {
public static let myNotificationKey = Notification.Name(rawValue: "myNotificationKey")
}
Post notification:
let userInfo = [ "text" : text ]
NotificationCenter.default.post(name: .myNotificationKey, object: nil, userInfo: userInfo)
Subscribe:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(self.notificationReceived(_:)), name: .myNotificationKey, object: nil)
}
Unsubscribe:
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self, name: .myNotificationKey, object: nil)
}
Method to be called:
func notificationReceived(_ notification: Notification) {
guard let text = notification.userInfo?["text"] as? String else { return }
print ("text: \(text)")
}
Pass text using userInfo which is a optional Dictionary of type [AnyHashable:Any]? in Swift 3.0 and it is [NSObject : AnyObject]? in swift 2.0
#IBAction func sendData(_ sender: UIButton) {
// post a notification
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "notificationName"), object: nil, userInfo: ["text": textValue.text])
print(textValue) // textValue printing
}
in viewDidLoad
// Register to receive notification
NotificationCenter.default.addObserver(self, selector: #selector(self. incomingNotification(_:)), name: NSNotification.Name(rawValue: "notificationName"), object: nil)
and in incomingNotification
func incomingNotification(_ notification: Notification) {
if let text = notification.userInfo?["text"] as? String {
print(text)
// do something with your text
}
}
In your sendData method pass textField.text into Notification object and in your incomingNotification do this:
guard let theString = notification.object as? String else {
print("something went wrong")
return
}
resultLabel.text = theString
You can also use blocks to pass data between controllers.
Use dispatchQueue because your notification is posting before your view load. Therefore just give delay in your notification Post.
#IBAction func sendData(_ sender: AnyObject) {
let userInfo = [ "text" : textView.text ]
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
NotificationCenter.default.post(name: .myNotificationKey, object: nil, userInfo: userInfo) }
}
Just use NotificationCenter to send and receive the notification that state changed. Pass the data through some a data model such as an ObservableObject (particularly if you're bridging between SwiftUI and UIKit). Here's are a couple of extension that make it pretty simple for lightweight inter-component signaling without the cumbersome forgettable semantics of NotificationCenter. (Of course you define your own Notification.Name constants to be meaningful to your purpose).
extension Notification.Name {
static let startEditingTitle = Notification.Name("startEditingTitle")
static let stopEditingTitle = Notification.Name("stopEditingTitle")
}
extension NotificationCenter {
static func wait(_ name : Notification.Name) async {
for await _ in NotificationCenter.default.notifications(named: name) {
break;
}
}
static func post(_ name : Notification.Name) {
NotificationCenter.default.post(name: name, object: nil)
}
#discardableResult static func postProcessing(_ name: Notification.Name, using block: #escaping (Notification) -> Void) -> NSObjectProtocol {
NotificationCenter.default.addObserver(forName: name, object: nil, queue: OperationQueue.main, using: block)
}
}
To post a notification is as simple as:
NotificationCenter.post(.startEditingTitle)
And to receive the notification elsewhere:
NotificationCenter.postProcessing(.startEditingTitle) (_ in {
print("Started editing title")
}
Or to just wait for the notification instead of asynchronously handling it:
NotificationCenter.wait(.startEditingTitle)

NSTimer invalidate not work

I can not stop the timer NSTimer created to perform secondary operations.
I have read and tried all sorts of possible Web guides but none have worked and the timer continues to run even when the Controller is destroyed. Below is the code in question:
/* Copy banner in a temp array and change order to random value */
private func getBanners(){
let realm = try! Realm()
let banners = self.synchronizer.getAllBanners()?.sorted("ordine")
if let banners = banners {
for banner in banners {
let random = Double.random(0.0, 1.0)
let currentOrderValue = banner.ordine
self.banners.append(banner)
do {
try realm.write({
self.banners.last!.ordine = currentOrderValue + random
})
} catch let error as NSError {
print(error)
}
}
}
}
/*Update index of banner to show */
func updateIndex(timer : NSTimer) {
if self.index+1 < self.banners.count {
for banner in self.banners{
print(banner.name + " \(banner.ordine)")
}
self.index+=1
} else {
self.index = 0
}
self.setImageBanner()
}
/* Set image of banner to show*/
private func setImageBanner() {
if self.banners.count > 0 {
self.bannerImage.hidden = false
let banner = self.banners[self.index]
let file = banner.images[0]
self.synchronizer.loadImageURL(file.link, imageView: self.bannerImage)
} else {
self.bannerImage.hidden = true
}
}
/* Start Timer */
func startTimerForBanners() {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.getBanners()
})
self.timer = NSTimer(timeInterval: 5, target: self, selector: #selector(self.updateIndex(_:)), userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(self.timer!, forMode: NSRunLoopCommonModes)
}
/* Open link on banner click */
func openLink(sender : UITapGestureRecognizer){
if self.index >= 0 && self.index < self.banners.count {
let banner = self.banners[self.index]
print(banner.name)
self.synchronizer.openLink(banner.url)
}
}
func trialLesson(sender : UITapGestureRecognizer){
performSegueWithIdentifier("trial_lesson_segue", sender: nil)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if let timer = self.timer {
timer.invalidate()
}
}
you need to invalidate the timer before starting a new timer. Currently I believe you are calling "startTimerForBanners" many times may be, so many timer thread has been initiated. One approach is to invlidate all timer one by one or just invalidate before starting new one like this
/* Start Timer */
func startTimerForBanners() {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.getBanners()
})
**self.timer.invalidate()**
self.timer = NSTimer(timeInterval: 5, target: self, selector: #selector(self.updateIndex(_:)), userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(self.timer!, forMode: NSRunLoopCommonModes)
}
Are you sure that your controller is really deallocated?
There is one thing you should be always aware about NSTimer: it retains it's target.
I would suggest to:
Start your timer in viewWillAppear
Invalidate and set timer to nil in viewDidDisappear
Also try replacing your start timer code with this (Swift 3):
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(self.updateIndex(_:)), userInfo: nil, repeats: true)

detect up volume change swift

I'm having problems detecting when someone presses up or down volume button. For the moment I just play a file but I want to know when the user presses the button to show an alert when the volume changes. I'm developing in Swift and I'm using AVFoundation to create this player. For the moment I can't find something that works in Swift. I'm very new to this language.
import UIKit
import AVFoundation
class ViewController: UIViewController {
var backgroundMusicPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
playBackgroundMusic("IronBacon.mp3")
}
func playBackgroundMusic(filename:String){
let url = NSBundle.mainBundle().URLForResource(filename, withExtension: nil)
print(url)
guard let newUrl = url else{
print("couldn't find file: \(filename)")
return
}
do{
backgroundMusicPlayer = try AVAudioPlayer(contentsOfURL: newUrl)
backgroundMusicPlayer.numberOfLoops = -1
backgroundMusicPlayer.prepareToPlay()
}catch let error as NSError{
print(error.description)
}
}
#IBAction func playPauseAction(sender: UIButton) {
sender.selected = !sender.selected
if sender.selected {
backgroundMusicPlayer.play()
} else {
backgroundMusicPlayer.pause()
}
}
func ShowAlert(title: String, message: String, dismiss: String) {
let alertController = UIAlertController(title: title, message:
message, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: dismiss, style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
}
func volumeUp(){
ShowAlert( "example", message: "example", dismiss: "close")
}
func volumeDown(){
ShowAlert( "example", message: "example", dismiss: "close")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
This should do the trick.
class ViewController: UIViewController {
// MARK: Properties
let notificationCenter = NSNotificationCenter.defaultCenter()
// MARK: Lifecycle
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
notificationCenter.addObserver(self,
selector: #selector(systemVolumeDidChange),
name: "AVSystemController_SystemVolumeDidChangeNotification",
object: nil
)
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
notificationCenter.removeObserver(self)
}
// MARK: AVSystemPlayer - Notifications
func systemVolumeDidChange(notification: NSNotification) {
print(notification.userInfo?["AVSystemController_AudioVolumeNotificationParameter"] as? Float)
}
}
For some reason, the accepted answer does not work. Here is how you can do it in latest iOS versions -
func addObserver() {
NotificationCenter.default.addObserver(self,
selector: #selector(systemVolumeDidChange),
name: Notification.Name("SystemVolumeDidChange"),
object: nil)
}
func systemVolumeDidChange(notification: NSNotification) {
Log.msg("New Volume = \(notification.userInfo?["Volume"] as? Float)")
}
There are a few more fields in user info that can determine the volume change reason etc.

Resources