Updating label if value in singleton changes - ios

I am getting some user information from Firebase and storing it into singleton. After that every time the value changes I want that the label changes also but it doesn't until I terminate the app and come back in.
How should I update label if value changes in singleton?
I have tab views. In first tab I assign values and in second tab I try to put the values to label.
This is my singleton:
class CurrentUser: NSObject
{
var generalDetails: User = User()/// Consecutive declarations on a line must be separated by ';'
static let sharedInstance = CurrentUser()
fileprivate override init(){
super.init()
}
}
And like this I assign values:
self.databaseRef.child("users").child(user.uid).observeSingleEvent(of: .value) { (snapshot:FIRDataSnapshot) in
guard let firebaseValue = snapshot.value as? [String:AnyObject], let userName = firebaseValue["username"] as? String, let email = firebaseValue["email"] as? String, let reputation = firebaseValue["reputation"] as? Int, let profilePicURL = firebaseValue["profileImageUrl"] as? String
else
{
print("Error with FrB snapshot")//Here
return
}
//Set values
self.currentUser.generalDetails = User(userName: userName, email: email, reputation: reputation, profileImageURL: profilePicURL, uid: user.uid)
}
And if I want to put the value to the label I simply do this(This reputation is the only thing that can change often):
self.reputationLabel.text = String(self.currentUser.generalDetails.reputation)

