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")
})
Related
I'm somewhat new to this and this is my first question on stackoverflow. Thanks in advance for your help and bear with me if my formatting sucks
I've got multiple views within my app (all displaying data using tableview subviews) that need to update automatically when the data changes on the database (Firestore), i.e. another user updates the data.
I've found a way to do this which is working well, but I want to ask the community if there's a better way.
Currently, I am creating a Timer object with a timeInterval of 2. On the interval, the timer queries the database and checks a stored data sample against updated data. If the two values vary, I run viewDidLoad which contains my original query, tableView.reloadData(), etc..
Any suggestions or affirmations would be very useful.
var timer = Timer()
var oldChallengesArray = [String]()
var newChallengesArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
//set tableview delegate
mainTableView.delegate = self
mainTableView.dataSource = self
//set challengesmodel delegate
challengesModel.delegate = self
//get challenges
DispatchQueue.main.async {
self.challengesModel.getChallenges(accepted: true, challengeDenied: false, incomingChallenges: false, matchOver: false)
self.mainTableView.reloadData()
}
scheduledTimerWithTimeInterval()
}
func scheduledTimerWithTimeInterval(){
// Scheduling timer to Call the function "updateCounting" with the interval of 1 seconds
timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(self.updateTableView), userInfo: nil, repeats: true)
}
#objc func updateTableView(){
ChallengeService.getAllUserChallengeIDs(accepted: true, challengeDenied: false, matchOver: false) { (array) in
if array.isEmpty {
return
} else {
self.newChallengesArray = array
if self.oldChallengesArray != self.newChallengesArray {
self.oldChallengesArray = self.newChallengesArray
self.newChallengesArray.removeAll()
self.viewDidLoad()
}
}
}
}
Firestore is a "realtime database", that means that the database warns you when changes happen to the data. To achieve that the app needs to subscribe to relevant changes in the db. The sample code below can be found here:
db.collection("cities").document("SF")
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
print("Current data: \(data)")
}
Also, I would like to point out that calling viewDidLoad is incorrect, you should never call viewDidLoad yourself, create an func to update the data. Something like this:
DispatchQueue.main.async {
self.mainTableView.reloadData()
}
I have a project in which I need to update the user's current location coordinates on the API server. How can I achieve this? Is continuously calling the API a good idea (I think no)?
I need to update coordinates continuously so other users can see me like in the Uber app the very first time we can see drivers near me.
Maybe you can implement a timer, and make a POST request by sending your lat long periodically.
class ViewController: UIViewController {
var timer: Timer!
override func viewDidLoad() {
timer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(makeRequest), userInfo: nil, repeats: true)
}
#objc func makeRequest() {
// make your post request here.
}
}
For example the timer given above will call makeRequest method every 10 seconds.
If you don't know much about networking, make a research on the following topics:
Swift Networking Layer, Alamofire, URLSession
**Step 1 :- Get latitude and long. of the user first.**
Reference URL:- https://stackoverflow.com/questions/12736086/how-to-get-location-latitude-longitude-value-in-variable-on-ios
**Step 2:- Define following variables globally.**
var timer:Timer?
var sourceLatitude = CLLocationDegrees()
var sourceLongitude = CLLocationDegrees()
**Step 3:- Setup timer for continuous update as below.**
override func viewDidLoad()
{
super.viewDidLoad()
timer = Timer.scheduledTimer(timeInterval: 15, target: self, selector: #selector(self.updateDriverLoction), userInfo: nil, repeats: true)
}
**Step 4:- call appropriate method for sending and updating lat. long. to the server using any of networking API.(In my case I have used Alamofire)**
// must be internal or public.
#objc func updateDriverLoction() {
if Reachability.isConnectedToNetwork() {
let param1:[String:String] = [
"latitude" : "\(sourceLatitude)",
"longitude" : "\(sourceLongitude)"
]
ServerClass.sharedInstance.putRequestWithUrlParameters(param1, path: BASE_CAB_URL + PROJECT_URL.UPDATE_DRIVER_LOCATION_API, successBlock: { (json) in
print(json)
let success = json["success"].stringValue
if success == "true" {
}
else {
}
}, errorBlock: { (NSError) in
// UIAlertController.showInfoAlertWithTitle("Alert", message: kUnexpectedErrorAlertString, buttonTitle: "Okay")
})
}
}
else{
UIAlertController.showInfoAlertWithTitle("Alert", message: "Please Check internet connection", buttonTitle: "Okay")
}
}
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)
In my application, I have a tableview that constantly (every 5 seconds) needs updating. To do this, I have set a timer which runs a method updating the data source (array) and calling the tableView.reloadData() method immediately afterwards. Unfortunately, a problem arises after the reloadData() method is called, one in which I cannot select a row without first scrolling! It's very strange. All I have to do is slightly make the table view scroll, and I can select cells again. Help!
viewDidLoad
DataHandler.updateCustomers() {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.updateTable()
})
}
let _ = NSTimer.scheduledTimerWithTimeInterval(5, target: self, selector: "updateData", userInfo: nil, repeats: true)
updateData
func updateData(){
DataHandler.updateCustomers() {
if (self.activeIDs.indexPathForSelectedRow == nil){
self.activeIDs.reloadData()
}
}
print("Data updated")
}
Thanks in advance!
I would like to know how to run an action after making the console count a given time. For example printing 'hello' after 3 seconds.
Thank you!!
You need to use the NSTimer class.
Example usage:
var timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector("sayHello"), userInfo: nil, repeats: true)
Somewhere else in your class...
func sayHello() {
println("Hello")
}
You can create delays with following ways!!
You can create a delay using asynAfter
DispatchQueue.main.asyncAfter(deadline: .now() + <secondsToDelay>) {
// Put your code here that you want to execute after delay
}
Another way is calling a function itself after a delay using objc's perform selector
perform(#selector(delayedMethod), with: nil, afterDelay: <secondsToDelay>)
#objc func delayedMethod() {
print("My delayed method call")
}
Of course you can also use Timer like in Woodstock's answer.