NSNotification being fired off more then one time? - ios

I am creating a sample player for a test project. I created a NSNotification to call a function to play the next audio track inside an array. The issue is the notification calls this function about 8 times in a row? I have no idea why this is occurring. Here is my code and thanks for the help!
let player = AVPlayer()
var urlPlayerItems = [String]()
var currentTrack: Int = 0
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Checks to see if player reached end
NotificationCenter.default.addObserver(self,
selector: #selector(PlayerViewController.autoplayNextTrack(notification:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: player.currentItem)
}
func playTrack() {
if urlPlayerItems.count > 0 {
let newMovieURL = URL(string: urlPlayerItems[currentTrack])!
asset = AVURLAsset(url: newMovieURL, options: nil)
player.play()
}
}
func autoplayNextTrack(notefication: NSNotification) {
if (currentTrack + 1) >= urlPlayerItems.count {
currentTrack = 0
} else {
currentTrack += 1
}
playTrack()
}

Aside from the fact that an observer shouldn't be set multiple times, i think that you should reset the player to zero right before calling the play function again
func autoplayNextTrack(notefication: NSNotification) {
player.seekToTime(kCMTimeZero)
if (currentTrack + 1) >= urlPlayerItems.count {
currentTrack = 0
} else {
currentTrack += 1
}
playTrack()
}

If you add your observers in viewDidAppear method, you need to make sure you only add them ONCE. the viewDidAppear methods will get called multiple times.
Easy way is just to make a BOOL and flag it when you have added/removed it .
EDIT:
Also, I don't see any method where you remove the observer anywhere in your code, make sure you remove it too when you want to stop observing.
Example:
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
or in your viewWillDissapear method.

Related

How to listen for change in variable inside a function

I have a class called Observers to observe Firebase Storage Upload Tasks, but before observing the progress, it waits for PHPickerviewcontroller to upload the video. I have an instance variable in my class, hasUploaded so that I can know when I can start to change the progress bar, however, with the way it's set up, the block of code in the if statement will never be called. I know there is didSet but that doesn't help me in this case, because I need to listen for change inside the function. How do I do that?
func observeProgress(progressWheel: UIActivityIndicatorView, errorLabel: UILabel, progressView: UIProgressView, progressLabel: UILabel)
{
progressLabel.text = "Downloading from Device...Please Wait (1/2)"
progressWheel.startAnimating()
progressWheel.alpha = 1
inProgress = true
//RIGHT HERE - Wait for hasUploaded to == true
if hasUploaded
{
progressWheel.alpha = 0
self.taskReference!.observe(.progress)
{ (snapshot) in
guard let progress = snapshot.progress?.fractionCompleted else { /**alert**/ return }
progressView.progress = Float(progress)
progressLabel.text = "\(round(100 * Float(progress)))% (2/2)"
if progress == 1
{
progressLabel.text = "Upload Successful!"
progressLabel.textColor = .black
progressView.progress = 0
}
}
}
}
I thought about it again and maybe it is easier for you to use the NotificationCenter.
In the Model or ViewController add
let nc = NotificationCenter.default
and thenadjust the hasUploaded variable to
var hasUploaded = false {
didSet {
let statusChange = ["userInfo": ["hasUploaded": hasUploaded]]
NotificationCenter.default
.post(name:
NSNotification.Name("com.user.hasUploaded"),
object: nil,
userInfo: statusChange)
}
}
In the controller with the function observeProgress, also add
let nc = NotificationCenter.default
Add the following to the viewDidLoad() function
NotificationCenter.default
.addObserver(self, selector:#selector(hasUploadedNotificationReceived(_:)),
name: NSNotification.Name ("com.user.hasUploaded"),
object: nil)
}
Finally create the function hasUploadedNotificationReceived() (which is called above whenever the notification will be received) to add the magic that should happen after the the change. For example:
#objc func hasUploadedNotificationReceived(_ notification: Notification){
let notification = notification.userInfo?["userInfo"] as? [String: Bool] ?? [:]
if (notification["hasUploaded"] as? Bool)! {
observeProgress(...) {
[...]
}
}
}
Please read also the documentation to figure out what options you have and what you can add or modify.
Beside this implementation, I also can imagine that the a Delegate as well as Combine and as #matt mentioned async/await could help to achieve your desired behavior.