You can do either of these:-
Communicate between the singleton and your class with delegate-protocol method , fire the delegate method in the class whenever your repo changes and update your label.
Open a different thread in your network link for the user's reputation in the viewController itself:-
override func viewDidLoad() {
super.viewDidLoad()
FIRDatabase.database().reference().child("users").child(FIRAuth.auth()!.currentUser!.uid).child("reputation").observe(.childChanged, with: {(Snapshot) in
print(Snapshot.value!)
//Update yor label
})
which will get called every time the value of reputation changes.

I like Dravidian's answer and I would like to offer an alternative: KVO
We use Key-Value Observing to monitor if our app is disconnected from the Firebase server. Here's the overview.
We have a singleton which stores a boolean variable isConnected, and that variable is set by observing a special node in Firebase
var isConnected = rootRef.child(".info/connected")
When connected/disconnected, the isConnected var changes state.
We have a little icon on our views that indicates to the user the connected state; when connected it's green, when disconnected it's red with a line through it.
That icon is a class and within each class we have code that observes the isConnected variable; when it's state changes all of the icons change automatically.
It takes very little code, is reusable, is clean and easily maintained.
Here's a code snippet from the Apple documentation
//define a class that you want to observe
class MyObjectToObserve: NSObject {
dynamic var myDate = NSDate()
func updateDate() {
myDate = NSDate()
}
}
//Create a global context variable.
private var myContext = 0
//create a class that observes the myDate var
// and will be notified when that var changes
class MyObserver: NSObject {
var objectToObserve = MyObjectToObserve()
objectToObserve.addObserver(self,
forKeyPath: "myDate",
options: .new,
context: &myContext)
There will be more to it but that's it at a 10k foot level.
The Apple documentation is here
Using Swift with Cocoa and Obj-c 3.01: Design Patterns
and scroll down the the Key-Value Observing Section. It's a good read and very powerful. It follows the same design pattern as Firebase (or vice-versa) - observe a node (variable) and tell me when it changes.

Related

Back button and creating new entry causes app to crash (UUID cannot be amended) Realm Swift

When I go back to VC1 (which allows the user to input a title for a book and create an entry in realm including the title and a UUID for that book) from VC2 (using the provided back button as part of the navigation controller not a custom segue) and then create a new book object in Realm (by adding another title to the text field in VC1), the app crashes saying I cannot amend the primary key once set.
I am intending to create a new entry (in theory I could add one, go back, add another etc) rather than try to overwrite an existing entry.
I've read the docs (and even looked at an old project where a similar thing is working) but I can't understand why it isn't just creating a new entry. I looked at the Realm docs (e.g. referenced in this answer Realm in IOS: Primary key can't be changed after an object is inserted)
Code here is VC1 allowing the user to create a new novel (by adding a title into a text field which is earlier in the code)
func createNewNovel() {
let realm = try! Realm()
novelCreated.novelID = UUID().uuidString
novelCreated.novelTitle = novelTitleInput.text!
novelCreated.createdDate = Date()
do {
try realm.write {
realm.add(novelCreated)
print ("Novel Created Successfully")
}
} catch {
print("error adding novel")
}
Then I prepare to pass the novelID to VC2 :
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let novelData = NovelObject()
novelData.novelID = novelCreated.novelID
if let destVC = segue.destination as? WriteVC {
destVC.novelIDPassed = novelData.novelID
}
}
This works fine and the segue triggers from a button press on VC1. I can also print the ID of the novel in VC2 so that's working fine.
BUT... when I go back from VC2 and input a new title in the text field in VC1 it should create a new NovelObject, not try to update the existing one (which is causing it to crash). You could even theoretically have the same book title twice but they should have a different UUID (which is the primary key)
I must be missing something trivial I suppose but can't work it out!
The novel is created as at the top :
class NewNovelVC: UIViewController {
let novelCreated = NovelObject()
#IBOutlet weak var novelTitleInput: UITextField!
#IBOutlet weak var createButtonOutlet: UIButton!
then it is populated with variables
This is due to classic issue of same object being in the memory re-instantiated which points to the same value of primary key. Creating a singleton class can be very handy here as well.
Create a service file. This will keep your RealSwift initialized on a specific thread as your current block.
RealService.swift
import RealmSwift
class RealmService {
static let uirealm = RealmService()
private var _initRS = try! Realm()
var realm: Realm! {
return _initRS
}
}
Novel.swift :
import RealmSwift
class Novel : Object {
#objc dynamic var uid : String? = nil
#objc dynamic var title: String? = nil
override static func primaryKey() -> String {
return "uid"
}
}
extension Novel {
func writeToRealm(){
try? RealmService.uirealm.realm.write {
print("Creating the Novel Object")
RealmService.uirealm.realm.add(self, update: true)
}
}
func DeleteFromRealm(object: Results<Novel>){
try? RealmService.uirealm.realm.write {
print("Deleting the Novel Object")
RealmService.uirealm.realm.delete(object)
}
}
}
and just implement as #Don mentioned, write in the block where you are setting the title.
var novel: Novel! // Declare this in above in the class.
novel = Novel()
novel.title = title
novel.uid = uid
novel.writeToRealm()
Hope that helped.

Firebase, how observe works?

I honestly I have tried to figure out when to call ref.removeAllObservers or ref.removeObservers, but I'm confused. I feed I'm doing something wrong here.
var noMoreDuplicators = [String]()
func pull () {
if let myIdi = FIRAuth.auth()?.currentUser?.uid {
let ref = FIRDatabase.database().reference()
ref.child("users").queryOrderedByKey().observe(.value, with: { snapshot in
if let userers = snapshot.value as? [String : AnyObject] {
for (_, velt) in userers {
let newUser = usera()
if let thierId = velt["uid"] as? String {
if thierId != myIdi {
if let userName = velt["Username"] as? String, let name = velt["Full Name"] as? String, let userIdent = velt["uid"] as? String {
newUser.name = name
newUser.username = userName
newUser.uid = userIdent
if self.noMoreDuplicators.contains(userIdent) {
print("user already added")
} else {
self.users.append(newUser)
self.noMoreDuplicators.append(userIdent)
}
}
}
}
}
self.tableViewSearchUser.reloadData()
}
})
ref.removeAllObservers()
}
}
Am I only supposed to call removeAllObservers when observing a single event, or...? And when should I call it, if call it at all?
From official documentation for observe(_:with:) :
This method is used to listen for data changes at a particular location. This is
the primary way to read data from the Firebase Database. Your block
will be triggered for the initial data and again whenever the data
changes.
Now since this method will be triggered everytime the data changes, so it depends on your usecase , if you want to observe the changes in the database as well, if not then again from the official documentation:
Use removeObserver(withHandle:) to stop receiving updates.
Now if you only want to observe the database once use observeSingleEvent(of:with:) , again from official documentation:
This is equivalent to observe:with:, except the block is
immediately canceled after the initial data is returned
Means that you wont need to call removeObserver(withHandle:) for this as it will be immediately canceled after the initial data is returned.
Now lastly , if you want to remove all observers , you can use this removeAllObserver but note that:
This method removes all observers at the current reference, but does
not remove any observers at child references. removeAllObservers must
be called again for each child reference where a listener was
established to remove the observers
Actually, you don't need to call removeAllObservers when you're observing a single event, because this observer get only called once and then immediately removed.
If you're using observe(.value) or observe(.childAdded), and others though, you would definitely need to remove all your observers before leaving the view to preserve your battery life and memory usage.
You would do that inside the viewDidDisappear or viewWillDisappear method, like so:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove your observers here:
yourRef.removeAllObservers()
}
Note: you could also use removeObserver(withHandle:) method.

Can a signal observer get access to the last emitted value of a ReactiveCocoa signal?

I'm starting using ReactiveCocoa and I'm still struggling with some basic concepts:
My app starts listening for geolocation data (init in my view model)
My app emits a signal with my current location (didFindCurrentPosition is called)
My view controller showing a map loads (viewDidLoad in my view controller)
My view controller starts observing the current location signal (still viewDidLoad)
My problem is: after step 2 is achieved, if no other event is sent on the signal, my view controller doesn't get notified.
How can my view controller get access to the last value from the signal? (ie how to get access at step 3 to a value emitted at step 2?)
Thanks for your help.
PS: ReactiveCocoa looks like a great library but I'm puzzled by the state of the documentation. IMHO, it is not very clear and lacks some clear guides on how to use it.
The Code
The view model:
class MyViewModel: LocationManagerDelegate {
let locationManager: LocationManager
let geolocationDataProperty = MutableProperty<Geolocation?>(nil)
let geolocationData: Signal<Geolocation?, NoError>
init() {
geolocationData = geolocationDataProperty.signal
// Location Management
locationManager = LocationManager()
locationManager.delegate = self
locationManager.requestLocation()
}
// MARK: - LocationManagerDelegate
func didFindCurrentPosition(location: CLLocation) {
geolocationDataProperty.value = Geolocation(location: location)
}
}
The view controller:
class MyViewController: UIViewController {
let viewModel = MyViewModel()
init() {
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel.geolocationData
.observe(on: UIScheduler())
.observeValues { geolocation in
debugPrint("GOT GEOLOCATION")
}
}
}
You already have a Property that holds the latest value emitted. Instead of using the property's signal use the producer. That way when you start the producer you will get the current value first (in none was sent you will get nil).
Cf. the documentation:
The current value of a property can be obtained from the value getter. The producer getter returns a signal producer that will send the property’s current value, followed by all changes over time. The signal getter returns a signal that will send all changes over time, but not the initial value.
So, regarding the code in the question, the viewDidLoad method should do something like the following:
viewModel.geolocationDataProperty
.producer
.start(on: UIScheduler())
.startWithValues { geolocation in
debugPrint("GOT GEOLOCATION")
}
Bindings from any kind of streams of values can be crated using the <~ operator but you can start to modify your code in the following way and see it is working fine, it is easier to debug :-)
class MyViewModel: LocationManagerDelegate {
let locationManager: LocationManager
let geolocationDataProperty = MutableProperty<Geolocation?>(nil)
init() {
geolocationDataProperty.signal.observeValues { value in
//use the value for updating the UI
}
// Location Management
locationManager = LocationManager()
locationManager.delegate = self
locationManager.requestLocation()
}
// MARK: - LocationManagerDelegate
func didFindCurrentPosition(location: CLLocation) {
geolocationDataProperty.value = Geolocation(location: location)
}
}
then we can try to use the binary operator for implementing the MVVM pattern, since having direct reference to the UI element inside the modelview is definitely a bad idea :-)
Take a look at this article, it is really interesting.
I am not sure if your map component is directly supporting the reactive framework.
see it has the ".reactive" extension and if it can used for updating the current position property
if it is present then the <~ operator can be used writing something similar to
map.reactive.position <~ viewModel.geolocationDataProperty
if it does not have the reactive extension then you can simply move this code in your viewcontroller
viewModel.geolocationDataProperty.signal.observeValues { value in
//use the value for updating the UI
}
If your problem is to have old value when it gets modified .You can try with Key-Value Observing . Add your property for value observation like
geolocationDataProperty.addObserver(self, forKeyPath: "value", options: [.new, .old], context: &observerContext)
Whenever your geolocationDataProperty gets modified ,you will receive that value in delegate method
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
let newValue = change?[.newKey] as? NSObject
let oldValue = change?[.oldKey] as? NSObject
//Add your code
}

