I've created a collectionView in Storyboard and then put the Delegate and DataSource methods in an extension to the ViewController which manages that screen.
The collectionView uses a layoutDelegate to show a four-by-four grid of images. All cells are shown in the grid, so a cell not being visible isn't a problem and they are all instances of the class imageCVC, a subclass of UICollectionViewCell
This all loads without a problem, but I now want to manipulate four random images before passing control to the user. Mindful that the collectionView may not have fully loaded by the end of viewDidLoad, I call the routine that chooses which image to manipulate, changeImages() in the viewDidLayoutSubviews method. The function is as follows:
func changeImages() {
collectionView.layoutIfNeeded()
let maxChanges = 30
var imageIndex = 0
var imageChanges 0
while imageChanges < maxChanges {
imageIndex = Int.random(in: 0..<(collectionView.numberOfItems(inSection: 0)))
if let cell = collectionView.cellForItem(as: IndexPath(row: imageIndex, section: 0)) as? imageCVC {
changeCell(cell)
imagesChanges += 1
}
}
}
(EDIT: Incorporated Sam's suggestion (below), but it still always returns nil!)
Unfortunately, whilst the imageIndex gets set correctly (so the collection knows how many elements it has), the cellForItem call always returns nil. I've forced the layout at the beginning of the function, but it has no effect.
Please could someone let me know what I'm doing wrong? Many thanks in advance.
In the following line:
imageIndex = Int.random(in: 0...(collectionView.numberOfItems(inSection: 0)))
The code starts from 0 and goes all the way to the collection view items count, so if the count is 10, the code goes from 0 to 10 including 10 which is 11 items in total. This is probably what is causing the crash since there are only 10 items and we try to access 11 items.
Just change:
0...(collectionView.numberOfItems(inSection: 0)
To
0..<(collectionView.numberOfItems(inSection: 0)
After further investigation, it appears that the collectionView data is not being loaded until after the viewDidLayoutSubviews - which seems a little contradictory to me, but hey, I'm sure there's a good reason... - and so I have implemented what I consider to be a work-around.
I've taken the call to changeImages() out from the viewDidLayoutSubviews and put it into the completion segment of a DispatchQueue.main..., written in the viewDidLoad, as follows:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
self.changeImages()
})
Essentially, I'm giving the system time (0.3 seconds) to complete it's full loading of the subviews, rather than actually placing my code at the correct part of the cycle when I know that the views have been fully loaded. A solution but, I suspect, an inelegant one.
If anyone knows how I should be approaching it, I'd be very interested to hear. Thanks.
I'm working with Swift language. I wrote an animation for a timer that lasts for 30 seconds and is full. Now I want to stop this animation, but I do not know how! I want to be able to start again from the beginning. Animation.
Thanks if you have a solution or a method that helps me🙏
You can try
self.myView.layer.removeAllAnimations()
Two possible ways:
Somewhere store variable for total time. And in every timer repeat increase this value. When total time reaches 30 seconds, remove animation
When you start animation, set action which gets executed after specific time
First possible way:
var time: Double = 0
#objc func timerChangedValue() {
time += 1
if time == 30 {
view.layer.removeAllAnimations()
view.layoutIfNeeded()
}
}
Second possible way:
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
self.view.layer.removeAllAnimations()
self.view.layoutIfNeeded()
}
I have defined some inline documentation in Xcode 9.4.1 like this:
/**
Initiates a timer that counts down to zero from a passed-in value defined in CXP. If the timer reaches zero, it executes a method to log out the user.
- parameter duration: An integer expressed in seconds that defines the time a user has to choose an action before being automatically logged out.
- returns: `doLogout()` only if the timer reaches zero.
*/
But when I view this documentation, there is a space in front of the parameter name duration:
Gabriel Theodoropolous wrote an informative post entitled Documenting Your Swift Code in Xcode Using Markdown in May 2016. In it, he provides a screen shot under the Keywords section where there is clearly no extra space– everything lines up nicely.
While our versions of Xcode differ (he was using Xcode 7), it seems crazy that if this is a bug, it would not have been fixed by now.
I have attempted to remove the space through a lot of trial and error but nothing has worked.
How can I remove the space in front of duration?
MORE CODE FOR REFERENCE:
class AutomaticLogoutTimer {
fileprivate var duration: Int = 0
fileprivate var timer: Timer = Timer()
fileprivate var disposeBag = DisposeBag()
/**
Initiates a timer that counts down to zero from a passed-in value defined in CXP. If the timer reaches zero, it executes a method to log out the user.
- parameter duration: An integer expressed in seconds that limits the time a user has to choose an action before being automatically logged out.
- returns: `doLogout()` only if the timer reaches zero.
*/
public func startTimer(duration: Int) {
self.duration = duration
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
self.duration -= 1
if(self.duration == 0) {
self.stopTimer()
return self.doLogout()
}
}
}
// a little more code here
}
I have created an app with a codeable array in Swift. I have a boolean field to show which item in my list is the active record. If I choose another item in my list to make active (true) I would like to mark all other records as false. I was wondering if using a for-in loop would be the proper way to do this?
I have this code to activate the button but have been told that is not the proper way to do it.
#IBAction func activateButtonTapped(_ sender: UIButton) {
activateButton.isSelected = !activateButton.isSelected
updateSaveButtonState()
}
Any suggestions would be appreciated.
A for-loop would be the most naive (simple) way to do this, but not the most performant as the number of button increases (you're bound by O(n) time).
If one and only one button can be active at a time then you're better off using another variable to reference the currently active button. Your code would instead look like this:
private weak var activeButton: UIButton?
#IBAction func activateButtonTapped(_ sender: UIButton) {
activeButton?.isSelected = false
sender.isSelected = true
activeButton = sender
}
This ensures O(1) time.
If you want multiple buttons active at once, you can use an array of active buttons, which you'd loop over to deactivate. In that case, you still have a worst case O(n) time complexity, but you're almost always going to be looping over a smaller subset of the buttons.
Yes, a for in loop would be perfect for this. You could set all the buttons selection status to false and then set the one you just activated to true.
for button in buttons {
button.isSelected = false
}
activateButton.isSelected = true
Or you could check in the loop if the button being tapped is the one that in the loop.
for button in buttons {
button.isSelected = button == activateButton
}
I have built a "metal detector" kind of app that runs the "calculateDistance" function every 2 seconds, which calculates the distance in meters between the user location and a set marker, sets that to var globalDistance. Depending on if that distance, for simplicity, is >=10meters or <10meters, I am playing a scheduled timer that calls the "audioplayer" function, which plays a "beep" sound every 2 seconds (if distance>=10m) or every 0.5seconds (if distance <10m).
Problem is, the timers never invalidate as I instruct them to. So if I move from <10m to >10m with my device, the 0.5sec beeping continues. I do audioTimer.invalidate() to stop the timer running from previous iteration.
Any idea what I am doing wrong with my code? Many thanks
func calculateDistance {
//here there is code that successfully calculates distance, every 2 seconds
var timerSeconds = 0.0
var audioTimer = Timer.scheduledTimer(timeInterval: (timerSeconds), target: self, selector: #selector(googleMaps.audioPlayer), userInfo: nil, repeats: true)
if globalDistance > 10 { // globalDistance is where i set the distance every 2 seconds, with a timer fired on ViewDidLoad
timerSeconds = 2
}
if globalDistance >= 0 && globalDistance <= 10 {
timerSeconds = 0.5
}
audioTimer.invalidate()
audioTimer = Timer.scheduledTimer(timeInterval: (timerSeconds), target: self, selector: #selector(googleMaps.audioPlayer), userInfo: nil, repeats: true)
audioTimer.fire()
}
func audioPlayer(){
AudioServicesPlaySystemSound(1104)
}
The basic idea is to make sure there is no code path by which a Timer is started without stopping any prior one. You current code has a couple of paths by which an existing timer is not invalidated before starting the next one.
Furthermore, I would suggest that you only invalidate the old Timer if the new beep frequency is different than the old beep frequency. (Why invalidate the 2 second repeating beeping and start another timer if the old 2 second timer will do the job fine?)
So, this means that you will:
pull both the Timer and the TimeInterval variables out of the function;
only do the "new timer" process if it decides that the beep interval has changed; and
make sure to always invalidate the old timer before creating a new one.
For example:
private var audioTimer: Timer?
private var beepInterval: TimeInterval?
private func updateBeepIntervalIfNeeded() {
// here there is code that successfully calculates distance, called with whatever frequency you want
let newBeepInterval: TimeInterval
if globalDistance > 10 {
newBeepInterval = 2
} else if globalDistance >= 0 {
newBeepInterval = 0.5
} else {
fatalError("less than 0?!") // I'm inferring from your code that this cannot happen, but by using `let` above, Swift warned me that we had a path of execution we hadn't previously considered
}
if beepInterval != newBeepInterval {
beepInterval = newBeepInterval
audioTimer?.invalidate()
audioTimer = Timer.scheduledTimer(timeInterval: beepInterval!, target: self, selector: #selector(beep(_:)), userInfo: nil, repeats: true)
audioTimer!.fire()
}
}
#objc func beep(_ timer: Timer) {
// perform beep here
}
The problem
There's several issues at hand here.
Firstly, I'd like to emphasis the difference between references and instances. When you call call an initializer, the system allocates a piece of memory for a new object, and gives you a reference to that memory, which is stored in whatever variable you assign it to. You can assign this reference to other variables, which will make copies of the reference. Each of these variable references the same original object. This object will continue to exist in memory until no more variables reference it.
In your case, you're not directly calling an initializer, but you're calling a static method which serves a similar purpose. A new object is allocated on your behalf, and you're given a reference, which you then assign to audioTimer. There's a catch to this, however. When you call Timer.scheduledTimer(timeInterval:target:selector:userInfo:repeats:), the newly constructed timer is scheduled on the current run loop for you. The run loop is what's in charge of firing your timer at the right time. The consequence of this is that now the runloop is referencing your timer, preventing the timer object from being destroyed. Unless you invalidate your timer to unregister it from its runloop, the timer will continue to exist and fire forever, even after you delete your deference to it.
Now let's take a look at your code, with some explanation as to what's going on:
func calculateDistance {
//here there is code that successfully calculates distance, every 2 seconds
var timerSeconds = 0.0
// 1) Initialize timer #1
var audioTimer = Timer.scheduledTimer(timeInterval: (timerSeconds), target: self, selector: #selector(googleMaps.audioPlayer), userInfo: nil, repeats: true)
if globalDistance > 10 { // globalDistance is where i set the distance every 2 seconds, with a timer fired on ViewDidLoad
timerSeconds = 2
}
if globalDistance >= 0 && globalDistance <= 10 {
timerSeconds = 0.5
}
// 2) Invalidate timer #1 (timer #1 is useless)
audioTimer.invalidate()
// 3) Initialize timer #1
audioTimer = Timer.scheduledTimer(timeInterval: (timerSeconds), target: self, selector: #selector(googleMaps.audioPlayer), userInfo: nil, repeats: true)
// 4) Fire timer #2 immediately
audioTimer.fire()
} // At the end of this method body:
// - Timer #2 was never invalidated
// - audioTimer no longer references Timer #2, but:
// - Timer #2's runloop still references it, keeping it alive
// - Timer #2 is leaked
// ... and will continue firing forever.
func audioPlayer(){
AudioServicesPlaySystemSound(1104)
}
We can see that a Timer is made in section one, which should fire off in timerSeconds seconds, 0. At section 2, that timer is invalidated. Even though the Timer was to fire off in 0 seconds, it is almost certain that its run loop hasn't gotten a chance to fire it yet. Thus, this time is created, never fires, and then invalidated. There's no reason for it to exist at all there.
Then, in section 3, Timer #2 is created and scheduled. it is manually fired at section 4, and then it's permanently leaked.
The solution
You need an instance variable that holds reference to the timer. Without this, you have no way of invalidating the timer that has been already scheduled.
Secondly, you need to invalidate the timer at the appropriate time.
I suggest you take a look at Rob's answer for an example.
Youre creating a new, infinitely repeating, timer once, invalidate it immediately (why?), and then create another (why?), which is leaked forever.
You are creating a new timer,invalidating then creating a timer again.
You could try creating the timer,and when calling the audioPlayer function,checking for which sound to play depending on the value of the timerSeconds variable.