Can you send objects other than strings in URLQueryItems? - swift

Ok, I am building an iMessage app and to transfer data back and forth I have to use URLQueryItems. I am working with an SKScene and need to transfer Ints, CGPoints, images, etc. Reading Apple's documentation and my own attempts it seems like you can only store strings in URLQueryItems.
As this us the only way to pass data back and forth, is there a (better) way to store other types of data? Currently I have been doing this:
func composeMessage(theScene: GameScene) {
let conversation = activeConversation
let session = conversation?.selectedMessage?.session ?? MSSession()
let layout = MSMessageTemplateLayout()
layout.caption = "Hello world!"
let message = MSMessage(session: session)
message.layout = layout
message.summaryText = "Sent Hello World message"
var components = URLComponents()
let queryItem = URLQueryItem(name: "score",value: theScene.score.description)
components.queryItems = [queryItem] //array of queryitems
message.url = components.url!
print("SENT:",message.url?.query)
conversation?.insert(message, completionHandler: nil)
}
Then on the flip side I have to convert this string back to an Int again. Doing this with CGPoints will be inefficient.. how would one pass something like a CGPoint in a URLQueryItem? Any other way than storing the x and y values as strings?
EDIT: This is how I have been receiving data from the other person and putting into their scene:
override func willBecomeActive(with conversation: MSConversation) {
// Called when the extension is about to move from the inactive to active state.
// This will happen when the extension is about to present UI.
// Use this method to configure the extension and restore previously stored state.
let val = conversation.selectedMessage?.url?.query?.description
print("GOT IT ", val)
if(val != nil)
{
scene.testTxt = val!
}
}

As you discovered, to pass data via URLQueryItem, you do have to convert everything to Strings since the information is supposed to be represented as a URL after all :) For CGPoint information, you can break the x and y values apart and send them as two separate Ints converted to String. Or, you can send it as a single String value in the form of "10,5" where 10 is the x and 5 is the y value but at the other end you would need to split the value on a comma first and then convert the resulting values back to Ints, something like this (at the other end):
let arr = cgPointValue.components(separatedBy:",")
let x = Int(arr[0])
let y = Int(arr[1])
For other types of data, you'd have to follow a similar tactic where you convert the values to String in some fashion. For images, if you have the image in your resources, you should be able to get away with passing just the name or an identifying number. For external images, a URL (or part of one if the images all come from the same server) should work. Otherwise, you might have to look at base64 encoding the image data or something if you use URLQueryItem but if you come to that point, you might want to look at what you are trying to achieve and if perhaps there is a better way to do it since large images could result in a lot of data being sent and I'm not sure if iMessage apps even support that. So you might want to look into limitations in the iMessage app data passing as well.
Hope this helps :)