swift ios - How to run function in ViewController from AppDelegate

I am trying to run a function in certain ViewController using AppDelegate
func applicationDidBecomeActive(_ application: UIApplication) {
ViewController().grabData()
}
But somehow the function does not seem to run at all when the app has become active after entering the app from the background.
The function looks like this
func grabData() {
self._DATASERVICE_GET_STATS(completion: { (int) -> () in
if int == 0 {
print("Nothing")
} else {
print(int)
for (_, data) in self.userDataArray.enumerated() {
let number = Double(data["wage"]!)
let x = number!/3600
let z = Double(x * Double(int))
self.money += z
let y = Double(round(1000*self.money)/1000)
self.checkInButtonLabel.text = "\(y) KR"
}
self.startCounting()
self.workingStatus = 1
}
})
}
And uses this var
var money: Double = 0.000
What have I missed?
Thanks!
ViewController().grabData() will create a new instance of the ViewController and call this function. Then.. as the view controller is not in use it will be garbage collected/removed from memory. You need to be calling this method on the actual view controller that is in use. Not a new instance of it.
The best option would be to listen for the UIApplicationDidBecomeActive notification that iOS provides.
NotificationCenter.default.addObserver(
self,
selector: #selector(grabData),
name: NSNotification.Name.UIApplicationDidBecomeActive,
object: nil)
make sure that you also remove the observer, this is usually done in a deinit method
deinit() {
NotificationCenter.default.removeObserver(self)
}
I simply solved it like this:
func applicationDidBecomeActive(_ application: UIApplication) {
let viewController = self.window?.rootViewController as! ViewController
viewController.grabData()
}

Label does not update using Swift

