Nested asynchronous blocks of code in Swift - ios

I am trying to nest a NSTimer.scheduledTimerWithTimeInterval call inside of an asynchronous NSURLSession.sharedSession().dataTaskWithRequest in Swift 2.0, but the code block inside of the test does not seem to get evaluated.
For example:
class testc{
#objc func test()
{
print("hello");
}
func loop()
{
if let url = NSURL(string : "https://www.google.com")
{
let url_req = NSURLRequest(URL: url);
let task = NSURLSession.sharedSession().dataTaskWithRequest(url_req)
{ data, response, error in
let timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(testc.test), userInfo: nil, repeats: true)
}
task.resume()
}
}
}
If we initialize this class and run loop nothing happens, the function test was never evaluated.

You need two changes to your code.
After creating the dataTask. You need to ask it to resume() to send the request.
The timer needs to be called on the main thread.
In your case, the dataTask is an asynchrnous task that runs on a background thread. In the implementation below, we are jumping back to the main thread to fire the timer.
I have added a counter to verify if the timer is firing repeatedly.
See updated code below.
class testc{
static var counter : Int = 0
#objc func test()
{ testc.counter++
print("hello -> \(testc.counter)");
}
func loop()
{
if let url = NSURL(string : "https://www.google.com")
{
let url_req = NSURLRequest(URL: url);
let task = NSURLSession.sharedSession().dataTaskWithRequest(url_req){ data, response, error in
dispatch_async(dispatch_get_main_queue(), {
self.setupTimer()
})
}.resume()
}
}
func setupTimer() {
let timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(testc.test), userInfo: nil, repeats: true)
}
}
let theBoy = testc()
theBoy.loop()

Related

iOS - Terminated due to memory issue error

So i am doing a network calls to retrieve image and using that i am show a kind of a video.
After a while i can see the memory lose and energy impact:
after a while my app crushed and i got :"Terminated due to memory issue error"
Before that i got that error from the image caller method:"error from dataResponse:The operation couldn’t be completed. No space left on device"
This are the two method i use:
class InstallationViewController: BaseViewController {
func imageCaller(url: String , success: #escaping (UIImage) -> Void, failure: #escaping () -> Void) {
let handler = AuthenticateHandler()
self.urlSession = URLSession(configuration: URLSessionConfiguration.default, delegate: handler, delegateQueue: OperationQueue.main)
self.imageThumbnailTask = urlSession?.dataTask(with: URL(string:url)!) { data, res, err in
if err != nil {
print("error from dataResponse:\(err?.localizedDescription ?? "Response Error")")
failure()
return
}
DispatchQueue.main.async {
if let imageData = data, let image = UIImage(data: imageData) {
success(image)
URLCache.shared.removeAllCachedResponses()
}
}
}
self.imageThumbnailTask?.resume()
}
func imageThumbnailcall() {
self.indicaotrTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.HandleOverTime), userInfo: nil, repeats: false)
self.imageCaller( url: self.isShowingThermal ? self.thermalUrl : self.visualUrl, success: { (image) in
self.indicaotrTimer?.invalidate()
DispatchQueue.main.async{
self.imageLoaderIndicator.stopAnimating()
self.backGroundImageView.image = image
}
if self.isInVC {
self.imageThumbnailcall()
}
}) {
self.imageLoaderIndicator.stopAnimating()
}
}
Worth mentioning that this line:
let handler = AuthenticateHandler()
self.urlSession = URLSession(configuration: URLSessionConfiguration.default, delegate: handler, delegateQueue: OperationQueue.main)
for digest protocol
Looks like you have a retain cycle in your closure in the imageThumbnailcall function.
The closure creates a strong reference to self, and since it's a recursive function you will run out of memory pretty quick.
You need to capture self as [weak self] or [unowned self] in the closure.
Example using [unowned self]:
func imageThumbnailcall() {
self.indicaotrTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.HandleOverTime), userInfo: nil, repeats: false)
self.imageCaller( url: self.isShowingThermal ? self.thermalUrl : self.visualUrl, success: { [unowned self] (image) in
self.indicaotrTimer?.invalidate()
DispatchQueue.main.async{
self.imageLoaderIndicator.stopAnimating()
self.backGroundImageView.image = image
}
if self.isInVC {
self.imageThumbnailcall()
}
}) {
self.imageLoaderIndicator.stopAnimating()
}
}
If you want to learn more about
Looks like you're recursively calling imageThumbnailcall. If you don't end the recursion at some point, you could see exactly the symptoms that you are reporting.
if self.isInVC {
self.imageThumbnailcall()
}
Are you making sure to set isInVC properly so you break out of the recursive loop?

