This may seem like a basic question but I don't know the answer. In my app, you earn coins after every play of the game. Here is my code for earning coins and saving them
let defaultCoins = UserDefaults()
var coinNumber = defaultCoins.integer(forKey: "coinSaved")
coinNumber = coinNumber + score
defaultCoins.set(coinNumber, forKey: "coinSaved")
I would like to be able to access coinNumber from a different view controller and be able to use it. So maybe make a variable = coinNumber that I can access anywhere. I have a shop and would like to be able to purchase a new character when the user has 1000 coins but currently cant do that as I don't know how to access coinNumber from my shopViewController
Since you're saving the data to UserDefaults it doesn't seem like you should need to propagate it between view controllers - just use the defaults as your 'source of truth' for the correct value. Instead of saving it to a custom UserDefaults object, however, just save it to the standard one that is created for all iOS/OS X apps: UserDefaults.standard
Just read it from it whenever you need it the way you're already doing above. A couple things to note about this approach:
You can use Key/Value observing to detect changes to the value when other controllers modify it, but you'll need to be careful as KVO can be easy to crash if you forget to remove observers properly.
You should probably call synchronize on it which forces the changes you've made to disk. This will make sure that if your app is closed suddenly that you've secured the data.
UserDefaults is great for when you have less than 10 pieces of data you're wanting to save, like a highest score or a set of preferences (that's what it's made to do), but it's not great for large data structures. You should look into CoreData or KeyedArchiver for bigger stuff.
Pro-tip: Rather than using the loose string "coinSaved" over and over again, define a static constant somewhere, or use an enumeration of a string type to pre-define all your values once. Then you get code completion and avoid errors.
So, your code above would look more like:
In one view controller:
let defaultCoins = UserDefaults.standard
var coinNumber = defaultCoins.integer(forKey: "coinSaved")
coinNumber = coinNumber + score
defaultCoins.set(coinNumber, forKey: "coinSaved")
defaultCoins.synchronize()
Then, in another controller where you need to read it:
let defaultCoins = UserDefaults.standard
let coinNumber = defaultCoins.integer(forKey: "coinSaved")
Your main problem is that you are creating a new UserDefaults object whenever you try to access it, you should either use the UserDefaults.standard instance or create one with a specific identifier.
I would also suggest creating a singleton to manage the coins if you are going to use them in several view controllers, this is to save your time writing the same safety checks over and over (like making sure you can't spend more coins than you have)
It would look something like this:
class CoinManager {
static let shared = CoinManager()
private var _coins: UInt {
didSet {
UserDefaults.standard.set(_coins, forKey: "coinSaved")
}
}
var coins: UInt { return _coins }
private init() {
_coins = UInt(UserDefaults.standard.integer(forKey: "coinSaved"))
}
func spend(coins: UInt) -> Bool {
guard _coins > coins else { return false }
_coins -= coins
return true
}
func store(coins: UInt) -> UInt {
_coins += coins
return _coins
}
}
As you can see, other objects can only read the coins property but they can't modify it directly, if they want to modify it they have to use either spend or store. spend makes sure you can't spend more coins than you have.
Related
Im making a Fitness app to learn Core data, and I have found that I need to let the user store every performed workout as a WorkoutLog item, and in there, there should be a series of ExerciseLogs which represents performances of that exercise (it contains each lift and also a reference to the actual exercise design).
Problem is that after a while i realize that i need to have these ordered, so that the next time i want to show the user their workout, the order that the exercisese were performed should be the same.
So I checked "ordered" in the top right of the image there, and now my code is in dire need of an update. I have tried to read as much as I could about working with NSOrderedSet and how to fetch them from core data and then manipulate them, but I havent really found much of use to me. (I have no experice in objective-c)
For example my code that used to be:
static func deleteWorkoutLog(_ workoutLogToDelete: WorkoutLog) {
guard let exerciseLogsToDelete = workoutLogToDelete.loggedExercises as? Set<ExerciseLog> else {
print("error unwrapping logged exercises in deleteWorkoutLog")
return
}
I get the error: .../DatabaseFacade.swift:84:77: Cast from 'NSOrderedSet?' to unrelated type 'Set' always fails
So what ive learned about sets and core data no longer seems applicable.
Im far from an expert in programming, but im very eager to learn how to get access to the loggedExercises instances.
TLDR; Is there a way to cast NSOrderedSet to something I can work with? How do we usually work with NSManagedSets from core data? Do we cast them to Arrays or MutableSets? I would very much appreciate an example or two on how to get started with retrieving and using these ordered sets!
Thanks
For anyone else wondering how to get started with orderedSets in core data:
After setting my the WorkoutLog.loggedExercises "to-many" relationship to be ordered, I managed to access them through the mutableOrderedSetValue function like this:
static func deleteWorkoutLog(_ workoutLogToDelete: WorkoutLog) {
let orderedExerciseLogs: NSMutableOrderedSet = workoutLogToDelete.mutableOrderedSetValue(forKey: "loggedExercises")
let exerciseLogsToDelete = orderedExerciseLogs.array
for exerciseLog in exerciseLogsToDelete {
guard let exerciseLog = exerciseLog as? ExerciseLog else {
return
}
Works great so far.
And to rearrange the NSOrderedSet I ended up doing something like this:
// Swap the order of the orderedSet
if let orderedExerciseLogs: NSOrderedSet = dataSourceWorkoutLog.loggedExercises {
var exerciseLogsAsArray = orderedExerciseLogs.array as! [ExerciseLog]
let temp = exerciseLogsAsArray[indexA]
exerciseLogsAsArray[indexA] = exerciseLogsAsArray[indexB]
exerciseLogsAsArray[indexB] = temp
let exerciseLogsAsOrderedeSet = NSOrderedSet(array: exerciseLogsAsArray)
dataSourceWorkoutLog.loggedExercises = exerciseLogsAsOrderedeSet
}
This is my code for setting the defaults.
override func viewDidLoad() {
super.viewDidLoad()
let defaults = UserDefaults.standard
defaults.set("\(coins) $", forKey: "labelName") }
coins is my integer variable. It increases every time someone clicks a button.
labelName is my label that shows how many coins are earned.
How to make it so the number of coins are saved locally and then updated when someone restarts the app?
I wouldn't save coins as a string which is what you're doing here:
defaults.set("\(coins) $", forKey: "labelName")
Instead save it as an Integer:
// Set data (whenever you change the value)
var coins = 100
UserDefaults.standard.set(coins, forKey: "Money")
When you want to get the data back (in viewDidLoad perhaps):
// Get Data
coins = UserDefaults.standard.integer(forKey: "Money")
I think you can save coins as an integer instead of a string, but then if you are using it as a string then you would have that change it back to a string after you accessed the data when you want to use it. If that makes sense.
I have an array of data that doesn't need to be a managed List, meaning I don't need Realm to create a new model for the items with links and the ability to query on the items, etc. I just want a simple array, typically of primitives that don't inherit from Object anyway, that will be persisted with my main object.
The only solution I can think of is to use NSData and NSKeyedArchiver/NSKeyedUnarchiver. Is that the best/only way to do this? Should I just use List even if I don't think I'll need it — what's the best practice for this situation?
Realm doesn't support arrays of primitives (although that functionality is coming soon), so the most straightforward solution is to use a List filled with model objects that just wrap your primitives. There's nothing wrong with archiving to and from NSData and storing the data in your Realm model, though, if you feel that better suits your particular use case.
Here's how I decided to deal with this:
var instructions: [String] {
get {
if _instructions == nil {
_instructions = NSKeyedUnarchiver.unarchiveObject(with: instructionsData) as? [String]
}
return _instructions!
}
set {
instructionsData = NSKeyedArchiver.archivedData(withRootObject: newValue)
_instructions = newValue
}
}
fileprivate var _instructions: [String]? = nil
dynamic var instructionsData = Data()
override static func ignoredProperties() -> [String] {
return ["instructions", "_instructions"]
}
That way I can use the array as I normally would and still have it persisted in a simple way (without having to create an actual List and having to manage a bunch of new models/objects).
I have multiple scenes that are stored in Global/BaseScene
Each SceneType: is stored as enum: set to an Integer similar to this present scene
First objective was to get the score to populate in each scene, there is seven of them. DONE thanks to stack overflow, and experimentation
I had to create default keys for each individual scene, to calculate the highScore, so I have seven unique "highScore" "keys" for each scene.
In the GameScene:
var highScoreA1: Int = 0
var score: Int = 0 {
didSet {
a1Score.text = "Score: \(score)"
}
}
//above called before override func didMoveToView(view: SKView) {
//Called in GameOver Method
let scoreDefault = NSUserDefaults.standardUserDefaults()
scoreDefault.setInteger(score, forKey: "score")
if (score > highScoreA1){
let highScoreA1Default = NSUserDefaults.standardUserDefaults()
highScoreA1Default.setInteger(score, forKey: "highScoreA1")
//highscoreA1Default.synchronize()
There are six more keys similar to this.. My objective is to populate a "totalScoreKey" in two different scenes a Hud Scene and another scene (possibly game over scene)
I was thinking a function to add these keys together to populate the total score.
Taking into consideration all these scenes are subclasses (of Global BaseScene, and each scene has sub classes (for the nodes operation, probably not relevant yet thought it might be useful)
I have tried: Moving all score data into a Class and using NSCoding/NSObject the required init, and optional binding, became a serious pain, and honestly I am trying to keep things simple for version one.
import Foundation
class GameState {
var score:Int = 0
var highScore:Int()
var totalScore:Int()
class var sharedInstance: GameState {
struct Singleton {
static let instance = GameState()
}
return Singleton.instance
}
}
init() {
// Init
score.type = 0
highScore = 0
totalScore = 0
// Load game state
let defaults = NSUserDefaults.standardUserDefaults()
highScore = defaults.integerForKey("highScore")
}
func saveState() {
// Update highScore if the current score is greater
highScore = max(score, highScore)
// Store in user defaults
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setInteger(highScore, forKey: "highScore")
defaults.setInteger(totalScore, forKey: "totalScore")
NSUserDefaults.standardUserDefaults().synchronize()
}
Various functions that have not worked they all default to zero, that is until I figure out how to retrieve data properly.
let total score = "totalScoreKey"
similar to this post exactly like this post actually except I had to do different configurations because of my on personal set up..
total Score example I tried to implement
outside of class and referring to that in the scene I needed to populate that data. NO Go defaults to zero.
How do I simply add the value of those keys together? For which I can display similar to the other scenes, I already have implemented.
later on down the road I may want to assign a key chain value, right now I am just trying to get it show up for posting in GameCenter. (which also has key "GameCenterHighScore")
Setting them all to the same key "highScore" does not work.... just to be clear, I tried multiple times. Thanks in advance.
EDIT
if I try to add all the defaults together to get the total, it throws the following error:
Expression was too complex to be solved in reasonable time; consider
breaking up the expression into distinct sub-expressions
[Swift compound arithmetic operation ERROR3
Trying my hand in Swift and creating my first "app". I'm essentially creating a cookie-clicker clone where you just click an image in the center of your screen repeatedly to increment your score.
I'm experiencing some issues where clicking the image once will increment my score by 1, but upon clicking it again, the score stays 1.
I'm almost positive I'm supposed to do something after saving to CoreData - I just don't know what. Do I have to refresh the View? Sorry for the newbie question
#IBAction func tapHomeImage(sender: AnyObject) {
//Score is the name of my Entity, with "points" being an attribute
let entity = NSEntityDescription.entityForName("Score", inManagedObjectContext: managedObjectContext!)
let score = Score(entity: entity!, insertIntoManagedObjectContext: managedObjectContext!)
// **As an aside question, is there a more efficient way to code the below? The issue I have is that score.points is of type NSNumber
let intScore = score.points as Int
let incrementedIntScore = intScore + 1
score.points = NSNumber(integer: incrementedIntScore)
var error: NSError?
managedObjectContext?.save(&error)
//homeScoreLabel is an IBOutlet for the Score displayed on the page
homeScoreLabel?.text = "\(score.points)"
}
Thank you very much!
You don't need to do anything after saving, you need to do something before calling this method. You need to find an instance of Score that already exists, and use that.
When you get to this line:
let score = Score(entity: entity!, insertIntoManagedObjectContext: managedObjectContext!)
You create a new instance of the Score entity, every time this method runs. So when you do this:
let intScore = score.points as Int
let incrementedIntScore = intScore + 1
score.points = NSNumber(integer: incrementedIntScore)
You're working with a brand new instance every time. The score is initially zero, and you always increase it to 1.
What you should do:
Use a single instance of Score and save that as a property of your view controller. That is, have something like this:
var score: Score?
Assign a value to this property once, when the view controller loads. Since you're saving data, you should use executeFetchRequest to look up a previously-saved instance. If no previous instance exists, then create a new one via insertIntoManagedObjectContext.
Use that instance in this IBAction.
There are other ways to fix the code. The key is to make sure that you use the same instance of Score every time instead of creating a new one at every tap.