I'm trying to improve a GitHub project I forked (https://github.com/giacmarangoni/Swift-Radio-Pro/tree/xcode8).
After some fixes and changes everything seems to work good but suddenly I noticed a really strange behavior.
When I open "NowPlayingViewController" for the first time and station starts to stream, everything is working and AVPlayer delegate updates user interface as expected (songLabel, titleLabel and albumArtwork).
After that, without stopping radio streaming, I tried to go back to "StationsViewController" and immediately to reopen "NowPlayingViewController" using "Now playing" button.
At this point delegation is still active, streaming is going on, but when song changes all variables in this view controller are updated but I can't say the same for the user interface. I tried to debug and I noticed that labels are populated but not updated. UI updates in the main thread and setNeedDisplay didn't help.
NowPlayingViewController
AVPlayer setup:
func setUpPlayer(){
radioPlayer = Player.radio
radioPlayer.rate = 1
NotificationCenter.default.addObserver(
self,
selector: #selector(self.playerItemDidReachEnd),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: self.radioPlayer.currentItem
)
}
Here you can find func onMetaData(_ metaData: [AVMetadataItem]?)).
//*****************************************************************
// MARK: - AVPlayerItem Delegate (for metadata)
//*****************************************************************
extension NowPlayingViewController: CustomAVPlayerItemDelegate {
func onMetaData(_ metaData: [AVMetadataItem]?) {
if let metaDatas = metaData{
startNowPlayingAnimation()
let firstMeta: AVMetadataItem = metaDatas.first!
let metaData = firstMeta.value as! String
var stringParts = [String]()
if metaData.range(of: " - ") != nil {
stringParts = metaData.components(separatedBy: " - ")
} else {
stringParts = metaData.components(separatedBy: "-")
}
// Set artist & songvariables
let currentSongName = track.title
track.artist = stringParts[0].decodeAllChars()
track.title = stringParts[0].decodeAllChars()
if stringParts.count > 1 {
track.title = stringParts[1].decodeAllChars()
}
if track.artist == "" && track.title == "" {
track.artist = currentStation.stationDesc
track.title = currentStation.stationName
}
DispatchQueue.main.async {
if currentSongName != self.track.title {
if kDebugLog {
print("METADATA artist: \(self.track.artist) | title: \(self.track.title)")
}
// Update Labels
self.artistLabel.text = self.track.artist
self.songLabel.text = self.track.title
self.updateUserActivityState(self.userActivity!)
// songLabel animation
self.songLabel.animation = "zoomIn"
self.songLabel.duration = 1.5
self.songLabel.damping = 1
self.songLabel.animate()
// Update Stations Screen
self.delegate?.songMetaDataDidUpdate(self.track)
// Query API for album art
self.resetAlbumArtwork()
self.queryAlbumArt()
}
}
}
}
}
This method is observed in "CustomAVPlayerItem" according to timedMetaData key path; It's fired every time AVPlayer metadatas change. This class is a subclass of AVPlayerItem:
import MediaPlayer
import Foundation
protocol CustomAVPlayerItemDelegate {
func onMetaData(_ metaData:[AVMetadataItem]?)
}
//*****************************************************************
// Makes sure that observers are removed before deallocation
//*****************************************************************
class CustomAVPlayerItem: AVPlayerItem {
var delegate : CustomAVPlayerItemDelegate?
init(url URL:URL)
{
if kDebugLog {print("CustomAVPlayerItem.init")}
super.init(asset: AVAsset(url: URL) , automaticallyLoadedAssetKeys:[])
addObserver(self, forKeyPath: "timedMetadata", options: NSKeyValueObservingOptions.new, context: nil)
}
deinit{
if kDebugLog {print("CustomAVPlayerItem.deinit")}
removeObserver(self, forKeyPath: "timedMetadata")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let avpItem: AVPlayerItem = object as? AVPlayerItem {
if keyPath == "timedMetadata" {
delegate?.onMetaData(avpItem.timedMetadata)
}
}
}
}
The following is my AVPlayer:
import MediaPlayer
//*****************************************************************
// This is a singleton struct using Swift
//*****************************************************************
struct Player {
static var radio = AVPlayer()
}
This is the segue function I use to open to "NowPlayingViewController". StationsViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "NowPlaying" {
self.title = ""
firstTime = false
let nowPlayingVC = segue.destination as! NowPlayingViewController
nowPlayingVC.delegate = self
if let indexPath = (sender as? IndexPath) {
// User clicked on row, load/reset station
if searchController.isActive {
currentStation = searchedStations[indexPath.row]
} else {
currentStation = stations[indexPath.row]
}
nowPlayingVC.currentStation = currentStation
nowPlayingVC.newStation = true
} else {
// User clicked on a now playing button
if let currentTrack = currentTrack {
// Return to NowPlaying controller without reloading station
nowPlayingVC.track = currentTrack
nowPlayingVC.currentStation = currentStation
nowPlayingVC.newStation = false
} else {
// Issue with track, reload station
nowPlayingVC.currentStation = currentStation
nowPlayingVC.newStation = true
}
}
}
}
Here's what I think you're not understanding and what's actually going on.
Normally, when you "go back" from a pushed view controller, the pushed view controller is popped and destroyed. Your pushed view controller is a NowPlayingViewController. It should be destroyed when you "go back" from it to the StationsViewController. Thus, when you show the NowPlayingViewController again, you would have to create a new, different NowPlayingViewController.
Okay, so far so good, provided you understand all of that. But in your case there is a further complication: you have a leak! Your old NowPlayingViewController is not being destroyed. Thus, when you "go back" to the StationsViewController and show the NowPlayingViewController for a second time, there are now two NowPlayingViewControllers — the new one that you see, and the old one that is leaking.
Okay, so your logging continues to show the old NowPlayingViewController, which is still observing and updating. But your eyes are seeing the new NowPlayingViewController, which is doing nothing. And that explains the phenomena you have described.
If this is right — and, from what you've said, I'm pretty sure it is — then you need to reorganize your architecture either so that you don't get this leak or so that when you show the NowPlayingViewController the second time you show the same NowPlayingViewController rather than creating a different one. (The first approach would be better.)

Add a delay to a for loop in swift

