I'm building an app which it communicates with socket periodically.
As long as the socket is open, then data will be transmitting from time to time.
However, my textFields(representing the data) do not update itself unless another view is introduces.
From the image above, initially my app will scans for the QR code as authentication method. Assuming authentication succeeded and the First view is loaded(the view after navigation controller).
The problem is it took quite some time to get the data.
Code that describe how I maneuver the connection.
override func viewDidLoad() {
super.viewDidLoad()
print("First view loaded")
//add observer
NSNotificationCenter.defaultCenter().addObserver(self, selector: "reachabilityStatusChanged", name: "ReachStatusChanged", object: nil)
reachabilityStatusChanged()
}
func reachabilityStatusChanged(){
switch reachabilityStatus{
case NOACCESS:
print("No access")
dispatch_async(dispatch_get_main_queue()){
print("No-access")
self.displayAlertMessage("No internet access, please try again later")
}
default:
dispatch_async(dispatch_get_main_queue()){
self.api.reportStatus()
self.api.importData()
socket.connect()
//data will be sent to text field at this point
}
}
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
print("First view appeared")
self.circleProgressView.progress = progressSliderValue
dispatch_async(dispatch_get_main_queue()){
self.distanceLabel.text = "\(appUserMileage)"
self.mileageLabel.text = "Target: \(appUserTarget) km"
}
}
From the code above as you can tell as the view is loaded(the view after navigation controller), it checks for network access. If network available then it will communicate with socket but by the time data gets in, the view is already loaded and appeared which means no data will be displayed at that time.
Is there a way to update the views periodically? I've done some research but all of them is about background fetching data which isn't suitable for my case.
I don't know where your appUserMileage and appUserTarget variables live, but how about something like this?
var appUserMileage: Int? {
didSet {
self.distanceLabel?.text = "\(appUserMileage ?? 0)" //? in case model calls this before outlets are loaded
}
}
var appUserTarget: Int? {
didSet {
self.mileageLabel?.text = "Target: \(appUserTarget ?? 0) km"
}
}
Related
I am pretty new to the swift, I am trying to implement the feature that when the user logged in, they will get directly to the home page rather than the login page every time they reopen the app.
I took the reference to the tutorial: https://www.youtube.com/watch?v=gjYAIXjpIS8&t=146s. and I implemented the is logged in boolean checking as he did, but I somehow encounter the trouble reopen the homepage while logged in. I have an error message:[Presentation] Attempt to present <UITabBarController: 0x7fa68102ea00> on <IFTTT.ViewController: 0x7fa67fe0c150> (from <IFTTT.ViewController: 0x7fa67fe0c150>) whose view is not in the window hierarchy.
This is how my login page controller class:(which is the entry point when opening the app) I tried present as the tutorial and performsegue and both shows up the same error message above
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if isLoggedIn() {
performSegue(withIdentifier: "logInJump", sender: nil)
}
}
fileprivate func isLoggedIn() -> Bool {
print("logged in status: \(UserDefaults.standard.bool(forKey: "isLoggedIn"))")
return UserDefaults.standard.bool(forKey: "isLoggedIn")
}
#IBAction func signInButton(_ sender: Any) {
print("sign in tapped")
if let appURL = URL(string: "http://vocation.cs.umd.edu/flask/register") {
UIApplication.shared.open(appURL) { success in
if success {
print("The URL was delivered successfully.")
} else {
print("The URL failed to open.")
}
}
} else {
print("Invalid URL specified.")
}
}
}
// button broder width
#IBDesignable extension UIButton {
#IBInspectable var borderWidth: CGFloat {
set {
layer.borderWidth = newValue
}
get {
return layer.borderWidth
}
}
#IBInspectable var cornerRadius: CGFloat {
set {
layer.cornerRadius = newValue
}
get {
return layer.cornerRadius
}
}
#IBInspectable var borderColor: UIColor? {
set {
guard let uiColor = newValue else { return }
layer.borderColor = uiColor.cgColor
}
get {
guard let color = layer.borderColor else { return nil }
return UIColor(cgColor: color)
}
}
}
and this is my messy story board and segues :
story board
I tried adding a navigation controller that the app entry point gets in there and it performs the isLoggedIn the same as the view controller class did, but it also has the same error.
Can someone walk me through how to fix it or any other better techniques? I felt like I am blind since I just get into the study of swift. Thank you!
You need to thread your request to perform the segue. Because your calling performSegue in the viewDidLoad() what happens is that your call is being called before everything is loaded, so you need to introduce some lag.
Threads are sometimes called lightweight processes because they have
their own stack but can access shared data. Because threads share the
same address space as the process and other threads within the
process, the operational cost of communication between the threads is
low, which is an advantage
An asynchronous function will await the execution of a promise, and an
asynchronous function will always return a promise. The promise
returned by an asynchronous function will resolve with whatever value
is returned by the function
Long story short, you need to wait for everything to be loaded into memory and if you're calling functions from the main stack/thread e.g. viewDidLoad() then there is a good chance that it hasn't been loaded into memory yet. Meaning, logInJump segue doesn't exist at that point in that view controller, thus your error.
The other possibility is you don't have the right view/segue ID but that should've thrown a different error.
Also, change the sender from nil to self. Actually this isn't necessary but I've always used self over nil
// change to desired number of seconds should be higher then 0
let when = DispatchTime.now() + 0.2
if isLoggedIn() {
DispatchQueue.main.asyncAfter(deadline: when) {
self.dismiss(animated: true, completion: nil)
self.performSegue(withIdentifier: "logInJump", sender: nil)
}
}
I am a beginner in iOS development, and I want to make an instagram clone app, and I have a problem when making the news feed of the instagram clone app.
So I am using Firebase to store the image and the database. after posting the image (uploading the data to Firebase), I want to populate the table view using the uploaded data from my firebase.
But when I run the app, the dummy image and label from my storyboard overlaps the downloaded data that I put in the table view. the data that I download will eventually show after I scroll down.
Here is the gif when I run the app:
http://g.recordit.co/iGIybD9Pur.gif
There are 3 users that show in the .gif
username (the dummy from the storyboard)
JokowiRI
MegawatiRI
After asynchronously downloading the image from Firebase (after the loading indicator is dismissed), I expect MegawatiRI will show on the top of the table, but the dummy will show up first, but after I scroll down and back to the top, MegawatiRI will eventually shows up.
I believe that MegawatiRI is successfully downloaded, but I don't know why the dummy image seems overlaping the actual data. I don't want the dummy to show when my app running.
Here is the screenshot of the prototype cell:
And here is the simplified codes of the table view controller:
class NewsFeedTableViewController: UITableViewController {
var currentUser : User!
var media = [Media]()
override func viewDidLoad() {
super.viewDidLoad()
tabBarController?.delegate = self
// to set the dynamic height of table view
tableView.estimatedRowHeight = StoryBoard.mediaCellDefaultHeight
tableView.rowHeight = UITableViewAutomaticDimension
// to erase the separator in the table view
tableView.separatorColor = UIColor.clear
}
override func viewWillAppear(_ animated: Bool) {
// check wheter the user has already logged in or not
Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
RealTimeDatabaseReference.users(uid: user.uid).reference().observeSingleEvent(of: .value, with: { (snapshot) in
if let userDict = snapshot.value as? [String:Any] {
self.currentUser = User(dictionary: userDict)
}
})
} else {
// user not logged in
self.performSegue(withIdentifier: StoryBoard.showWelcomeScreen, sender: nil)
}
}
tableView.reloadData()
fetchMedia()
}
func fetchMedia() {
SVProgressHUD.show()
Media.observeNewMedia { (mediaData) in
if !self.media.contains(mediaData) {
self.media.insert(mediaData, at: 0)
self.tableView.reloadData()
SVProgressHUD.dismiss()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: StoryBoard.mediaCell, for: indexPath) as! MediaTableViewCell
cell.currentUser = currentUser
cell.media = media[indexPath.section]
// to remove table view highlight style
cell.selectionStyle = .none
return cell
}
}
And here is the simplified code of the table view cell:
class MediaTableViewCell: UITableViewCell {
var currentUser: User!
var media: Media! {
didSet {
if currentUser != nil {
updateUI()
}
}
}
var cache = SAMCache.shared()
func updateUI () {
// check, if the image has already been downloaded and cached then just used the image, otherwise download from firebase storage
self.mediaImageView.image = nil
let cacheKey = "\(self.media.mediaUID))-postImage"
if let image = cache?.object(forKey: cacheKey) as? UIImage {
mediaImageView.image = image
} else {
media.downloadMediaImage { [weak self] (image, error) in
if error != nil {
print(error!)
}
if let image = image {
self?.mediaImageView.image = image
self?.cache?.setObject(image, forKey: cacheKey)
}
}
}
So what makes the dummy image overlaps my downloaded data?
Answer
The dummy images appear because your table view controller starts rendering cells before your current user is properly set on the tableViewController.
Thus, on the first call to cellForRowAtIndexPath, you probably have a nil currentUser in your controller, which gets passed to the cell. Hence the didSet property observer in your cell class does not call updateUI():
didSet {
if currentUser != nil {
updateUI()
}
}
Later, you reload the data and the current user has now been set, so things start to work as expected.
This line from your updateUI() should hide your dummy image. However, updateUI is not always being called as explained above:
self.mediaImageView.image = nil
I don't really see a reason why updateUI needs the current user to be not nil. So you could just eliminate the nil test in your didSet observer, and always call updateUI:
var media: Media! {
didSet {
updateUI()
}
Alternatively, you could rearrange your table view controller to actually wait for the current user to be set before loading the data source. The login-related code in your viewWillAppear has nested completion handers to set the current user. Those are likely executed asynchronously .. so you either have to wait for them to finish or deal with current user being nil.
Auth.auth etc {
// completes asynchronously, setting currentUser
}
// Unless you do something to wait, the rest starts IMMEDIATELY
// currentUser is not set yet
tableView.reloadData()
fetchMedia()
Other Notes
(1) I think it would be good form to reload the cell (using reloadRows) when the image downloads and has been inserted into your shared cache. You can refer to the answers in this question to see how an asynch task initiated from a cell can contact the tableViewController using NotificationCenter or delegation.
(2) I suspect that your image download tasks currently are running in the main thread, which is probably not what you intended. When you fix that, you will need to switch back to the main thread to either update the image (as you are doing now) or reload the row (as I recommend above).
Update your UI in main thread.
if let image = image {
DispatchQueue.main.async {
self?.mediaImageView.image = image
}
self?.cache?.setObject(image, forKey: cacheKey)
}
Is viewWillAppear the best place in the lifecycle to import my data from a webservice? This relates to a small exchange rate app.
In a tableview from viewwillappear, we go to http://api.fixer.io to update an array called rates, and all of the returned data in a class RatesData. If the Internet connection fails we either use the data we already have, or look to a file on the phone file system.
The time it takes to import the data means that I run cellForRowAt indexPath before my data array is populated; meaning that the data appears after a perceptible delay (I've default cells to load) before being updated with exchange rates.
I will implement coredata next as a better solution but the first time the app runs we would still get this undesired effect.
override func viewWillAppear(_ animated: Bool) {
searchForRates()
importCountriessync()
}
private func searchForRates(){
Request.fetchRates(withurl: APIConstants.eurURL) {[weak self] (newData:RatesData, error:Error?)->Void in
DispatchQueue.main.async {
//update table on the main queue
//returns array of rates
guard (error == nil) else {
print ("did not recieve data - getting from file if not already existing")
if ( self?.rates == nil)
{
self?.searchForFileRates()
}
return
}
self?.rates = newData.rates
let newData = RatesData(base: newData.base, date: Date(), rates: newData.rates)
self?.ratesFullData = newData
self?.tableView.reloadData()
}
}
}
func searchForFileRates(){
print ("file rates")
Request.fetchRates(withfile: "latest.json") { [weak self] (newData: RatesData)->Void in
DispatchQueue.main.async {
//update table on the main queue
//returns array of rates
self?.rates = newData.rates
let newData = RatesData(base: newData.base, date: Date(), rates: newData.rates)
self?.ratesFullData = newData
self?.tableView.reloadData()
}
}
}
Yes viewWillAppear is fine as long as the fetch is asynchronous.
Just remember it will be fired every time the view appears. Example when this view controller is hidden by another modal view controller and the modal view controller is dismissed, viewWillAppear will be called. If you want it to be called only once you could invoke it in viewDidLoad
Summary
viewWillAppear - Invoked every time view appears
viewDidLoad - Invoked once when the view first loads
Choose what meets your needs.
I have some information to send to Firebase. The thing is I want to send the data but I also have to pull the data from there first. The data I get is based on the users input.
I'm already making several nested async calls to Firebase. Not only do i have to wait for the calls to finish to make sure the data has been set but I don't want to have the user waiting around unnecessarily when they can leave the scene and the data can be pulled and changed in a background task.
I was thinking about using a NSNotification after the performSegueWithIdentifier is triggered. The observer for the notification would be inside viewWillDisappear.
Is this safe to do and if not what's the best way to go about it?
Code:
var ref: FIRDatabaseReference!
let uid = FIRAuth.auth()?.currentUser?.uid
let activityIndicator = UIActivityIndicatorView()
override func viewDidLoad() {
super.viewDidLoad()
self.ref = FIRDatabase.database().reference().child(self.uid!)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(fetchSomeValueFromFBThenUpdateAndResendAnotherValue), name: "FbFetchAndSend", object: nil)
}
#IBAction func buttonPressed(sender: UIButton) {
activityIndicator.startAnimating()
levelTwoRef //send levelTwo data to FB run 1st callback
scoreRef //send score data to FB run 2nd callback
powerRef //send power data to FB run 3rd callback
lifeRef //send life data to FB run Last callback for dispatch_async...
dispatch_async(dispatch_get_main_queue()){
activityIndicator.stopAnimating()
performSegueWithIdentifier....
//Notifier fires after performSegue???
NSNotificationCenter.defaultCenter().postNotificationName("FbFetchAndSend", object: nil)
}
}
func fetchSomeValueFromFBThenUpdateAndResendAnotherValue(){
let paymentRef = ref.child("paymentNode")
paymentRef?.observeSingleEventOfType(.Value, withBlock: {
(snapshot) in
if snapshot.exists(){
if let dict = snapshot.value as? [String:AnyObject]{
let paymentAmount = dict["paymentAmount"] as? String
let updatePayment = [String:AnyObject]()
updatePayment.updateValue(paymentAmount, forKey: "paymentMade")
let updateRef = self.ref.child("updatedNode")
updateRef?.updateChildValues(updatePayments)
}
You are adding the observer in viewWillDisappear, So it won't get fired because it won't be present when your segue is performed.
Add the observer in viewDidLoad and it will work.
But if you just want to call fetchSomeValueFromFBThenUpdateAndResendAnotherValue() when the view is disappearing then there is no need for observer.
Simply call the method on viewWillDisappear like this -
override func viewWillDisappear(animated: Bool)
{
super.viewWillDisappear(animated)
fetchSomeValueFromFBThenUpdateAndResendAnotherValue()
}
i have this view controller
class ViewController: UIViewController {
override func viewWillAppear(animated: Bool) {
let user = NSUserDefaults()
let mobileNumber = user.valueForKey("mobileNumber") as? String
if let mobileNumber = mobileNumber {
print("mobile number = \(mobileNumber)")
}else {
print("no mobile number")
}
}
#IBAction func makePhoneCall(sender: UIButton) {
if let phoneCall = phoneCall {
let user = NSUserDefaults()
user.setValue(phoneCall, forKey: "mobileNumber")
when the user clicks on a button, i save the mobileNumber in nsuserdefault.
then i click the button, then i open the app again, but problem is that when i open the app agian, i don't bet any message from the viewWillAppear even though i am printing in the if and in the else part.
tylersimko is correct that viewWillAppear(_:) is not called when the application enters the foreground and that event is instead captured by "application will enter background".
That said, you don't need to observe this from the app delegate but could instead use the UIApplicationWillEnterForegroundNotification notification:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "applicationDidEnterForeground", name: UIApplicationWillEnterForegroundNotification, object: nil)
}
func applicationDidEnterForeground() {
// Update variable here.
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
The above code:
When your view loads, your view controller registers to have the function applicationDidEnterForeground() called whenever the application enters the foreground.
The function applicationDidEnterForeground() does whatever needs to be done.
The view controller unregisters from all notifications when it deallocates to avoid a zombie reference in iOS versions before 9.0.
Given that you are working with NSUserDefaults, you could instead consider observing NSUserDefaultsDidChangeNotification.
In AppDelegate.swift, make your change in applicationWillEnterForeground:
func applicationWillEnterForeground(application: UIApplication) {
// do something
}
Alternatively, if you want to keep your changes in the ViewController, you could set up a function and call it like this:
func applicationWillEnterForeground(application: UIApplication) {
ViewController.refreshView()
}