NSUserDefaults messing with SegmentedControl - ios

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.

Related

Swift using delegate tableview to refresh content from another class

I need to update the content of a TableView from another class that execute an async call to an external API.
I implemented a protocol to refresh the table after the API call but seem that this code is never executed.
I can't understand what is wrong with my code.. probably I forgot something, this is my first application and I just start studying protocols implementation
Below I wrote an example of my code where I remove all unnecessary code (for this question).
Protocol definition:
protocol ProductUpdateDelegate: class {
func refreshTableView()
}
Tabelview Implementation:
class TestTableViewController: UITableViewController,
UITextFieldDelegate, ProductUpdateDelegate {
var productList = [productInfo]()
// Delegate function implementation
func refreshTableView() {
self.tableView.reloadData()
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.rowHeight = 50;
self.tableView.dataSource = self
self.tableView.delegate = self
more code ...
}
#IBAction private func buttonTappedAction(_ sender: Any) {
code that get input from user and did some check...
if(isValidCode){
self.storeProduct(productCode: code)
}
}
func storeProduct(productCode: String){
if(isNewCode){
self.productList.append(productInfo(code: productCode,
quantity: 1))
}
more code ...
}
Class that need to update some information on tableView
import Foundation
class Product {
weak var delegate: ProductUpdateDelegate?
some code here...
func getProductInfoFromAPI(){
API CAll...
if(result != "no_data")
self.delegate?.refreshTableView()
}
more code ...
}
class used to store informations about products and used to update products description using remote API
import Foundation
class productInfo: NSObject, NSCoding, Codable {
var code = ""
var quantity = 1
public var product_name = "Wait for information..."
init(code: String, quantity : Int,
product_name : String = "Wait for information...") {
self.code = code
self.quantity = quantity
self.product_name = product_name
super.init()
self.getProductInfoFromAPI()
}
required convenience init(coder aDecoder: NSCoder) {
let code = aDecoder.decodeObject(forKey: "code") as! String
let quantity = aDecoder.decodeInteger(forKey: "quantity")
self.init(code:code, quantity:quantity)
self.getProductInfoFromAPI()
}
func getProductInfoFromAPI(){
let productInfo = Product()
productInfo.getInfoByCode(productCode: code, refObject: self)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.code, forKey: "code")
aCoder.encode(self.quantity, forKey: "quantity")
}
}
UPDATE1: The product class is called from a some other classes (4 in total) that I not included here for sake of simplicity
The application workflow is:
Application start
show the TestTableViewController
Then there are some different options for the user to update the TableView content each option is controlled by a separate class, so the product class is called from some other classes and is the only one that interact directly with the TestTableViewController
UPDATE2:
Add more code to show the complete process.
After view initialization, the user can click on a button and can insert a product code.
The inserted code is saved in a array of productInfo using append. This action activate the init method of the productInfo class and a call to a remote service start to retrieve information about the product itself.
The problem is that the article information are update correctly but are not show on the tableview because the view is not updated.
Because is an async process, i need a way to update the view content when the retrieving process has been completed.
So i think that the best solution was the protocols. But as already stated, the function refreshTableView seem ignored.
var refreshControl = UIRefreshControl()
#IBOutlet weak var tblView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
refreshControl.attributedTitle = NSAttributedString(string: "")
refreshControl.addTarget(self, action: #selector(refresh(sender:)), for: UIControl.Event.valueChanged)
tblView.addSubview(refreshControl)
}
#objc func refresh(sender:AnyObject) {
refreshControl.endRefreshing()
apiFunction()
}
func apiFunction(){
// call your api
}
Based in your code, you're not setting any value to your weak variable named delegate in Product class. You should set it to the class who should be using it.
One way is to make your Product class as singleton in a way that it will only initialize once.
e.g.
class Product {
static let shared = Product()
weak var delegate: ProductUpdateDelegate?
private init() { }
func getProductInfoFromAPI(){
if(result != "no_data")
self.delegate?.refreshTableView()
}
}
And in you tableview controller instead of let productInfo = Product() you can use let productInfo = Product.shared then set your delegate.
productInfo.delegate = self
That's it!

Today Widget unable to load with a specific line of code