I have a coding 'issue'.
I have a label, which text I want to change dynamically every 2 seconds.
I've done the following:
// WELCOME STRING ARRAY
let welcomeContainer:[String] = ["Welcome","Benvenuti","Bienvenue","Willkommen","üdvözlet","Dobrodošli","добро пожаловать","Witajcie","Bienvenido","Ласкаво просимо","Vitajte","欢迎你来"]
and then, rather than using a timerwithinterval (which seemed to be too much for this simple task), I tried with the delay method in my function inside for loop:
func welcomeLabelChange() {
for i in 0..<welcomeContainer.count {
welcomeLabel.text = welcomeContainer[i]
delay(delay: 2.0, closure: {})
}
Unfortunately it's entirely skipping the delay... the for loop is executed instantly and just the last text in the array is displayed.
What am I doing wrong?
I found this OBJ-C answer, but it's suggesting an (old) NSTimer implementation.
You can also use this function to delay something
//MARK: Delay func
func delay(_ delay:Double, closure:#escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}
and usage is :
delay(2) //Here you put time you want to delay
{
//your delayed code
}
Hope it will help you.
define those variables
var i = 0
let timer : Timer?
Place this timer in your view did load or wherever you want to start the label change
timer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector:#selector(YourViewController.changeText), userInfo: nil, repeats: true)
and implement this method:
func changeText(){
if i>=welcomeContainer.count {
i = 0
}
welcomeLabel.text = welcomeContainer[i]
i += 1
}
when you want to stop it or change the view controller dont forget to call
timer.invalidate()
You can add sleep function
for i in 0..<welcomeContainer.count {
welcomeLabel.text = welcomeContainer[i]
sleep(2) // or sleep(UInt32(0.5)) if you need Double
}
If you want to keep it all inline you can do this:
var loop: ((Int) -> Void)!
loop = { [weak self] count in
guard count > 0 else { return }
//Do your stuff
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
loop(count - 1)
}
}
loop(10) //However many loops you want
With Timer, you should be careful to call invalidate of the Timer in viewDidDisappear or else you may not release the view controller.
Alternatively, you can use a GCD dispatch timer, in which you completely eliminate the strong reference cycle by using [weak self] pattern:
#IBOutlet weak var welcomeLabel: UILabel!
var timer: DispatchSourceTimer!
override func viewDidLoad() {
super.viewDidLoad()
let welcomeStrings = ["Welcome", "Benvenuti", "Bienvenue", "Willkommen", "üdvözlet", "Dobrodošli", "добро пожаловать", "Witajcie", "Bienvenido", "Ласкаво просимо", "Vitajte", "欢迎你来"]
var index = welcomeStrings.startIndex
timer = DispatchSource.makeTimerSource(queue: .main)
timer.scheduleRepeating(deadline: .now(), interval: .seconds(2))
timer.setEventHandler { [weak self] in
self?.welcomeLabel.text = welcomeStrings[index]
index = index.advanced(by: 1)
if index == welcomeStrings.endIndex {
index = welcomeStrings.startIndex // if you really want to stop the timer and not have this repeat, call self?.timer.cancel()
}
}
timer.resume()
}
Marked answer doesn't delay loop iterations and you still get just the last value in the label.text.
You can solve it like this:
func showWelcome(_ iteration: Int = 0) {
let i = iteration>=self.welcomeContainer.count ? 0 : iteration
let message = self.welcomeContainer[i]
self.delay(2){
self.welcomeLabel.text = message
return self.showWelcome(i + 1)
}
}
Usage:
showWelcome()

How to synchronize different actions in Swift Language for IOS

I created a timer in one class, and tried to do something else in another class while timer works, and do other thing while timer stops. For example, show every second when timer works. I simplified the code as below. How to realize that?
import Foundation
import UIView
class TimerCount {
var timer: NSTimer!
var time: Int!
init(){
time = 5
timer = NSTimer.scheduledTimerWithTimeInterval( 1.0 , target: self, selector: Selector("update"), userInfo: nil, repeats: true)
}
func update(){
if(time > 0) {
time = time - 1
// do something while timer works
}
else{
timer.invalidate()
timer = nil
time = 5
}
}
}
class Main: UIView {
var Clock: TimerCount!
override func viewDidLoad() {
Clock = TimerCount()
//? do something else while clock works
// ? do other things while clock stops
// FOR EXAMPLE: show every second when timer works
if(Clock.time > 0){
println(Clock.time)
}else{
println("clocker stops")
}
}
}
viewDidLoad is most likely only going to be called once. You could simply make the update method be in your Main object and then pass that instance of main and that update method into the scheduledTimerWithTimerInterval call. Otherwise you need a new method in the Main class to call from the timerCount class and pass in the int for the current time.
This is in your Main class:
func updateMethodInMain(timeAsInt: Int){
//do stuff in Main instance based on timeAsInt
}
This is what you have in your timer class:
func update(){
if(time > 0) {
time = time - 1
instanceNameForMain.updateMethodInMain(time)
}
else{
timer.invalidate()
timer = nil
time = 5
}
}
}

Resources