I am trying to save a high score using Swift for my SpriteKit game. There are several good examples on StackOverflow, one of which I got to work temporarily, but could not function properly in the Swift file where all of my nodes (and actual game) is located.
*Most of the following code is from a stack overflow answer.
This code I put in a separate file called "HighScore":
import Foundation
class HighScore: NSObject {
var highScore: Int = 0
func encodeWithCoder(aCoder: NSCoder!) {
aCoder.encodeInteger(highScore, forKey: "highScore")
}
init(coder aDecoder: NSCoder!) {
highScore = aDecoder.decodeIntegerForKey("highScore")
}
override init() {
}
}
class SaveHighScore:NSObject {
var documentDirectories:NSArray = []
var documentDirectory:String = ""
var path:String = ""
func ArchiveHighScore(#highScore: HighScore) {
documentDirectories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
documentDirectory = documentDirectories.objectAtIndex(0) as String
path = documentDirectory.stringByAppendingPathComponent("highScore.archive")
if NSKeyedArchiver.archiveRootObject(highScore, toFile: path) {
println("Success writing to file!")
} else {
println("Unable to write to file!")
}
}
func RetrieveHighScore() -> NSObject {
var dataToRetrieve = HighScore()
documentDirectories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
documentDirectory = documentDirectories.objectAtIndex(0) as String
path = documentDirectory.stringByAppendingPathComponent("highScore.archive")
if let dataToRetrieve2 = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? HighScore {
dataToRetrieve = dataToRetrieve2
}
return(dataToRetrieve)
}
}
In the scene where I actually want to get an input and output for the highscore I have:
var Score = HighScore()
override func viewDidLoad() {
super.viewDidLoad()
Score.highScore = 100
SaveHighScore().ArchiveHighScore(highScore: Score)
var retrievedHighScore = SaveHighScore().RetrieveHighScore() as HighScore
println(retrievedHighScore.highScore)
}
So when a particular node passes a "block" the high score should increment accordingly, and then save the number (as long as it is higher than the current highscore.)
func blockRunner() {
Score.highScore = 0
SaveHighScore().ArchiveHighScore(highScore: Score)
var retrievedHighScore = SaveHighScore().RetrieveHighScore() as! HighScore
println(retrievedHighScore.highScore)
for(block, blockStatus) in blockStatuses {
var thisBlock = self.childNodeWithName(block)!
if blockStatus.shouldRunBlock() {
blockStatus.timeGapForNextRun = random()
blockStatus.currentInterval = 0
blockStatus.isRunning = true
}
if blockStatus.isRunning {
if thisBlock.position.x > blockMaxX {
thisBlock.position.x -= CGFloat(groundspeed)
}
else{
thisBlock.position.x = self.origBlockPositionX
blockStatus.isRunning = false
retrievedHighScore.highScore++
if ((retrievedHighScore.highScore % 5) == 0) {
groundspeed++
}
self.scoreText.text = String(retrievedHighScore.highScore++)
}
}else{
blockStatus.currentInterval++
}
}
}
For some reason, it will only increment to 1 and then just display 1 in scoreText, even if it has passed more than one block. If I just declare a normal variable and subtitute it in for retrievedHighScore.highScore++, everything works fine. When I use retrievedHighScore.highScore, it only increments to one and just displays 1 in scoreText, strangely, the 1 isn't even saved.
I really recommend using NSUserDefaults in this situation to persist your high score. I also recommend not creating a highscores object for the sake of simply having an Integer variable. You're creating alot of unnecessary overhead by utilizing a class to model a simple Integer.
All that code for archiving highscore can be simplified to 1 line: NSUserDefaults.standardUserDefaults().setInteger(highScore, forKey: "Highscore")
When you need to overwrite the highscore (when a new highscore is to replace the old one) you can simply overwrite the old highscore by calling the above line of code again.
You save alot of work, and your code will perform more efficiently. Using a class to store a single value type in your situation is a terrible choice. Crusty will be mad...
Related
I have a project that mixes Swift and objective c. The controller for my preference panes is written in Swift. I can’t seem to read those preferences in Objective C.
This is the code that writes the parameter into the preferences…
let userDefaults = Preferences.shared.getUserDefaults()
#IBAction func DefType(_ sender: NSPopUpButton) {
var parameter:NSNumber
parameter = DefType.indexOfSelectedItem + 1 as NSNumber
userDefaults.set(parameter, forKey: ConstantUtility.StorageKeys.SelectedType.rawValue)
}
func getUserDefaults() -> UserDefaults {
return UserDefaults.standard
}
The above code is working as the following code is in the function that sets up the preference pane And it initiates the popup correctly.
func setupUI() {
var temp1:Int
temp1 = userDefaults.parameter(forKey: ConstantUtility.StorageKeys.SelectedType.rawValue) ?? 0
temp1 -= 1
DefType.selectItem(at: temp1)
}
On the other side of the program is the following code intended to read the preferences. This code always produces an answer of zero.
- (void) tableViewDoubleTapAction {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
long selectedType = [userDefaults integerForKey:#"SelectedType"];
I'm assuming the problem is in the objectiveC code but I'm open to changing either or both. Been spinning my wheels too long on this one.
Thanks in advance!
I set up a Preferences class as follows...
class Preferences : NSObject {
private override init() {}
#objc static let shared = Preferences()
#objc var selectedType: ConstantUtility.SelectedType {
get {
let type = UserDefaults.standard.integer(forKey: ConstantUtility.StorageKeys.SelectedType.rawValue)
guard type != 0, let selectedType = ConstantUtility.SelectedType(rawValue: type) else {
return ConstantUtility.SelectedType.One
}
return type
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: ConstantUtility.StorageKeys.SelectedType.rawValue)
}
}
the swift code to read the preference was changed to
temp1 = userDefaults.integer(forKey: ConstantUtility.StorageKeys.SelectedChart.rawValue)
And the Objective C code became
- (void) tableViewDoubleTapAction {
long selectedChartType = Preferences.shared.selectedType;
It is all working!
I have a struct for my model and it needs to conform to a protocol that is NSObject only.
I am looking for a viable alternative to converting the model to a class. The requirements are:
Keeping the model a value type
Updating the model when photoLibraryDidChange is called
This would be the ideal implementation if PHPhotoLibraryChangeObserver would not require the implementation to be an NSObject
struct Model: PHPhotoLibraryChangeObserver {
var images:[UIImages] = []
fileprivate var allPhotos:PHFetchResult<PHAsset>?
mutating func photoLibraryDidChange(_ changeInstance: PHChange) {
let changeResults = changeInstance.changeDetails(for: allPhotos)
allPhotos = changeResults?.fetchResultAfterChanges
updateImages()
}
mutating func updateImages() {
// update self.images
...
}
}
I cannot pass the model to an external class implementing the observer protocol as then all the changes happen on the copy (its a value type...)
Any ideas? Best practices?
EDIT: Reformulated the question
EDIT 2: Progress
I have implemented a delegate as a reference type var of my model and pushed the data inside. Now photoLibraryDidChangeis not being called anymore.
This is the stripped down implementation:
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver {
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
override init(){
super.init()
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
if let changeResults = changeInstance.changeDetails(for: self.allPhotos!) {
self.allPhotos = changeResults.fetchResultAfterChanges
//this neve gets executed. It used to provide high quality images
self.updateImages()
}
}
}
func startFetching(){
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
PHPhotoLibrary.shared().register(self)
//this gets executed and fetches the thumbnails
self.updateImages()
}
fileprivate func appendImage(_ p: PHAsset) {
let pm = PHImageManager.default()
if p.mediaType == .image {
pm.requestImage(for: p, targetSize: CGSize(width: 1024, height: 768), contentMode: .default, options: nil){
image, _ in
if let im = image {
self.images.append(im)
}
}
}
}
fileprivate func updateImages() {
self.images = []
if let ap = allPhotos {
for index in 0..<min(ap.count, 10) {
let p = ap[index]
appendImage(p)
}
}
}
}
struct Model {
private var pkAdapter = PhotoKitAdapter()
var images:[UIImage] {
pkAdapter.images
}
func startFetching(){
pkAdapter.startFetching()
}
// example model implementation
mutating func select(_ image:UIImage){
// read directly from pkAdapter.images and change other local variables
}
}
I have put a breakpoint in photoLibraryDidChange and it just does not go there. I also checked that pkAdapter is always the same object and does not get reinitialised on "copy on change".
**EDIT: adding the model view **
This is the relevant part of the modelview responsible for the model management
class ModelView:ObservableObject {
#Published var model = Model()
init() {
self.model.startFetching()
}
var images:[UIImage] {
self.model.images
}
...
}
EDIT: solved the update problem
It was a bug in the simulator ... on a real device it works
I ended up with 2 possible designs:
decouple the PhotoKit interface completely from the model, let the modelview manage both and connect them, as the model view has access to the model instance.
create a PhotoKit interface as var of the model and push the mutable data that is generated by the PhotoKit interface inside it, so it can be changed with an escaping closure. The model is never called from the interface but just exposes the data inside the PhotoKit through a computer property.
I will show two sample implementation below. They are naive in many respect, they ignore performance problems by refreshing all pictures every time the PhotoLibrary is updated. Implementing proper delta updates (and other optimisation) would just clutter up the code and offer no extra insight on the solution to the original problem.
Decouple the PhotoKit interface
ModelView
class ModelView:ObservableObject {
var pkSubscription:AnyCancellable?
private var pkAdapter = PhotoKitAdapter()
#Published var model = Model()
init() {
pkSubscription = self.pkAdapter.objectWillChange.sink{ _ in
DispatchQueue.main.async {
self.model.reset()
for img in self.pkAdapter.images {
self.model.append(uiimage: img)
}
}
}
self.pkAdapter.startFetching()
}
}
Model
struct Model {
private(set) var images:[UIImage] = []
mutating func append(uiimage:UIImage){
images.append(uiimage)
}
mutating func reset(){
images = []
}
}
PhotoKit interface
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver, ObservableObject {
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
if let changeResults = changeInstance.changeDetails(for: self.allPhotos!) {
self.allPhotos = changeResults.fetchResultAfterChanges
self.updateImages()
self.objectWillChange.send()
}
}
}
func startFetching(){
PHPhotoLibrary.requestAuthorization{status in
if status == .authorized {
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
self.allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
PHPhotoLibrary.shared().register(self)
self.updateImages()
}
}
}
fileprivate func appendImage(_ p: PHAsset) {
// This actually appends multiple copies of the image because
// it gets called multiple times for the same asset.
// Proper tracking of the asset needs to be implemented
let pm = PHImageManager.default()
if p.mediaType == .image {
pm.requestImage(for: p, targetSize: CGSize(width: 1024, height: 768), contentMode: .default, options: nil){
image, _ in
if let im = image {
self.images.append(im)
self.objectWillChange.send()
}
}
}
}
fileprivate func updateImages() {
self.images = []
if let ap = allPhotos {
for index in 0..<min(ap.count, 10) {
let p = ap[index]
appendImage(p)
}
}
}
PhotoKit interface as property of the model
ModelView
class ModelView:ObservableObject {
#Published var model = Model()
}
Model
struct Model {
private var pkAdapter = PhotoKitAdapter()
var images:[UIImage] { pkAdapter.images }
}
PhotoKit interface
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver{
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
override init(){
super.init()
startFetching()
}
// the rest of the implementation is the same as before
...
I want a midi manager that means it can be called from anywhere...a Singleton instance...and handle music requests from different threads.
This is in the Playground
import PlaygroundSupport
import AudioToolbox
class MusicPlayerManager {
static let sharedInstance = MusicPlayerManager()
private init() {}
var musicPlayer : MusicPlayer? = nil
var sequence : MusicSequence? = nil
var track : MusicTrack? = nil
var time = MusicTimeStamp(1.0)
var player: OSStatus? = nil
var musicTrack: OSStatus? = nil
func playNotes(notes: [UInt8]) {
_ = NewMusicSequence(&sequence)
player = NewMusicPlayer(&musicPlayer)
player = MusicPlayerSetSequence(musicPlayer!, sequence)
player = MusicPlayerStart(musicPlayer!)
musicTrack = MusicSequenceNewTrack(sequence!, &track)
for index:Int in 0...6 {
var note = MIDINoteMessage(channel: 0,
note: notes[index],
velocity: 64,
releaseVelocity: 0,
duration: 1.0)
guard let track = track else {fatalError()}
musicTrack = MusicTrackNewMIDINoteEvent(track, time, ¬e)
time += 1
}
player = MusicPlayerSetSequence(musicPlayer!, sequence)
player = MusicPlayerStart(musicPlayer!)
}
}
var notes: [UInt8] = [71,69,62,72,71,69,67]
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
MusicPlayerManager.sharedInstance.playNotes(notes: notes)
}
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
MusicPlayerManager.sharedInstance.playNotes(notes: notes)
}
PlaygroundPage.current.needsIndefiniteExecution = true
Unfortunately the music often doesn't play at all, or plays over itself from each instance.
At worst it should play the first instance of the music; why doesn't it nicely do that?
I think you ran into a form of readers-writers problem here by attempting to simultaneously manipulate instance variables on your singleton from different threads. I don't know what your desired music player behavior is if it's being called from multiple threads at once, but for me one easy improvement would be to give your MusicPlayerManager singleton it's own DispatchQueue, where multiple overlapping requests can be handled sequentially.
You could define one like this:
let concurrentQueue = DispatchQueue(label: "com.stackoverflow.gamma", attributes: .concurrent)
And then wrap the code inside the playNotes function like this:
func playNotes(notes: [UInt8]) {
self.concurrentQueue.async(flags: .barrier) {
...
}
}
I have a struct and sometimes, for some users, there will be a crash when trying to access a variable of that type.
struct AppSettings {
var mute:Bool {
didSet {
if mute != oldValue {
let savedSettings = NSUserDefaults.standardUserDefaults()
savedSettings.setBool(mute, forKey: KEY_SETTING_MUTE)
}
}
}
init() {
let savedSettings = NSUserDefaults.standardUserDefaults()
if let savedMute = savedSettings.objectForKey(KEY_SETTING_MUTE) as? Bool {
mute = savedMute
} else {
mute = false
}
}
}
var appSettings = AppSettings()
And someplace during startup of the app it sometimes crashes
if appSettings.mute { // This will sometimes cause a crash
} // in AppDelegate or the methods it calls
This is only for some users and I cannot seem to reproduce it. Not being to reproduce it is the worst because it leaves me with nothing to work with.
Searching for unsafe mutable addressor as an error doesn't help because there are almost no results.
I think you solution is over engineered and difficult to understand or debug. I've been using a similar solution in my apps for a few years now to manage session state and wish to share with you. It's something you could lift straight from here and implement and be trouble free.
I work on a Session principle meaning the values which I wish to store and read go through a solid Session class and leave no room for error.
Here is how it's used from anywhere in the application.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// how to set the value to be stored in userdefaults
Session.HasMuted = true
// two example of how to read the value
if Session.HasMuted == true {
print("has muted")
}
if Session.HasMuted == false {
print("has not muted")
}
}
Here is the Session class
class Session {
class var HasMuted: Bool {
get {
guard let HasMuted = UserDefaults.standard.object(forKey: SessionSpace.HasMuted) as? Bool else {
return false
}
return HasMuted
}
set {
UserDefaults.standard.set(newValue, forKey: SessionSpace.HasMuted)
}
}
}
and the accompanying Session Space struct
struct SessionSpace {
static let HasMuted = "HasMuted"
}
I would consider adjusting this to suit your style, I'm not a big fan of capital letters for key strings etc, this is a more elegant readable and implement and forget solution. You can extend this by adding a setter / getter block and key string in the session space block and again use from anywhere in the app in seconds knowing it's trouble free and robust. Let me know if you need any more help. Hope you adopt it and save it to snippets.
It is better to
use
init() {
let savedSettings = UserDefaults.standard
let savedMute = savedSettings.bool(forKey: KEY_SETTING_MUTE)
instead of
init() {
let savedSettings = NSUserDefaults.standardUserDefaults()
if let savedMute = savedSettings.objectForKey(KEY_SETTING_MUTE) as? Bool {
I would like to call a function which is coded on another class.
So far I have made a struct on the file structs.swift for my data:
struct defValues {
let defCityName: String
let loadImages: Bool
init(defCity: String, loadImgs: Bool){
self.defCityName = defCity
self.loadImages = loadImgs
}
}
I have made the file Defaults.swift containing:
import Foundation
class DefaultsSet {
let cityKey: String = "default_city"
let loadKey: String = "load_imgs"
func read() -> defValues {
let defaults = NSUserDefaults.standardUserDefaults()
if let name = defaults.stringForKey(cityKey){
print(name)
let valuesToReturn = defValues(defCity: name, loadImgs: true)
return valuesToReturn
}
else {
let valuesToReturn = defValues(defCity: "No default city set", loadImgs: true)
return valuesToReturn
}
}
func write(city: String, load: Bool){
let def = NSUserDefaults.standardUserDefaults()
def.setObject(city, forKey: cityKey)
def.setBool(load, forKey: loadKey)
}
}
in which I have the two functions read, write to read and write data with NSUsersDefault respectively.
On my main ViewController I can read data with:
let loadeddata: defValues = DefaultsSet().read()
if loadeddata.defCityName == "No default city set" {
defaultCity = "London"
}
else {
defaultCity = loadeddata.defCityName
defaultLoad = loadeddata.loadImages
}
But when I try to write data it gives me error. I use this code:
#IBOutlet var settingsTable: UITableView!
#IBOutlet var defaultCityName: UITextField!
#IBOutlet var loadImgs: UISwitch!
var switchState: Bool = true
#IBAction func switchChanged(sender: UISwitch) {
if sender.on{
switchState = true
print(switchState)
}else {
switchState = false
print(switchState)
}
}
#IBAction func saveSettings(sender: UIButton) {
DefaultsSet.write(defaultCityName.text, switchState)
}
You need an instance of the DefaultsSet class
In the view controller add this line on the class level
var setOfDefaults = DefaultsSet()
Then read
let loadeddata = setOfDefaults.read()
and write
setOfDefaults.write(defaultCityName.text, switchState)
The variable name setOfDefaults is on purpose to see the difference.
Or make the functions class functions and the variables static variables and call the functions on the class (without parentheses)
From the code you posted, it seems you either need to make the write method a class method (just prefix it with class) or you need to call it on an instance of DefaultsSet: DefaultsSet().write(defaultCityName.text, switchState).
Another issue I found is that you also need to unwrapp the value of the textField. Your write method takes as parameters a String and a Bool, but the value of defaultCityName.text is an optional, so String?. This results in a compiler error.
You can try something like this:
#IBAction func saveSettings(sender: UIButton) {
guard let text = defaultCityName.text else {
// the text is empty - nothing to save
return
}
DefaultsSet.write(text, switchState)
}
This code should now compile and let you call your method.
Let me know if it helped you solve the problem