I've added a Today extension to my app and it all works fine until a specific line of code is compiled. NB: compiled, not executed!
My TodayViewController is:
class StoredDoses {
func getDoses(doses: inout [Dose]) {
if let userD = UserDefaults(suiteName: "com.btv.mySuite") {
if let dosesData = userD.object(forKey: "doses_key") {
do {
// -----------------------------------------------
// Comment the line below out and the widget works
doses = try PropertyListDecoder().decode([Dose].self, from: dosesData as! Data)
// -----------------------------------------------
} catch {
print ("ERROR")
}
}
}
}
}
class TodayViewController: UIViewController, NCWidgetProviding {
#IBOutlet weak var aText: UILabel!
#IBOutlet weak var bText: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view from its nib.
}
func widgetPerformUpdate(completionHandler: (#escaping (NCUpdateResult) -> Void)) {
// Perform any setup necessary in order to update the view.
// If an error is encountered, use NCUpdateResult.Failed
// If there's no update required, use NCUpdateResult.NoData
// If there's an update, use NCUpdateResult.NewData
//Just for development stage - not real, final code
let form = DateFormatter()
form.timeStyle = .short
aText.text = form.string(from: Date())
completionHandler(NCUpdateResult.newData)
}
}
So, the above code isn't well written, but it's what I've used to finally narrow down the cause of the unloading widget. The array of Doses is a custom, codable class, but if I try to get an array of String then it's the same. The StoredDoses code is included in the main app and doesn't cause any problems.
Just to re-iterate: I'm not trying to execute any method in the StoredDoses class. I don't even have an instance of it in the widget. When the doses = ... line is merely commented out then the widget loads and the aText label in the widget appears with the current time in it.
Ok, so thanks to #Chris' apparently unconnected advise I got it sorted!
It appears to have been an Interface Builder issue: somehow it had retained the original name of the UILabel that was auto-created when I added the Today extension in Xcode. At some point, after connecting an IBOutlet to the label with "Hello World" in it, I'd renamed it to something slightly more relevant but hadn't unconnected it before over-typing the new name in the TodayViewController.
The console didn't throw up any problems and at times seemed to work, but when the line with
try PropertyListDecoder().decode([Dose].self, from: dosesData as! Data)
was present then it stopped working without any console messages.
I only found that out after I explored #Chris comment about the as! Data. I re-wrote to first get the Data:
if let userD = UserDefaults(suiteName: "com.btv.mySuite") {
if let dosesData = userD.object(forKey: "doses_key") {
if let unwrappedData = dosesData as? Data {
do {
doses = try PropertyListDecoder().decode([SplitDose].self, from: unwrappedData)
} catch {
doses.removeAll()
}
}
}
}
Once this was compiled (remember, it's still not being executed - this is just sitting there waiting to be used) the console threw up a message and the app crashed out showing the old UILabel name as not key-compliant. Reconnecting the UILabel in IB fixed everything and I could compile the original code....
This probably deserves a Radar entry but right now I don't want to waste another day re-creating (if at all possible) this problem!

Update tableView row from AppDelegate Swift 4

[![enter image description here][1]][1]
Hello. I have a tableview like in the picture above and I'm receiving some silent push notifications. Depending on them I need to reload a specific cell from the tableView. Since I'm getting the notification in the AppDelegate and there at the moment I'm reloading the whole tableView...but personally I don't find this the best solution since I only need to update a specific row.
Any hints please how can I update just a specific cell from appDelegate?
if userInfo["notification_type"] as? String == "update_conversation" {
if let rootVC = (self.window?.rootViewController as? UINavigationController)?.visibleViewController {
if rootVC is VoiceViewController {
let chatRoom = rootVC as! VoiceViewController
chatRoom.getConversations()
// the get Conversations method makes a call to api to get some data then I reload the whole tableView
}
}
func getConversations() {
let reachabilityManager = NetworkReachabilityManager()
if (reachabilityManager?.isReachable)! {
ServerConnection.getAllConversation { (data) in
if let _ = data{
self.conversations = data
self.onlineRecent = self.conversations
GlobalMainQueue.async {
self.mainTableView.reloadData()
}
}
}
}
}
This is my getConversation method which is used in VoiceViewController to populate my tableview
Have the app delegate broadcast an app-specific notification center notification (on the main thread). Have the view controller that contains your table view listen for that notification and update the cell in question as needed. That way you don't contaminate your app delegate. The app delegate should only deal with system level app stuff, not business logic.
You could get your row’s cell using self.mainTableView.cellForRow(at:IndexPath(…), and update it directly.
Or, I’ve found you save a load of time and your view controllers end up more reliable using ALTableViewHelper [commercial - available on Framework Central here]. It’s free to try.
The helper does the most of the work - you describe how the data connects to the UITableView. I’ve put together an example (on GitHub here), which I hope is something like what you’re trying to do.
import ALTableViewHelper
class VoiceViewController {
// #objc for the helper to see the var’s, and dynamic so it sees any changes to them
#obj dynamic var conversations: Any?
#obj dynamic var onlineRequest: Any?
func viewDidLoad() {
super.viewDidLoad()
tableView.setHelperString(“””
section
headertext "Conversation Status"
body
Conversation
$.viewWithTag(1).text <~ conversations[conversations.count-1]["title"]
$.viewWithTag(2).text <~ "At \\(dateFormat.stringFromDate(conversations[conversations.count-1]["update"]))"
UpdateButton
$.viewWithTag(1).isAnimating <~ FakeConversationGetter.singleton.busy
“””, context:self)
}
func getConversations() {
let reachabilityManager = NetworkReachabilityManager()
if (reachabilityManager?.isReachable)! {
ServerConnection.getAllConversation { (data) in
if let _ = data {
// change the data on the main thread as this causes the UI changes
GlobalMainQueue.async {
self.conversations = data
self.onlineRequest = self.conversations
}
}
}
}
}

Updating label if value in singleton changes

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.

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