Fail to change the label text with a timer in swift

As a swift beginner, I'm building a simple app which will get data from a website to update the label text.
In the ViewController.swift I begin with a function called requestCycle():
override func viewDidLoad() {
super.viewDidLoad()
requestCycle()
}
In this requestCycle() function I create a timer to call the http request function basicAuthHttpRequest():
func requestCycle(){
self.timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(ViewController.basicAuthHttpRequest), userInfo: nil, repeats: true);
RunLoop.current.add(self.timer, forMode: RunLoopMode.commonModes);
}
In the basicAuthHttpRequest() function, I set up an http request to get data from a url, and use the data to update the label text:
...
//http request, parse json, store the data in TempIn
let TempInString = String(describing: TempIn!)
self.TempInLabel.text = TempInString
print(TempInString)
print(self.TempInLabel.text!)
...
When I run the app, the data will be printed("33" and Optional("33")) and NO ERRORs occur. However, the text of the label shown is not changed at all.
If I use a button to trigger the function basicAuthHttpRequest(), after clicking the button, the label text will be changed in a few seconds.
What's wrong with my poor timer? =.=
You have to update text on the main thread.
DispatchQueue.main.async {
let TempInString:String = String(describing: TempIn!)
self.TempInLabel.text = TempInString as String
}
Try below code :-
DispatchQueue.main.async(execute: { self.TempInLabel.text = TempInString as String })
The timer is running on background thread. so you have to update label value with main thread. You have to use below code for the same.
self.performSelector(onMainThread: #selector(updateUI), with: nil, waitUntilDone: true)
func updateUI() {
print("here update your UI")
}
Also you can do with OperationQueue.
OperationQueue.main.addOperation({
print("here update your UI")
})

How to manage download queue?

I am taking user input to download files from the server. The downloading task may include requesting web services.
I am expecting something like this:
1) Whenever the user selects a file to download or requesting for web
service, then it should be treated as one block of operation or task
and should go in the queue which will be managed globally at the app
level.
2) At the same time if the queue is empty then it should
automatically start executing the current task.
3) If queue contains
any operation then it should execute all old operation in
synchronously then execute the last one.
Can any one suggest how this can be done by the optimized way?
Take a look what I tried:
class func downloadChaptersFromDownloadQueue() {
let gbm = GlobalMethods()
for chapterDetail in gbm.downloadOpertationQueue.array.enumerated() {
if chapterDetail.element.chapterdata.state == .non || chapterDetail.element.chapterdata.state == .paused || chapterDetail.element.chapterdata.state == .downloading {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloading
let s = DispatchSemaphore(value: 0)
self.downloadActivty(courseId: chapterDetail.element.courseId, mod: chapterDetail.element.chapterdata, selectedIndexpath: chapterDetail.element.cellForIndexpath, success: { (result) in
if (result) {
if (WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .downloaded)) {
s.signal()
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloaded
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": 1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
else {
s.signal()
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
}
else {
_ = WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .non)
s.signal()
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
})
s.wait()
}
}
}
Create an async queue. First use a dispatch group to track how many requests are completed and get notified when all are finished (completely async).
Next, enqueue ALL your requests. Each request should have a unique identifier so you know which request completed or failed (the chapterId & pageNumber in your case should be enough).
Execute all the requests at once (again it is async) and you will be notified when each one completes (on the main queue through your completion block). The completion block should be called with all the requests responses and their unique identifiers.
Example:
class NetworkResponse {
let data: Data?
let response: URLResponse?
let error: Error?
init(data: Data?, response: URLResponse?, error: Error?) {
self.data = data
self.response = response
self.error = error
}
}
class NetworkQueue {
static let instance = NetworkQueue()
private let group = DispatchGroup()
private let lock = DispatchSemaphore(value: 0)
private var tasks = Array<URLSessionDataTask>()
private var responses = Dictionary<String, NetworkResponse>()
private init() {
}
public func enqueue(request: URLRequest, requestID: String) {
//Create a task for each request and add it to the queue (we do not execute it yet). Every request that is created, we enter our group.
self.group.enter();
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
//Only one thread can modify the array at any given time.
objc_sync_enter(self)
self.responses.updateValue(NetworkResponse(data: data, response: response, error: error), forKey: requestID)
objc_sync_exit(self)
//Once the request is complete, it needs to leave the group.
self.group.leave()
}
//Add each task to the queue.
self.tasks.append(task)
}
public func execute(completion: #escaping (_ responses: Dictionary<String, NetworkResponse>) -> Void) {
//Get notified on the main queue when every single request is completed (they all happen asynchronously, but we get one notification)
self.group.notify(queue: DispatchQueue.main) {
//Call our completion block with all the responses. Might be better to use a sorted dictionary or something here so that the responses are in order.. but for now, a Dictionary with unique identifiers will be fine.
completion(self.responses)
}
//Execute every task in the queue.
for task in self.tasks {
task.resume()
}
//Clear all executed tasks from the queue.
self.tasks.removeAll()
}
}
EDIT (Using your own code):
class func downloadChaptersFromDownloadQueue() {
let gbm = GlobalMethods()
let group = DispatchGroup()
let lock = NSLock()
//Get notified when ALL tasks have completed.
group.notify(queue: DispatchQueue.main) {
print("FINISHED ALL TASKS -- DO SOMETHING HERE")
}
//Initially enter to stall the completion
group.enter()
defer {
group.leave() //Exit the group to complete the enqueueing.
}
for chapterDetail in gbm.downloadOpertationQueue.array.enumerated() {
if chapterDetail.element.chapterdata.state == .non || chapterDetail.element.chapterdata.state == .paused || chapterDetail.element.chapterdata.state == .downloading {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloading
//Enter the group for each downloadOperation
group.enter()
self.downloadActivty(courseId: chapterDetail.element.courseId, mod: chapterDetail.element.chapterdata, selectedIndexpath: chapterDetail.element.cellForIndexpath, success: { (result) in
lock.lock()
defer {
group.leave() //Leave the group when each downloadOperation is completed.
}
if (result) {
if (WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .downloaded)) {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloaded
lock.unlock()
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": 1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
else {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
lock.unlock()
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
}
else {
_ = WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .non)
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
lock.unlock()
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
})
}
}
}
Again, this is asynchronous because you don't want the user waiting forever to download 100 pages..
For tasks like these the first thing you need to do is to do them asynchronously using dispatch_async, so that they are on a different thread and don't impact the performance of the application (or freeze it.)
Whenever your downloads succeeds/fails, you can always control what happens next in it's completion block. (I suggest you use recursion for what you're trying to achieve as it suits your needs).
Hope this helps!