NSUserDefaults messing with SegmentedControl

I have a simple expense tracker app that has at the top a Segmented control to change currency (Romanian currency, Euro and $ in this order). In my list manager I have methods that convert from one to another and in the view controller, based on the Segmented control, I call them accordingly.
My problem is the following: When the app starts, the selected currency is always the first - Ro currency. If I add some elements and then I quit the app with another currency selected, I guess NSUserDefaults synchronizes with those values. When I open the app again, Ro currency is again selected, but with the $/euro values. (Say I add 410 ron, which converted is ~$100. I select $ and kill the app from multitasking. When I open it up again, it show 100 ron, instead of 410). If a currency is selected and add a certain amount, it performs ok (if $ is selected and I add 100, when I switch the control to ron it displays 410). I guess I have to change something with the synchronization, but I can't figure out where and how.
EDIT2: Some code (sorry)
//This is the expenses class
import UIKit
class Expense: NSObject, NSCoding {
//MARK: Properties
var title:String?
var amount:Double?
init(title:String,amount:Double){
self.title = title
self.amount = amount
}
override init(){
super.init()
}
required init(coder aDecoder: NSCoder){
if let titleDecoded = aDecoder.decodeObjectForKey("title") as? String{
title = titleDecoded
}
if let amountDecoded = aDecoder.decodeObjectForKey("amount") as? Double{
amount = amountDecoded
}
}
func encodeWithCoder(aCoder: NSCoder) {
if let titleEncoded = title {
aCoder.encodeObject(titleEncoded, forKey:"title")
}
if let amountEncoded = amount {
aCoder.encodeObject(amountEncoded, forKey:"amount")
}
}
}
//The NSUserDefaultsManager
import UIKit
class NSUserDefaultsManager: NSObject {
static let userDefaults = NSUserDefaults.standardUserDefaults()
class func synchronize(){
let myData = NSKeyedArchiver.archivedDataWithRootObject(ExpenseManager.expenses)
NSUserDefaults.standardUserDefaults().setObject(myData, forKey: "expensearray")
NSUserDefaults.standardUserDefaults().synchronize()
}
class func initializeDefaults(){
if let expensesRaw = NSUserDefaults.standardUserDefaults().dataForKey("expensearray"){
if let expenses = NSKeyedUnarchiver.unarchiveObjectWithData(expensesRaw) as? [Expense]{
ExpenseManager.expenses = expenses
}
}
}
}
I call the initializeDefaults in the tableview manager in view did load, the synchronize in view did appear and in the App Delegate module, sync in applicationWillTerminate.
ANSWER
I found a solution - it was quite obvious. I found it in a Google Books - iOS 9 Programming Fundamentals by Matt Neuburg.
In the segmented control action I added the following
let c = self.currencySelector.selectedSegmentIndex
NSUserDefaults.standardUserDefaults().setObject(c, forKey: "selectedcurrency")
while in viewDidLoad I added
let c = NSUserDefaults.standardUserDefaults().objectForKey("selectedcurrency") as! Int
currencySelector.selectedSegmentIndex = c
sorry I don't have enough reputation to comment. But could you show the code, and Im guessing you're not synchronizing something or just not pulling it up in ViewDidLoad, etc.