You can use iMessageDataKit library for storing key-value pairs in your MSMessage objects. It makes setting and getting data really easy and straightforward like:
let message: MSMessage = MSMessage()
message.md.set(value: 7, forKey: "moveCount")
message.md.set(value: "john", forKey: "username")
message.md.set(values: [15.2, 70.1], forKey: "startPoint")
message.md.set(values: [20, 20], forKey: "boxSize")
if let moveCount = message.md.integer(forKey: "moveCount") {
print(moveCount)
}
if let username = message.md.string(forKey: "username") {
print(username)
}
if let startPoint = message.md.values(forKey: "startPoint") {
print("x: \(startPoint[0])")
print("y: \(startPoint[1])")
}
if let boxSize = message.md.values(forKey: "boxSize") {
let size = CGSize(width: CGFloat(boxSize[0] as? Float ?? 0),
height: CGFloat(boxSize[1] as? Float ?? 0))
print("box size: \(size)")
}
(Disclaimer: I'm the author of iMessageDataKit)

Related

Accessing Firebase Data inside unique AutoID

This is my first question and I'm still learning Swift/Xcode/Firebase, so I appreciate your patience. I've been stalking StackOverflow and have found a lot of answers to help with various things, but nothing that makes sense for the problem I've been struggling with for 2 days.
I am writing a program that will save a date picked on a previous viewcontroller and a set of user-entered floats from text fields to a Firebase database, and append each data set as a separate entry instead of overwriting the previous data. Using the first block of code below, I've got this problem solved except I can't find a way to do it without using AutoID. This leaves me with a setup like this in Firebase, but with multiple categories and "optionSelected" sections in each category:
program-name
Category 1
optionSelected
L1cggMnqFqaJf1a7UOv
Date: "21-12-2017"
Variable 1 Float: "12345"
Variable 2 Float: "26.51"
L1ciVpLq1yXm5khimQC
Date: "30-12-2017"
Variable 1 Float: "23456"
Variable 2 Float: "35.88"
Code used to save:
func newWithNewVars() {
let myDatabase = Database.database().reference().child("Category 1").child(optionSelected)
let variable1 = textField1.text
let variable2 = textField2.text
let variable1Float = (textField1.text! as NSString).floatValue
let variable2Float = (textField2.text! as NSString).floatValue
let writeArray = ["Date": textPassedOverDate, "Variable 1 Float": variable1Float, "Variable 2 Float": variable2Float]
myDatabase.childByAutoId().setValue(gasArray) {
(error, reference) in
if error != nil {
print(error!)
}
else {
print("Message saved successfully!")
}
}
}
The problem comes with recalling data. Since the AutoID is unique, I can't figure out how to access the data deeper inside for calculations. Specifically, I want to be able to make a new entry, press the save data button, and have it find the most recent entry in the "optionSelected" section so it can do calculations like subtract the older variable 1 from the new variable 1 and such.
Given the above description, layout, and code used above, what code structure would allow me to find the most recent date and access the data inside the AutoID sections for a specific category and "optionSelected"?
Thank you for your help.
The issue you're having is that you're trying to dig deeper but can't as you don't have a hold of that id. You'll want to use the .childAdded in your reference observation when you want to get inside of a list in your JSON tree when you don't have a hold of that id to get inside - this will be called as many times as there are values inside of Category 1 tree:
let reference = Database.database().reference()
reference.child("Category 1").child("optionSelected").observe(.childAdded, with: { (snapshot) in
let uniqueKey = snapshot.key // IF YOU WANT ACCESS TO THAT UNIQUE ID
print(uniqueKey)
guard let dictionary = snapshot.value as? [String: AnyObject] else { return }
let date = dictionary["date"] as? String
let variableOne = dictionary["Variable 1 Float"] as? Float
let variableOne = dictionary["Variable 2 Float"] as? Float
}, withCancel: nil)
You may also want to avoid using spaces in your database keys to avoid any problems in the near future. I'd stick with the common lowercased underscore practice e.g. "category_1" or "variable_2_float"

How to pass and get multiple URLQueryItems in Swift?

Ok, I am working in an iMessage app and am trying to parse more than 1 url query item from the selected message here- I have been successful getting/sending just 1 value in a query:
override func willBecomeActive(with conversation: MSConversation) {
// Called when the extension is about to move from the inactive to active state.
// This will happen when the extension is about to present UI.
if(conversation.selectedMessage?.url != nil) //trying to catch error
{
let components = URLComponents(string: (conversation.selectedMessage?.url?.query?.description)!)
//let val = conversation.selectedMessage?.url?.query?.description
if let queryItems = components?.queryItems {
// process the query items here...
let param1 = queryItems.filter({$0.name == "theirScore"}).first
print("***************=> GOT IT ",param1?.value)
}
}
When I just have 1 value, just by printing conversation.selectedMessage?.url?.query?.description I get an optional with that 1 value, which is good. But with multiple I cant find a clean way to get specific values by key.
What is the correct way to parse a URLQueryItem for given keys for iMessage?
When you do conversation.selectedMessage?.url?.query?.description it simply prints out the contents of the query. If you have multiple items then it would appear something like:
item=Item1&part=Part1&story=Story1
You can parse that one manually by splitting the string on "&" and then splitting the contents of the resulting array on "=" to get the individual key value pairs in to a dictionary. Then, you can directly refer to each value by key to get the specific values, something like this:
var dic = [String:String]()
if let txt = url?.query {
let arr = txt.components(separatedBy:"&")
for item in arr {
let arr2 = item.components(separatedBy:"=")
let key = arr2[0]
let val = arr2[1]
dic[key] = val
}
}
print(dic)
The above gives you an easy way to access the values by key. However, that is a bit more verbose. The way you provided in your code, using a filter on the queryItems array, is the more compact solution :) So you already have the easier/compact solution, but if this approach makes better sense to you personally, you can always go this route ...
Also, if the issue is that you have to write the same filtering code multiple times to get a value from the queryItems array, then you can always have a helper method which takes two parameters, the queryItems array and a String parameter (the key) and returns an optional String value (the value matching the key) along the following lines:
func valueFrom(queryItems:[URLQueryItem], key:String) -> String? {
return queryItems.filter({$0.name == key}).first?.value
}
Then your above code would look like:
if let queryItems = components?.queryItems {
// process the query items here...
let param1 = valueFrom(queryItems:queryItems, key:"item")
print("***************=> GOT IT ", param1)
}
You can use iMessageDataKit library. It makes setting and getting data really easy and straightforward like:
let message: MSMessage = MSMessage()
message.md.set(value: 7, forKey: "user_id")
message.md.set(value: "john", forKey: "username")
message.md.set(values: ["joy", "smile"], forKey: "tags")
print(message.md.integer(forKey: "user_id")!)
print(message.md.string(forKey: "username")!)
print(message.md.values(forKey: "tags")!)
(Disclaimer: I'm the author of iMessageDataKit)

Use Measurement for input in NSTextFieldCell

I want to be able to nicely use a Measurement and MeasurementFormatter for output and input with a NSTextFieldCell.
I am able to display the measurement correctly with...
let areaFormatter = MeasurementFormatter()
areaFormatter.unitStyle = .medium
areaFormatter.unitOptions = .providedUnit
let area = Measurement<UnitArea>( value: 123.43, unit: .squareInches)
let editInput = NSTextFieldCell
editInput.objectValue = area
editInput.formatter = areaFormatter
This displays something like
123.43 in^2
The problem starts when I want to read this back in with
var inputArea = editInput.objectValue as! Measurement<UnitArea>
I think because the get Object value of the Measurement Formatter is not defined.
open func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool
Is my understanding correct? Are there any examples in Swift where this function has been defined for measurements?
Right now the user can edit the entire string including the text in the units. Is there a good way to block the units in the NSTextFieldCell? I would like the user to be able to edit the number but not the units and then return the measurement with
var inputArea = editInput.objectValue as! Measurement<UnitArea>
so this gets displayed
123.43 in^2
but only the 123.43 can be edited.

Store CLLocation Manager Values in Device swift

I'm building an app that sends (every x seconds) to an API the location values with some extra values (session, ID, etc) that is working nice (see here Update CLLocation Manager on another method). But for improved feature, we are considering the device can lost (for some amount of time) internet connection. So I need to temporary store all values and when reconnected send them again to the API.
I'm considering several ways to do it:
Core Data (difficult implementation)
Realm (very little experience)
NSDisctionary
Can anyone suggest (and show how, if possible) the best way to implement this feature?
If you want to store a some of non-sensitive values (such as a password), I suggest to use NSUserDefaults, you can easily use it like a dictionary:
Note: Swift 2 Code.
For example:
// shared instance (singleton)
let userDefaults = NSUserDefaults.standardUserDefaults()
// **storing**:
let myString = "my string"
userDefaults.setObject(myString, forKey: "kMyString")
let myInt = 101
userDefaults.setInteger(myInt, forKey: "kMyInt")
let myBool = true
userDefaults.setBool(myBool, forKey: "kMyBool")
userDefaults.synchronize()
// **retrieving**:
//use optional binding for objects to check if it's nil
if let myRetrievedString = userDefaults.objectForKey("kMyString") as? String {
print(myRetrievedString)
} else {
// if it's nil
print("there is now value for key: kMyString")
}
// if there is no value for key: kMyInt, the output should be zero by default
let myRetrievedInt = userDefaults.integerForKey("kMyInt")
// if there is no value for key: kMyBool, the output should be false by default
let myRetrievedBool = userDefaults.boolForKey("kMyBool")
Tadaaaaaa:
func arrayOfDictionaries() {
var offline:[[String:AnyObject]] = []
offline.append(["LATITUDE: ": userLocation.coordinate.latitude, "LONGITUDE: ": userLocation.coordinate.longitude, "SPEED: ": userLocation.speed])
NSUserDefaults().setObject(offline, forKey: "offLine")
if let offLinePositions = NSUserDefaults().arrayForKey("offLine") as? [[String:AnyObject]] {
//print(offLinePositions)
for item in offLinePositions {
print(item["LATITUDE: "]! as! NSNumber) // A, B
print(item["LONGITUDE: "]! as! NSNumber) // 19.99, 4.99
print(item["SPEED: "]! as! NSNumber) // 1, 2
}
}
}

How do I 'seed' using GKRandomSource from Swift's Gameplaykit to remember the shuffle between sessions

I'm new to programming and have been learning Swift by doing a number of online courses. In one of the courses we built a basic trivia game and I've been progressively trying to improve it by doing my own coding (best way to learn!).
Recently I came across what's called a Fisher-Yates shuffle and, after much trial and error (and with the help of the stack overflow community) was able to use GKRandomSource from Swift's Gameplaykit to shuffle my trivia questions around so that they were being asked randomly. This was an improvement on the original arc4random code I was using because the shuffle removed questions already asked from the overall pool of questions, thereby ensuring that they did not repeat (at least in iOS9).
This works well within the session, but once the user quits the app and relaunches it, the shuffle starts from scratch. So I was looking at a way to have the app 'remember' the questions already asked between sessions. My research led me to the idea of seeding and I've been trying to get this to work with my GKRandomSource code, but I'm obviously missing something.
Any advice etc would be most welcome - especially since I'm not entirely sure that this 'seeding' approach will achieve my ultimate aim of not repeating questions already asked in previous sessions of the app.
Below are what I believe to be the relevant bits of my revised code.
All questions and potential answer choices are stored in a .json file as such:
{
"id" : "1",
"question": "Earth is a:",
"answers": [
"Planet",
"Meteor",
"Star",
"Asteroid"
],
"difficulty": "1"
}
I use the following code to load the .json file:
func loadAllQuestionsAndAnswers()
{
let path = NSBundle.mainBundle().pathForResource("content", ofType: "json")
let jsonData : NSData = NSData(contentsOfFile: path!)!
allEntries = (try! NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.MutableContainers)) as! NSArray
//println(allEntries)
}
And below is my most recent code for trying to achieve the shuffle of all questions and replicate it in future sessions):
var allEntries : NSArray!
var shuffledQuestions: [AnyObject]!
var nextQuestion = -1
var mySeededQuestions : [AnyObject]
loadAllQuestionsAndAnswers()
if #available(iOS 9.0, *) {
let lcg = GKLinearCongruentialRandomSource(seed: mySeededQuestions)
let shuffledQuestions = lcg.arrayByShufflingObjectsInArray(allEntries)
nextQuestion++
loadQuestion(nextQuestion)
// Fallback on earlier versions
}else{
let randomNumber = Int(arc4random_uniform(UInt32(allEntries.count)))
loadQuestionPreiOS9(randomNumber)
}
I know at the very least I have a problem with the above code, but I'm at a loss. I'm also thinking that maybe I'm missing a step in terms of storing the seed?
For the sake of completeness, I use a label to display the question and four images to display the potential answers, using the following code:
func loadQuestion(index : Int)
{
let entry : NSDictionary = shuffledQuestions[index] as! NSDictionary
let question : NSString = entry.objectForKey("question") as! NSString
let arr : NSMutableArray = entry.objectForKey("answers") as! NSMutableArray
//println(question)
//println(arr)
labelQuestion.text = question as String
let indices : [Int] = [0,1,2,3]
//let newSequence = shuffle(indices)
let newSequence = indices.shuffle()
var i : Int = 0
for(i = 0; i < newSequence.count; i++)
{
let index = newSequence[i]
if(index == 0)
{
// we need to store the correct answer index
currentCorrectAnswerIndex = i
}
let answer = arr.objectAtIndex(index) as! NSString
switch(i)
{
case 0:
buttonA.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 1:
buttonB.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 2:
buttonC.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 3:
buttonD.setTitle(answer as String, forState: UIControlState.Normal)
break;
default:
break;
}
}
buttonNext.hidden = true
// we will need to reset the buttons to reenable them
ResetAnswerButtons()
}
func loadQuestionPreiOS9(index : Int)
{
let entry : NSDictionary = allEntries.objectAtIndex(index) as! NSDictionary
let question : NSString = entry.objectForKey("question") as! NSString
let arr : NSMutableArray = entry.objectForKey("answers") as! NSMutableArray
//println(question)
//println(arr)
labelQuestion.text = question as String
let indices : [Int] = [0,1,2,3]
//let newSequence = shuffle(indices)
let newSequence = indices.shuffle()
var i : Int = 0
for(i = 0; i < newSequence.count; i++)
{
let index = newSequence[i]
if(index == 0)
{
// we need to store the correct answer index
currentCorrectAnswerIndex = i
}
let answer = arr.objectAtIndex(index) as! NSString
switch(i)
{
case 0:
buttonA.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 1:
buttonB.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 2:
buttonC.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 3:
buttonD.setTitle(answer as String, forState: UIControlState.Normal)
break;
default:
break;
}
}
buttonNext.hidden = true
// we will need to reset the buttons to reenable them
ResetAnswerButtons()
}
Finally, I use the following code to present the user with a 'Next' button after they've answered a question:
#IBAction func PressedButtonNext(sender: UIButton) {
print("button Next pressed")
if #available(iOS 9.0, *) {
nextQuestion++
loadQuestion(nextQuestion)
}else{
let randomNumber = Int(arc4random_uniform(UInt32(allEntries.count)))
loadQuestionPreiOS9(randomNumber)
}
I know my coding is probably quite verbose and unnecessary, but up until this latest improvement it's been working fine and I actually understand most of it (I think!)
There are really two questions here: what you're asking about and what you seem to want. They're both worth answering for different reasons, so...
How to seed a GK(Whatever)RandomSource
(All the GKRandomSource subclasses have seeds, even though the superclass GKRandomSource itself doesn't... that's because each class has its own data type for seeds. But the usage is the same.)
The critical bits of the code you've posted don't even compile due to a type mismatch: the seed/init(seed:) value for GKLinearCongruentialRandomSource is an integer, not an array of objects. The documentation for that value spells out what it's for (emphasis added) and how to use it:
Any two random sources initialized with the same seed data will generate the same sequence of random numbers. To replicate the behavior of an existing GKLinearCongruentialRandomSource instance, read that instance’s seed property and then create a new instance by passing the resulting data to the initWithSeed: initializer.
So, if you want to replicate a sequence of random numbers:
Create a random source with the plain initializer.
let source = GKLinearCongruentialRandomSource()
Save off that source's seed value.
let seed = source.seed // -> some UInt64 value
// write seed to user defaults, a file, a web service, whatever.
Use that random source for whatever.
Later, when you launch again and want the same sequence, read in the seed value and create a random source using the seed.
let seed = // read in seed value from wherever you saved it
let source = GKLinearCongruentialRandomSource(seed: seed)
This still doesn't get you what you're actually looking for, though: If source in step 1 produced the sequence 1, 6, 3, 9, 2, 7, source from step 4 will also produce the sequence 1, 6, 3, 9, 2, 7 — the seed doesn't record where you "left off" in a sequence. Or, since you're using it for an array shuffle, it'll produce the same shuffled ordering of the array as the first shuffle, but it doesn't remember what you did with the shuffled array thereafter.
How to use a shuffled ordering across multiple app launches
If you want to shuffle an array, walk through it in order, and then on a later run of your app continue walking through the same shuffled array from where you left off, you need to design around that requirement.
Shuffle on the first launch.
Record something about the ordering produced. (Say, a mapping of indices in the shuffle to indices in the original data.)
When walking through the shuffled array, record how far you've gone through it.
On later runs of the app, use the record of the ordering and the record of progress to decide where you are.
Here's a rough pass at that. (Note that I'm not touching your data model — this is a program design question, and SO is not a coding service. You'll need to think about how to flesh out this design to match your model and its use cases.)
struct Defaults {
static let lastQuestionIndex = "lastQuestionIndex"
static let questionOrder = "questionOrder"
}
let questions: [Question] // array of model objects, always in fixed order
func nextQuestion() -> Question {
let defaults = NSUserDefaults.standardUserDefaults()
if let lastIndex = defaults.integerForKey(Defaults.lastQuestionIndex) {
// we've run before, load the ordering
guard let shuffledOrder = defaults.arrayForKey(Defaults.questionOrder) as? [Int]
else { fatalError("save questionOrder with lastQuestionIndex") }
// advance the saved index so the next call to this function
// will get the next question
if lastIndex + 1 < count {
defaults.setInteger(lastIndex + 1, forKey: Defaults.lastQuestionIndex)
} else {
// ran out of shuffled questions, forget the order so we
// can reshuffle on the next call
defaults.removeObjectForKey(Defaults.questionOrder)
defaults.removeObjectForKey(Defaults.lastQuestionIndex)
}
// map lastQuestionIndex from sequential to shuffled
// and return the corresponding answer
let shuffledIndex = shuffledOrder[lastIndex]
return questions[shuffledIndex]
} else {
// first run, shuffle the question ordering (not the actual questions)
let source = GKRandomSource()
let sequentialOrder = Array(0..<questions.count)
let shuffledOrder = source.arrayByShufflingObjectsInArray(sequentialOrder)
// save the ordering, and the fact that we're asking the first question
defaults.setObject(shuffledOrder, forKey: Defaults.questionOrder)
defaults.setInteger(0, forKey: Defaults.lastQuestionIndex)
// return the first question in the shuffled ordering
let shuffledIndex = shuffledOrder[0]
return questions[shuffledIndex]
}
}
That's probably a bit pseudocode-ish (so you might need to worry about casting arrays to work with NSUserDefaults, etc), but as a general design it should be enough to give you some food for thought.
You can also use the following to drop off a certain amount of values so if you keep a roll count dropping that many on next start is as easy as :
arc4.dropValues(rollCount)