Swift Timer.scheduledTimer() doesn't work

I have two views in my swift app. I am performing a segue as below.
ViewController.swift -----------------> GameViewController.swift
When loading the GameViewController an value array also passed to GameViewController.swift from ViewController.swift
A timer should be initialized in GameViewController.swift
I tried to initialize a timer and call a method through it, but it doesn't work.
Followings are my code snippets.
ViewController.swift
func signIn(difficultyLvl:String){
let username = usernameTxt.text
let password = passwordTxt.text
let url = URL(string: "http://192.168.1.106/speed/scoreBoardController.php?username="+username!+"&password="+password!+"&action=SIGNIN")
let task = URLSession.shared.dataTask(with: url!) {(data, response, error) in
let isPassed = String(data: data!, encoding:.utf8)?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
var gameViewControllerParams = [Int: [String: String]]()
gameViewControllerParams[0] = ["userId" : isPassed!]
gameViewControllerParams[1] = ["difficultyLvl" : difficultyLvl]
if(isPassed != "null"){
self.performSegue(withIdentifier: "gotoGame", sender: gameViewControllerParams)
}
}
task.resume()
}
GameViewController.swift
class GameViewController: UIViewController {
var gameViewControllerParams = [Int: [String: String]]()
override func viewDidLoad() {
super.viewDidLoad()
let _ = Timer.scheduledTimer(timeInterval: 1.0, target:self, selector: #selector(self.setCalculationLs), userInfo:nil,repeats: true)
}
func setCalculationLs(){
print("Timing")
}
}
Timers don't work on background queues (without some sleight of hand involving creating run loops or manually scheduling it on an existing run loop). But you should never initiate any UI update from anything other than the main queue, anyway.
So, since you're calling performSegue from a URLSession completion closure (which runs on a background queue), it's actually running viewDidLoad from the background queue, too. Thus the attempt to schedule the timer is failing. To get around this, you have to manually dispatch the performSegue code to the main queue:
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
...
if isPassed != "null" {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "gotoGame", sender: ...)
}
}
}
If you're ever unsure whether some code is running on the main queue or not, refer to the documentation. Or you can use a dispatch precondition:
dispatchPrecondition(condition: .onQueue(.main))
That way it will (in debug builds) stop the app if you've accidentally invoked the code from a background queue.
Unrelated to your current problem, but as an aside, to avoid a strong reference cycle between the timer and the view controller, you generally want to keep a reference to the timer so that you can invalidate it when the view disappears (e.g. create timer in viewDidAppear and remove it in viewDidDisappear). Otherwise you can end up retaining the GameViewController after it was dismissed, e.g.:
class GameViewController: UIViewController {
weak var timer: Timer?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
timer = Timer.scheduledTimer(timeInterval: 1.0, target:self, selector: #selector(setCalculationLs(_:)), userInfo: nil, repeats: true)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
timer?.invalidate()
}
#objc func setCalculationLs(_ timer: Timer) {
print("Tick")
}
}
Or in iOS 10 or later, you can use the block-based variant with weak reference to self, and invalidate in deinit:
class GameViewController: UIViewController {
weak var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
self?.setCalculationLs()
}
}
deinit {
timer?.invalidate()
}
func setCalculationLs() {
print("Tick")
}
}
Swift 5.5
I was having this issue as well while working in a segued view controller. Since segues are executed as a background thread to the main thread (main view controller), running timer within the segued view controller won't work. So, that being known and also knowing timers don't work in background threads, I did some digging and found this Apple Documentation gem.
I added the following line right after my scheduled timer. The timer now works beautifully. It is just sending the timer to the main thread for execution.
myTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.updateWithTimer), userInfo: nil, repeats: true)
RunLoop.main.add(myTimer, forMode: .common)

