In a game screen, I use a background queue to count elapsed time in a game, this func is based on Daniel Galasko solution, a perfect solution for my app: it allows user to navigate through other VC, while timer is still on.
The VC hierarchy is quite simple : the game settings VCs are handled in a tabBarController. The game screen is apart. User can change settings while timer is on. Settings are stored in CoreData.
In my game screen, where I need to display the timer, I have a label that displays the elapsed time and 2 buttons : Play/Pause button and Reset button.
I call my setup timer func in ViewDidLoad.
The default value for my timer is the stored value in CoreData, it has been defined in settings. And this value is incremented by 1 every second when timer is on.
I also have a static let shared that keep timer status (resumed / suspended).
When I'm on the game screen, and if my timer is suspended, my play/Pause button works perfect : I can navigate to others views (mean dismiss my screen game), present again my screen game and resume my counter. It updates my label correctly.
The problem is when I dismiss the game screen view while timer is running. Timer works (print func shows that timer is still running), but when I present the screen again, I'm unable to pause/resume/restart it, and my label is stopped at the second I came back... while timer is still running back.
private var counter: Int16?
var t = RepeatingTimer(timeInterval: 1)
let gameIsOn = isGameOnManager.shared
override func viewDidLoad() {
super.viewDidLoad()
print("is timer On ? \(String(describing: gameIsOn.isgameOn))")
buildTimer()
if gameIsOn.isgameOn == true {
resumeTapped = true
t.resume()
PlayB.setImage(UIImage(named:"pause"), for: .normal)
} else {
resumeTapped = false
}
}
func buildTimer(){
self.t.eventHandler = {
self.counter! += 1
print("counter \(String(describing: self.counter!))")
self.coreDataEntity?.TimeAttribute = self.counter ?? 0
self.save()
DispatchQueue.main.async {
self.dataField.text = String(describing: self.counter ?? 0)
}
}
}
#objc func didTapButton(_ button: UIButton) {
if resumeTapped == false {
t.resume()
resumeTapped = true
gameIsOn.isgameOn = true
PlayB.setImage(UIImage(named:"pause"), for: .normal)
}
else if resumeTapped == true {
t.suspend()
resumeTapped = false
gameIsOn.isgameOn = false
PlayB.setImage(UIImage(named:"play"), for: .normal)
}
}
You have a strong reference cycle between your view controller, the timer, and the timer’s event handler. You should use a weak reference in the closure to break this cycle:
func buildTimer() {
t.eventHandler = { [weak self] in
guard let self = self else { return }
...
}
}
That fixes the strong reference cycle.
But when you fix this, there is a chance that you’re going to see your timer stop running when you dismiss this view controller. If this is the case, that will indicate that the timer is not in the right object. It should be in some higher level object that persists throughout the app, not inside this view controller which is presented and dismissed.
Related
there are 5 cells in table view, each have countdown on it, but countdown running fast, it decreases more than 1 second in one call.
this is my table view cell class, I have created a labelSatus here
class ActivityCell: UITableViewCell {
#IBOutlet weak var labelStatus: UILabel!
//Variable Declaration
var timer: Timer?
var totalTime:Double = 0
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func designCell(data:ActivityModal, removeTimer:Bool){
if removeTimer {
if let timer = self.timer {
timer.invalidate()
self.timer = nil
}
}
else{
let timeStampLimit = (data.date)/1000 + 86400 //for 24 hours
let currentTimeStamp = NSDate().timeIntervalSince1970
if timeStampLimit > currentTimeStamp{
self.totalTime = timeStampLimit - currentTimeStamp
self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
}
}
#objc func updateTimer() {
self.labelStatus.text = String.getString(CommonUtils.convertTimeStampToHour(unixtimeInterval: self.totalTime, dateFormat:"HH:mm:ss"))
if self.totalTime != 0 {
self.totalTime -= 1 // decrease counter timer
} else {
if let timer = self.timer {
timer.invalidate()
self.timer = nil
}
}
}
}
There are a few issues to take care of when working with timers on table view cells. Note that the UITableView reuses the cell objects, so after a cell disappears from the screen after scrolling, it will be reused to be displayed in another place inside the table view.
Supposing you call designCell() in tableView:(_:cellForRowAt:) method, you might be creating more timers for a single cell (i.e. a lot of timers which will trigger the same cell's updateTimer() method). Note that a Timer will not be deallocated after you drop all your references to it, if the timer is still valid. At this point, you might be creating a new timer for a specific cell without having the chance to invalidate the old one.
This would be handled by stopping the timer in preapreForReuse() method from the table view cell subclass. This method is called when the cell is going to be reused:
class ActivityCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
self.timer?.invalidate()
self.timer = nil
}
}
This will solve (one of) the timer problem(s), but there is one more issue: you are going to lose the time tracked by that cell. There is no way to preserve that state into the cell itself. You must do it in the view controller and pass the displayed cell the time it needs to track in tableView(_:cellForRowAt:) method. If you are already doing it, then this is not a problem.
Another problem is that the timers will live forever (or until it's cell stops it). For some of the timers, if the cell object lost the reference to it, nobody will ever stop that timer anymore, so it will just fire every second until the app is killed.
I'm not really sure at this point whether the timer will retain the cell object using the target-selector method for creating a timer, but there is an issue regardless of that:
If the timer retains the cell object, that means that you will have memory leaks: that means that the timers and the cells will never be deallocated and will continuously use memory and processing resources;
If the timer does not retain the cell object, then there might occur a crash once the cell is being deallocated.
You should stop the cell's timer in tableView(_:didEndDisplaying:forRowAt) method. At this point, the cell is being hidden, so the timer makes no sense anymore.
Of course, all of this will lead you to another issue: preserving the time state for the cells. This handling should be done from the view controller presenting the table view, and not from the table view cell.
TL;DR:
You will have a lot of headaches having a timer on each cell, in fact it's almost impossible to handle things that way. You should have your timers corresponding to each cell in the view controller that presents the table view and the things will simplify a lot.
I have a question about saving the last status of the buttons for a "Hung Person" game. By using UserDefaults I was able to save almost all the status of the user when the game was closed (Screen1 shows the user's game status while playing), as you can see, the user chose 2 wrong letters that were not in the hidden word (M and N, which are in red and disabled) and the user correctly guessed the O, B, C letters (which disappeared from the alphabet and also are disabled).
When the user closes the app and when it is reopened again, I was able to reload almost all the previous data of the last session played, except for the buttons status. You can see this in Screen2. I used UserDefaults to save and load the game. I save the status of the game every time the user taps on any button (trying to guess the hidden word's characters), and I load the data back in viewDidLoad().
The game is made programmatically, I created the buttons using something like the following:
for row in 0..<5 {
for column in 0..<6 {
if counterLetter < 26 {
let letterButton = UIButton(type: .system)
letterButton.titleLabel?.font = UIFont.systemFont(ofSize: titleLabelFontSize)
letterButton.setTitle(englishAlphabet[counterLetter], for: .normal)
letterButton.isHidden = false
letterButton.isEnabled = true
letterButton.alpha = 1
letterButton.setTitleColor(.red, for: .disabled)
letterButton.addTarget(self, action: #selector(letterTapped), for: .touchUpInside)
letterButton.layer.borderWidth = 1
letterButton.layer.borderColor = UIColor.black.cgColor
// Button's frame creation
let frame = CGRect(x: column*widthButton, y: row*heightButton,
width: widthButton, height: heightButton)
letterButton.frame = frame
buttonsView.addSubview(letterButton)
letterButtons.append(letterButton)
counterLetter += 1
} else {
continue
}
}
}
It is in the letterTapped method (inside the #selector(letterTapped) part in the previous code) where I save the user's progress when any alphabet button is tapped. As I said before, I recall the player's last status in viewDidLoad().
I tried to save the buttons status inside the letterTapped method as well, but I haven't been able to save or reload the buttons status as the player's had.
Can you give me please a hand about where I have to use userDefaults to save the last session status of the alphabet buttons, please? So that when the players reopens the app, the screen2 is the same as screen1.
If you need me to share my code, I can do willingly it.
Thanks in advance.
Regards!
What you are looking for is a K.V.O called "UIApplicationWillEnterForeground" and "UIApplicationDidEnterBackground"
In your viewDidLoad of your ViewController you should do the following:
func viewDidLoad() {
super.viewDidLoad()
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: Notification.Name.UIApplicationWillEnterForeground, object: nil)
notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: Notification.Name.UIApplicationDidEnterBackground, object: nil)
then implement these function in the same ViewController:
func appMovedToForeground() {
print("Application moved to Foreground!")
}
func appMovedToBackground() {
print("Application moved to Background!")
}
now when your app goes to the background or returns to the foreground those functions will get called
Let's say I has a audio player app and there is song list (tableView) in VC1 to present all the songs and clicking any row of it to open another VC2 to present the playing scene. In this playing scene there is some buttons for playing sequence (repeat, repeatOne, shuffle).
Since user will frequently dismissed/popup this VC2 to get back to VC1 (song list), I store the playing sequence button value to another file called SongData.swift and define a static var static var repeatOneSequence = false to store the value of VC2. Then retrieve it if SongData.repeatOneSequence == true like this in VC2.
Now it is working, but it is just my way...I didn't get info about how to do it properly. I'm wondering if there is other way to store the data of dismissed VC2 or which way is the better one to do it.
/ / / Insert code snip for reference
#IBAction func repeatButton(_ sender: UIButton) {
if SongData.repeatOneSequence == true {
print("repeatOneSequence TRUE!")
repeatButton.setImage(UIImage(named: "icons8-repeat-50"), for: .normal)
repeatButton.imageView?.contentMode = .scaleAspectFit
SongData.repeatOneSequence = false
} else {
print("repeatOneSequence FALSE!")
repeatButton.setImage(UIImage(named: "icons8-repeat-one-50"), for: .normal)
repeatButton.imageView?.contentMode = .scaleAspectFit
SongData.repeatOneSequence = true
}
}
I'm trying to implement a record button for my chat, which records as long as you hold the button. I implemented a longpressGestureRecognizer, but unfortunately it only records for one second, no matter how long I press.
Here is the code:
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPress(_:)))
longPressGestureRecognizer.minimumPressDuration = 1
self.recordingSession = AVAudioSession.sharedInstance()
do {
try self.recordingSession.setCategory(AVAudioSessionCategoryPlayAndRecord)
try self.recordingSession.setActive(true)
self.recordingSession.requestRecordPermission() { [unowned self] allowed in
DispatchQueue.main.async {
if allowed {
self.record_button.addGestureRecognizer(longPressGestureRecognizer)
} else {
// failed to record!
}
}
}
} catch {
// failed to record!
}
// Gesture Recognizer for the Record Button, so as long as it is pressed, record!
func longPress(_ longPressGestureRecognizer: UILongPressGestureRecognizer){
if longPressGestureRecognizer.state == .ended {
print("long press ended")
let recordImage = UIImage(named: "ic_mic_white")
record_button.setImage(recordImage, for: .normal)
self.recordTapRelease()
}
if longPressGestureRecognizer.state == .began {
let recordingTapImage = UIImage(named: "ic_mic_none_white")
record_button.setImage(recordingTapImage, for: .normal)
self.recording()
}
}
EDIT
I implemented the .touchdown .touchupinside events etc.
I still get the same behaviour, unless I slight up above the record button leaving the orange view. Then the recording image button image also changes, showing recording, and if I release and move further up it stops recording.
You don't even have to create UILongPressGestureRecognizer for achieving this; You can do it by implementing touchDown, touchUpInside and touchDragExit events for a UIButton.
At the first look, it might seems that it's more complicated than working with UILongPressGestureRecognizer, but I think that it is more logical and even more readable.
Follow the steps in this answer and hopefully you will get the desired behavior for your recording button. It also has another answer if you insist to use UILongPressGestureRecognizer.
Hope this helped.
I made an animation with UIButton via somes images.
Here's the code:
#IBAction func Touchdown(sender: AnyObject) {
Izer.setImage(image1, forState: UIControlState.Normal)
Izer.imageView!.animationImages = [image1, image2, image3, image4, image5,
image6, image7,image8]
Izer.imageView!.animationDuration = 0.9
Izer.imageView!.startAnimating()
playButton.enabled = false
}
#IBAction func TouchUp(sender: AnyObject) {
soundRecorder.stop()
playButton.enabled = true
}
When I touch the button , the animation start. But I want to stop it via my Touchup function.
How can I do it ?
Thank you , and sorry for my bad english :(
Add this to your TouchUp func:
Izer.imageView!.stopAnimating()
p.s. A good place to find information about functions is in the Apple documentation - it really is quite good. So this is the page for an imageView and if you look on the left under tasks, or if you scroll down, you can see the functions and properties you can call and set for animating an imageView.
Ok, looks like you want the button to act as a toggle switch. On the first touch the button starts animating an imageview and records something. When touched again the animation stops and the button is enabled again.
You can achieve this by declaring a bool variable that tracks the state of the button. If the boolean value is set to true, you run the animation & recording. If the boolean value is false, you stop the and recording.
Here is the sample code :
class ViewController: UIViewController {
#IBOutlet weak var mainView: UIView!
var isButtonPressed = false{
// Adding a Property Observer, that reacts to changes in button state
didSet{
if isButtonPressed{
// Run the Recording function & Animation Function.
startAnimationAndRecording()
}else{
// Stop the Recoding function & Animation Function.
stopAnimationAndRecording()
}
}
}
#IBAction func changeButtonValue(sender: UIButton) {
// Toggle the button value.
isButtonPressed = !isButtonPressed
}
func startAnimationAndRecording(){
// Add your animation and recording code here.
}
func stopAnimationAndRecording(){
//Add your stop Animation & Stop Recording code here.
}
}