SpriteKit: Why does it wait one round for the score to update? (Swift)

In one scene, I have this code:
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setInteger(score, forKey: "scoreKey")
defaults.synchronize()
When the user makes contact with the gap, the code runs:
score++
If the user hits an obstacle, the GameOverScene takes over. Here's the code I have for the GameOverScene to move the score to scene to scene:
let defaults = NSUserDefaults.standardUserDefaults()
let score = defaults.integerForKey("scoreKey")
scoreLabel.text = "\(score)"
However, there's a bug in my code where the scoreLabel doesn't update its text. For example, let's say a user scores 1 and dies. When he dies, the gameOverScene will come up and say that the score was 1. Then, lets say the user clicks restart, and scores 5 and then dies. In the GameOverScene, the scoreLabel will say 1.
Please help me!
You don't need to really call synchronise anymore if you use iOS 8 or above. This is recommended by Apple, yet a lot of people still do it. So get rid of that line if you still use it.
My preferred way for game data is using a singleton GameData class with NSCoding. No need to add variables all over the place and much cleaner. I advise you reading this.
http://www.raywenderlich.com/63235/how-to-save-your-game-data-tutorial-part-1-of-2
You can also integrate iCloud key value storage that way, it is very easy as its similar to user defaults (see my example below)
Anyway to start you off here is a simple example of how this could look.
import Foundation
/// Keys
private struct Key {
static let encodedData = "encodedData"
static let highScore = "highScore"
}
class GameData: NSObject, NSCoding {
// MARK: - Static Properties
/// Shared instance
static let shared: GameData = {
if let decodedData = UserDefaults.standard.object(forKey: Key.encodedData) as? GameData {
return gameData
} else {
print("No data, creating new")
return GameData()
}
}
// MARK: - Properties
/// Defaults
private let localDefaults = UserDefaults.standard
private let iCloudDefaults = NSUbiquitousKeyValueStore.default()
/// Progress (not saved, no need for saving the score because of the highScore var. Still have here for global single access)
var score = 0
/// Progress (saved)
var highScore = 0
// MARK: - Init
private override init() {
super.init()
print("GameData init")
NotificationCenter.default.addObserver(self, selector: #selector(updateFromCloud), name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: iCloudDefaults)
iCloudDefaults.synchronize()
}
// MARK: - Convenience Init
convenience required init?(coder decoder: NSCoder) {
self.init()
print("GameData convenience init")
// Progress
highScore = decoder.decodeInteger(forKey: Key.highScore)
}
// MARK: - Encode
func encodeWithCoder(encoder: NSCoder) {
// Progress
encoder.encodeInteger(highScore, forKey: Key.highScore)
// MARK: - User Methods
/// Save
func save() {
if score > highScore {
highScore = score
}
saveLocally()
saveToCloud()
}
// MARK: - Internal Methods
/// Save locally
private func saveLocally() {
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(self)
localDefaults.setObject(encodedData, forKey: Key.encodedData)
}
/// Save to icloud
private func saveToCloud() {
print("Saving to iCloud")
// Highscores
if (highScore > iCloudDefaults.objectForKey(Key.highScore) as? Int ?? Int()) {
iCloudDefaults.setObject(highScore, forKey: Key.highScore)
}
/// Update from icloud
func updateFromCloud() {
print("Updating from iCloud")
// Highscores
highScore = max(highScore, iCloudDefaults.object(forKey: Key.highScore) as? Int ?? Int())
// Save
saveLocally()
}
Now in any scene if you want to use the score or saved highScore property you for example could say
GameData.shared.score++
or
scoreLabel.text = "\(GameData.shared.score)"
highScoreLabel.text = "\(GameData.shared.highScore)"
All your text labels will be updated immediately if you transition to a new scene or update the .text property. No need for userDefault sync etc.
Calling ...shared... will also initialise the helper. If you want to load gameData as soon as your game has launched you could call
GameData.shared
in your appDelegate or viewController. Its probably not really needed but your could still do it just to ensure the helper is initialised as soon as the game is launched.
If you want to save you call
GameData.shared.save()
Just remember to reset the score back to 0 in your gameScene.swift in the ViewDidLoad method.
GameData.shared.score = 0
This should make your life much easier. If you want to use iCloud all you have to do is go to your target and capabilities and turn on iCloud and tick keyValueStorage (not core data). Done.
Note:
To take it even a step further you could get the KeychainWrapper helper from JRendel on gitHub. Than instead of using NSUserDefaults to store the encoded gameData you use keychain, its dead simple to use.
You can save your score like below code:
NSUserDefaults.standardUserDefaults().setInteger(score, forKey: "scoreKey")
Then you can get your score was saved like below code:
if NSUserDefaults.standardUserDefaults().objectForKey("scoreKey") != nil
{
score = NSUserDefaults.standardUserDefaults().objectForKey("scoreKey") as! Int
}
scoreLabel.text = "\(score)"
To answer the question from your comment about using global structure...
According to docs :
Global variables are variables that are defined outside of any
function, method, closure, or type context.
Means you should define your struct right after the import statements at the top of the any file.
You make a structure like pointed from the link I've posted in comments, like this (I've placed the struct definition inside the GameScene.swift file):
struct GlobalData
{
static var gold = 0;
static var coins = 0;
static var lives = 0;
static var score = 0;
}
This struct will be available in GameOverScene as well. So, before transition, you will do something like :
GlobalData.score = 20
GlobalData.coins = 10
//etc.
And in your GameOverScene you will access it like this:
scoreNode.text = String(GlobalData.score)//scoreNode is SKLabelNode

Resources