How can I create delay in iOS Swift so task A ends before task B begins?

Basically what I need is followings:
start task A (play sound)
wait until task A finish
start task B (listen to mic for 4 seconds)
stop task B
repeat 1-4
I use dispatch_after for step 3 and 4 but that didn't work. The simulator played all the sound files without listening to the mic. How can I fix this problem?
#IBAction func play(sender: AnyObject) {
var words = ["laud","puff","keen","death","knock"]
for item in words {
let path = NSBundle.mainBundle().pathForResource(item, ofType: "m4a")
let fileURL = NSURL(fileURLWithPath: path!)
do {
player = try AVAudioPlayer(contentsOfURL: fileURL)
player.delegate = self
player.play()
} catch {
print("Error AVAudioPlayer")
}
while player.playing {
print("Playing")
}
print("stop playing")
startListening()
let seconds = 4.0
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(seconds * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), { () -> Void in
self.stopListening()
})
}
}
Since you have already set player.delegate = self, you need to implement the method (See documentation here)
audioPlayerDidFinishPlaying(_:successfully:)
to catch the event that audio playing was finished
func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
if flag {
startLitening()
}
}
Then, you can use #John 's answer which is NSTimer to do the remaining tasks
let seconds = 4.0
let timer = NSTimer.scheduledTimerWithTimeInterval(seconds * Double(NSEC_PER_SEC), target: self, selector: "stopListening:", userInfo: "", repeats: false)
stopListening() {
// do anything else
playAudioAgain()
}
Use a NSTimer:
let timer = NSTimer.scheduledTimerWithTimeInterval(seconds * Double(NSEC_PER_SEC), target: self, selector: "stopListening:", userInfo: "", repeats: false)

Resources