I have notification badges for two of my tabs, at the 1 position and the 2 position of the UITabBarController. The badges for the number 1 position show fine and even if the app is open it will show the new badge number. However the 2 position tab shows once, and when the app is open or even closed it won't show the new badge number. It only will show the badge number after hours of inactivity. Would this be an issue with my code or a problem on my backend? The 1 position is request notifications and 2 position is chat messages.
class NewTabBarViewController: UITabBarController {
var numPendingRequests = 0
var numUnseenConversations = 0
override func viewDidLoad() {
super.viewDidLoad()
observeNumPendingRequests()
observeNumUnseenConversations()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
sendFCMToken()
observeNumPendingRequests()
observeNumUnseenConversations()
AuthService().getCurrentUserInfo()
// Show notification center if the app launched from a notification
// Typically from the background or killed app state
if NotificationService.launchedFromNotification {
showNotificationCenter()
NotificationService.launchedFromNotification = false
}
// Observe if the app is opened from a notification
// Typically from the foreground state
NotificationCenter.default.addObserver(self, selector: #selector(showNotificationCenter), name: NotificationService.didLaunchFromNotification, object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// Stop observing if the app is opened from a notification
NotificationCenter.default.removeObserver(self, name: NotificationService.didLaunchFromNotification, object: nil)
}
// Select the tab item with the notification center
#objc func showNotificationCenter() {
self.selectedIndex = 2
}
/**
Sends the users private Firebase Cloud Messaging Token to the database
to be used by the Cloud Functions for sending Push Notifications
*/
func sendFCMToken() {
guard let token = Messaging.messaging().fcmToken, let user = Auth.auth().currentUser else { return }
let db = Database.database().reference()
let ref = db.child("FCMToken/\(user.uid)")
ref.setValue(token)
}
/**
Observes the number of received requests that are PENDING
and updates the App Badge Icon Number and tabBar Item accordingly
*/
func observeNumPendingRequests() {
guard let user = Auth.auth().currentUser else { return }
let db = Database.database().reference()
let ref = db.child("receivedRequests/\(user.uid)").queryOrderedByValue().queryEqual(toValue: "PENDING")
ref.observe(.value, with: { snapshot in
self.numPendingRequests = Int(snapshot.childrenCount)
self.setApplicationBadgeCount()
})
}
func observeNumUnseenConversations() {
guard let user = Auth.auth().currentUser else { return }
let db = Database.database().reference()
let ref = db.child("conversations/users/\(user.uid)").queryOrdered(byChild: "seen").queryEqual(toValue: false)
ref.observe(.value, with: { snapshot in
self.numUnseenConversations = Int(snapshot.childrenCount)
self.setApplicationBadgeCount()
})
}
func setApplicationBadgeCount() {
let total = numUnseenConversations + numPendingRequests
UIApplication.shared.applicationIconBadgeNumber = Int(total)
self.tabBar.items?[1].badgeValue = numPendingRequests > 0 ? "\(numPendingRequests)" : nil
self.tabBar.items?[2].badgeValue = numUnseenConversations > 0 ? "\(numUnseenConversations)" : nil
}
}
Is there actually a number of unseen conversations being returned? If it's nil, then there will not be a badge. Double check that, and even try setting it to a hard-coded string for a quick and dirty test. Also, are you on tab 1 when trying to see the updated badge on tab 2? Try changing the tab 2 badge when you have that tab selected. You might also want to make sure that self.tabBar.items?[2] is not nil.
Related
In my application I use Firebase Realtime Database to store data about users. I would like to be sure that when I read this data to display it in the view (e.g. their nickname), that the reading has been done before displaying it. Let me explain:
//Initialization of user properties
static func initUsers(){
let usersref = dbRef.child("Users").child(userId!)
usersref.observeSingleEvent(of: .value) { (DataSnapshot) in
if let infos = DataSnapshot.value as? [String : Any]{
self.username = infos["username"] as! Int
//The VC is notified that the data has been recovered
let name = Notification.Name(rawValue: "dataRetrieved")
let notification = Notification(name: name)
NotificationCenter.default.post(notification)
}
}
}
This is the code that runs in the model and reads the user's data when they log in.
var isFirstAppearance = true
override func viewDidLoad() {
super.viewDidLoad()
//We initialise the properties associated with the user
Users.initUsers()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if isFirstAppearance {
let name = Notification.Name(rawValue: "dataRetrieved")
NotificationCenter.default.addObserver(self, selector: #selector(registerDataToView), name: name, object: nil)
isFirstAppearance = false
}
else{
registerDataToView()
}
}
//The user's data is assigned to the view
#objc func registerDataToView(){
usernameLabel.text = String(Users.username)
}
Here we are in the VC and when the view loads we call initUsers in viewDidLoad. In viewWillAppear, if it's the first time we load the view then we create a listener which calls registerDataToView if the reading in the database is finished. Otherwise we simply call registerDataToView (this is to update the labels when we return to this VC).
I would like to know if it is possible, for example when we have a very bad connection, that the listener does not intercept the dataRetrieved notification and therefore that my UI displays only the default texts? Or does the headset wait to receive the notification before moving on?
If it doesn't wait then how can I wait for the database read to finish before initializing the labels?
Thanks for your time :)
Don't wait. Never wait. Tell the receiver that the data are available with a completion handler
static func initUsers(completion: #escaping (Bool) -> Void) {
let usersref = dbRef.child("Users").child(userId!)
usersref.observeSingleEvent(of: .value) { (DataSnapshot) in
if let infos = DataSnapshot.value as? [String : Any]{
self.username = infos["username"] as! Int
completion(true)
return
}
completion(false)
}
}
And use it
override func viewDidLoad() {
super.viewDidLoad()
//We initialise the properties associated with the user
Users.initUsers { [unowned self] result in
if result (
// user is available
} else {
// user is not available
}
}
}
I have an iPhone application with a level in it that is based on the gravityY parameter of a device motion call to motionmanager. I have fixed the level to the pitch of the phone, as I wish to show the user whether the phone is elevated or declined relative to a flat plane (flat to the ground) through its x-axis...side to side or rotated is not relevant. To do that, I have programmed the app to slide an indicator (red when out of level) along the level (a bar)....its maximum travel is each end of the level.
The level works great...and a correct value is displayed, until the user locks the phone and puts it in his or her back pocket. While in this stage, the level indicator shifts to one end of the level (the end of the phone that is elevated in the pocket), and when the phone is pulled out and unlocked, the app does not immediately restore the level - it remains out of level, even if I do a manual function call to restore the level. After about 5 minutes, the level seems to restore itself.
Here is the code:
func getElevation () {
//now get the device orientation - want the gravity value
if self.motionManager.isDeviceMotionAvailable {
self.motionManager.deviceMotionUpdateInterval = 0.05
self.motionManager.startDeviceMotionUpdates(
to: OperationQueue.current!, withHandler: {
deviceMotion, error -> Void in
var gravityValueY:Double = 0
if(error == nil) {
let gravityData = self.motionManager.deviceMotion
let gravityValueYRad = (gravityData!.gravity.y)
gravityValueY = round(180/(.pi) * (gravityValueYRad))
self.Angle.text = "\(String(round(gravityValueY)))"
}
else {
//handle the error
self.Angle.text = "0"
gravityValueY = 0
}
var elevationY = gravityValueY
//limit movement of bubble
if elevationY > 45 {
elevationY = 45
}
else if elevationY < -45 {
elevationY = -45
}
let outofLevel: UIImage? = #imageLiteral(resourceName: "levelBubble-1")
let alignLevel: UIImage? = #imageLiteral(resourceName: "levelBubbleGR-1")
let highElevation:Double = 1.75
let lowElevation:Double = -1.75
if highElevation < elevationY {
self.bubble.image = outofLevel
}
else if elevationY < lowElevation {
self.bubble.image = outofLevel
}
else {
self.bubble.image = alignLevel
}
// Move the bubble on the level
if let bubble = self.bubble {
UIView.animate(withDuration: 1.5, animations: { () -> Void in
bubble.transform = CGAffineTransform(translationX: 0, y: CGFloat(elevationY))
})
}
})
}
}
I would like the level to restore almost immediately (within 2-3 seconds). I have no way to force calibration or an update. This is my first post....help appreciated.
Edit - I have tried setting up a separate application without any animation with the code that follows:
//
import UIKit
import CoreMotion
class ViewController: UIViewController {
let motionManager = CMMotionManager()
#IBOutlet weak var angle: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func startLevel(_ sender: Any) {
startLevel()
}
func startLevel() {
//now get the device orientation - want the gravity value
if self.motionManager.isDeviceMotionAvailable {
self.motionManager.deviceMotionUpdateInterval = 0.1
self.motionManager.startDeviceMotionUpdates(
to: OperationQueue.current!, withHandler: {
deviceMotion, error -> Void in
var gravityValueY:Double = 0
if(error == nil) {
let gravityData = self.motionManager.deviceMotion
let gravityValueYRad = (gravityData!.gravity.y)
gravityValueY = round(180/(.pi) * (gravityValueYRad))
}
else {
//handle the error
gravityValueY = 0
}
self.angle.text = "(\(gravityValueY))"
})}
}
}
Still behaves exactly the same way....
OK....so I figured this out through trial and error. First, I built a stopGravity function as follows:
func stopGravity () {
if self.motionManager.isDeviceMotionAvailable {
self.motionManager.stopDeviceMotionUpdates()
}
}
I found that the level was always set properly if I called that function, for example by moving to a new view, then restarting updates when returning to the original view. When locking the device or clicking the home button, I needed to call the same function, then restart the gravity features on reloading or returning to the view.
To do that I inserted the following in the viewDidLoad()...
NotificationCenter.default.addObserver(self, selector: #selector(stopGravity), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(stopGravity), name: NSNotification.Name.UIApplicationWillTerminate, object: nil)
This notifies the AppDelegate and runs the function. That fixed the issue immediately.
I'm implementing In-App purchases in an XCode project, and everything works fine except for one error. When the user isn't connected to the internet and he clicks a purchase button, the app crashes. I believe this happens because the in-app purchases haven't been fetched from iTunes and clicking the button can't run the purchase process. This crash also happens when the user clicks the button on the first second that the shop screen loads, because – I think – some time is needed to fetch (or request) the products. Here's what I'm talking about:
override func didMove(to view: SKView) {
...
fetchAvailableProducts()
}
func fetchAvailableProducts() {
// Put here your IAP Products ID's
let productIdentifiers = NSSet(objects:
productID100,
productID250,
productID500,
productIDRemoveAds,
productIDUnlockAll)
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
I'm basing my code on this tutorial.
Is there a way to change my code to make it "crash-proof", so that it first checks if the products can be bought to let you use the button?
I use SwiftyStorkeKit ( https://github.com/bizz84/SwiftyStoreKit ) , which uses a completion handler to fill in the products (will save you from reinventing the wheel as well - and is a great resource to learn from)
As for checking network connection, I use Apple's Reachability ( https://developer.apple.com/library/content/samplecode/Reachability/Introduction/Intro.html ). Here is the relevant parts of an implementation. It also check in situations where the app loses focus. You can also use the network check before any store operation.
class vcon: UIViewController {
#IBOutlet weak var noConnectionView: UIView!
func isNetworkAvailable() -> Bool {
//quick test if network is available
var netTest:Reachability? = Reachability(hostName: "apple.com")!
if netTest?.currentReachabilityStatus == .notReachable {
netTest = nil
return false
}
netTest = nil
return true
}
func displayNoNetworkView() {
//this example pulls from a storyboard to a view I have in front of everything else at all times, and shows the view to block everything else if the network isnt available
let ncvc = UIStoryboard(name: "HelpPrefsInfo", bundle: nil).instantiateViewController(withIdentifier: "noNetworkVC") as! noNetworkVC
ncvc.view.frame = noConnectionView.bounds
ncvc.view.backgroundColor = color03
ncvc.no_connection_imageView.tintColor = color01
ncvc.noInternetConnection_label.textColor = color01
noConnectionView.addSubview(ncvc.view)
}
func hideDataIfNoConnection() {
//the actual code that displays the no net connection view
if !isNetworkAvailable() {
if noConnectionView.isHidden == true {
noConnectionView.alpha = 0
noConnectionView.isHidden = false
self.iapObjects = []
UIView.animate(withDuration: 0.50, animations: {
self.noConnectionView.alpha = 1
}, completion:{(finished : Bool) in
});
}
} else {
if noConnectionView.isHidden == false {
self.collection_view.reloadData()
UIView.animate(withDuration: 0.50, animations: {
self.noConnectionView.alpha = 0
}, completion:{(finished : Bool) in
self.noConnectionView.isHidden = true
self.loadIAPData()
});
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: .UIApplicationDidBecomeActive, object: nil)
displayNoNetworkView()
loadIAPData()
}
func loadIAPData() {
load the data if the network is available
if isNetworkAvailable() {
helper.requestProductsWithCompletionHandler(completionHandler: { (success, products) -> Void in
if success {
self.iapObjects = products!
self.collection_view.reloadData()
} else {
let alert = UIAlertController(title: "Error", message: "Cannot retrieve products list right now.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
})
}
}
func willEnterForeground() {
hideDataIfNoConnection()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
hideDataIfNoConnection()
}
I'm implementing the login possibility with touchID using Swift.
Following: when the App is started, there is a login screen and a touchID popup - that's working fine. The problem occurs, when the app is loaded from background: I want the touchID popup appear over a login screen if a specific timespan hasn't been exceeded yet - but this time I want the touchID to go to the last shown view before the app entered background. (i.e. if the user wants to cancel the touchID, there is a login screen underneath where he then can authenticate via password, which leads him to the last shown view OR if the touchID authentication succeeded, the login screen should be dismissed and the last shown view presented.)
I really tried everything on my own, and searched for answers - nothing did help me. Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
//notify when foreground or background have been entered -> in that case there are two methods that will be invoked: willEnterForeground and didEnterBackground
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: "willEnterForeground", name:UIApplicationWillEnterForegroundNotification, object: nil)
notificationCenter.addObserver(self, selector: "didEnterBackground", name: UIApplicationDidEnterBackgroundNotification, object: nil)
password.secureTextEntry = true
if (username != nil) {
username.text = "bucketFit"
}
username.delegate = self
password.delegate = self
if let alreadyShown : AnyObject? = def.objectForKey("alreadyShown") {
if (alreadyShown == nil){
authenticateWithTouchID()
}
}
}
willEnterForeground:
func willEnterForeground() {
//save locally that the guide already logged in once and the application is just entering foreground
//the variable alreadyShown is used for presenting the touchID, see viewDidAppear method
def.setObject(true, forKey: "alreadyShown")
if let backgroundEntered : AnyObject? = def.objectForKey("backgroundEntered") {
let startTime = backgroundEntered as! NSDate
//number of seconds the app was in the background
let inactivityDuration = NSDate().timeIntervalSinceDate(startTime)
//if the app was longer than 3 minutes inactiv, ask the guide to input his password
if (inactivityDuration > 2) {
showLoginView()
} else {
def.removeObjectForKey("alreadyShown")
showLoginView()
}
}
}
authenticateWithTouchID():
func authenticateWithTouchID() {
let context : LAContext = LAContext()
context.localizedFallbackTitle = ""
var error : NSError?
let myLocalizedReasonString : NSString = "Authentication is required"
//check whether the iphone has the touchID possibility at all
if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error) {
//if yes then execute the touchID and see whether the finger print matches
context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: myLocalizedReasonString as String, reply: { (success : Bool, evaluationError : NSError?) -> Void in
//touchID succeded -> go to students list page
if success {
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.performSegueWithIdentifier("studentsList", sender: self)
})
} else {
// Authentification failed
print(evaluationError?.description)
//print out the specific error
switch evaluationError!.code {
case LAError.SystemCancel.rawValue:
print("Authentication cancelled by the system")
case LAError.UserCancel.rawValue:
print("Authentication cancelled by the user")
default:
print("Authentication failed")
}
}
})
}
}
shouldPerformSegueWithIdentifier:
override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
if (false) { //TODO -> username.text!.isEmpty || password.text!.isEmpty
notify("Login failed", message: "Please enter your username and password to proceed")
return false
} else if (false) { //TODO when backend ready! -> !login("bucketFit", password: "test")
notify("Incorrect username or password", message: "Please try again")
return false
//if the login page is loaded after background, dont proceed (then we need to present the last presented view on the stack before the app leaved to background)
} else if let alreadyShown : AnyObject? = def.objectForKey("alreadyShown") {
if (alreadyShown != nil){
//TODO check whether login data is correct
dismissLoginView()
return false
}
}
return true
}
Thank you in advance.
What you could do is create a AuthenticationManager. This manager would be a shared instance which keep track of whether authentication needs to be renewed. You may also want this to contain all of the auth methods.
class AuthenticationManager {
static let sharedInstance = AuthenticationManager()
var needsAuthentication = false
}
In AppDelegate:
func willEnterForeground() {
def.setObject(true, forKey: "alreadyShown")
if let backgroundEntered : AnyObject? = def.objectForKey("backgroundEntered") {
let startTime = backgroundEntered as! NSDate
//number of seconds the app was in the background
let inactivityDuration = NSDate().timeIntervalSinceDate(startTime)
//if the app was longer than 3 minutes inactiv, ask the guide to input his password
if (inactivityDuration > 2) {
AuthenticationManager.sharedInstance.needsAuthentication = true
}
}
}
Then, subclass UIViewController with a view controller named SecureViewController. Override viewDidLoad() in this subclass
override fun viewDidLoad() {
super.viewDidLoad()
if (AuthenticationManager.sharedInstance().needsAuthentication) {
// call authentication methods
}
}
Now, make all your View Controllers that require authentication subclasses of SecureViewController.
I'm trying to understand CloudKit, but I'm having an issue figuring out a real-world issue if I eventually turned it into an app.
I've got a tableviewcontroller that's populated by CloudKit, which works fine. I've got a detailviewcontroller that is pushed when you tap one of the entries in the tableview controller. The detailviewcontroller pulls additional data from CloudKit that wasn't in the initial tableviewcontroller. All of this works perfectly fine.
Now, the detailviewcontroller allows changing of an image and saving it back to a public database. That's working fine as well.
The scenario I'm dealing with is thinking about this core function being used in an app by multiple people. If one person uploads a photo (overwriting an existing photo for one of the records), what happens if another user currently has the app in the background? If they open the app, they'll see the last photo they saw on that page, and not the new photo. So I want to reload the data from CloudKit when the app comes back to the foreground, but I must be doing something wrong, because it's not working. Using the simulator and my actual phone, I can change a photo on one device and the other device (simulator or phone) doesn't update the data when it comes into the foreground.
Here's the code I'm using: First in ViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
println("viewDidLoad")
NSNotificationCenter.defaultCenter().addObserver(self, selector: "activeAgain", name: UIApplicationWillEnterForegroundNotification, object: nil)
and then the activeAgain function:
func activeAgain() {
println("Active again")
fetchItems()
self.tblItems.reloadData()
println("Table reloaded")
}
and then the fetchItems function:
func fetchItems() {
let container = CKContainer.defaultContainer()
let publicDatabase = container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Items", predicate: predicate)
publicDatabase.performQuery(query, inZoneWithID: nil) { (results, error) -> Void in
if error != nil {
println(error)
}
else {
println(results)
for result in results {
self.items.append(result as! CKRecord)
}
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.tblItems.reloadData()
self.tblItems.hidden = false
})
}
}
}
I tried adding a reload function to viewWillAppear, but that didn't really help:
override func viewWillAppear(animated: Bool) {
tblItems.reloadData()
}
Now here's the code in DetailViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "activeAgain", name: UIApplicationWillEnterForegroundNotification, object: nil)
self.imageScrollView.contentSize = CGSizeMake(1000, 1000)
self.imageScrollView.frame = CGRectMake(0, 0, 1000, 1000)
self.imageScrollView.minimumZoomScale = 1.0
self.pinBoardScrollView.maximumZoomScale = 8.0
showImage()
and then the showImage function:
func showImage() {
imageView.hidden = true
btnRemoveImage.hidden = true
viewWait.hidden = true
if let editedImage = editedImageRecord {
if let imageAsset: CKAsset = editedImage.valueForKey("image") as? CKAsset {
imageView.image = UIImage(contentsOfFile: imageAsset.fileURL.path!)
imageURL = imageAsset.fileURL
self.title = "Image"
imageView.hidden = false
btnRemoveImage.hidden = false
btnSelectPhoto.hidden = true
}
}
}
and the activeAgain function:
func activeAgain() {
showImage()
println("Active again")
}
Neither of these ViewControllers reload the data from CloudKit when they return. It's like it's cached the table data for the first ViewController and the image for the DetailViewController and it's using that instead of downloading the actual CloudKit data. I'm stumped, so I figure I better try this forum (my first post!). Hopefully I included enough required info to be useful.
Your showImage function is using a editedImage CKRecord. Did you also reload that record? Otherwise the Image field in that record would then still contain the old CKAsset.
Besides just reloading on a moment you feel right, you could also let the changes be pushed by creating a CloudKit subscription. Then your data will even change when it's active and changed by someone else.