Game Center - sending and receiving data with Swift - ios

I am trying to send the following structure from one player to another:
struct GamePacket {
var action: String
var pointArray: [CGPoint]
}
I'm having a hard time figuring out the correct way to convert the GamePacket to Data and back again.
Here is the code I have so far for sending packets:
func send(packet: GamePacket) {
//convert GamePacket to Data here
let data = Data.init()
var remotePlayerArray = [GKPlayer]()
if let currentPlayer = self.currentPlayer, let match = self.match, let playerArray = self.match?.players {
for player in playerArray {
if player != currentPlayer {
remotePlayerArray.append(player)
}
}
do {
try match.send(data, to: remotePlayerArray, dataMode: GKMatchSendDataMode.reliable)
}
catch {
print("connectionError")
}
}
}
And the code for receiving:
func match(_ match: GKMatch, didReceive data: Data, fromRemotePlayer player: GKPlayer) {
//convert Data to GamePacket here
}
From some samples written in ObjectiveC, I managed to convert the GamePacket to Data using something simmilar to the following:
let data = NSData(bytes: &packet, length: MemoryLayout<GamePacket>.size) as Data
However, I can't figure out how to convert the Data back to a GamePacket on the receiving end, nor am I sure this is the correct way to do it.
Any help is greatly apreciated. Thank you.

Use Codable
struct GamePacket: Codable {
var action: String
var pointArray: [CGPoint]
}
Then you can convert to Data easily:
func save<T: Encodable>(_ item: T, to url: URL) throws -> Data {
let encoder = JSONEncoder()
return try encoder.encode(item)
}
func load<T: Decodable>(from data:Data) throws -> T {
let decoder = JSONDecoder()
let item = try decoder.decode(T.self, from: data)
return item
}

A quick and dirty solution would be something like this:
func encodeGamePacket(packet: GamePacket) -> NSData {
return NSData(bytes: &gamePacket, length: MemoryLayout<GamePacket>.size)
}
func decodeGamePacket(data: NSData) -> GamePacket? {
var tempBuffer:GamePacket? = nil
data.getBytes(&tempBuffer, length: MemoryLayout<GamePacket>.size)
return tempBuffer
}
I have not messed with direct addresses myself under swift yet, so I am not entirely sure whether this is the best approach. Note that I used an optional return type, you can design this differently in your code (maybe add some checks, unwrap the variable and return it or throw an exception when the checks fail).
Alternatively you could design a method that writes your GamePacket into a String (for readability, for example), which you can in turn transform into NSData (String has a data method) or you turn GamePacket into an NSCoding compliant class that offers methods to convert itself into NSData as well.

Related

Cannot cast/decode data from server into Swift data model?

I need to cast the below response from my server as [UserResult] but I cannot get it to work??
What am I doing wrong?
func userSearch(keyword: String, completion: #escaping (Result<[UserResult], ResponseError>) -> Void ) {
socket.emit("userSearch", keyword)
socket.on("userFound") { ( data, ack) in
print(data) // prints below NSArray
if !data.isEmpty {
if let response = data as? [UserResult] {
print("USERS \(response)") // WILL NOT WORK?
completion(.success(response))
}
} else {
completion(.failure(.badRequest("No users found")))
}
}
}
Data from server
[<__NSArrayM 0x60000040e5b0>(
{
profileUrl = "www.address1.com";
username = chrissmith;
},
{
profileUrl = "www.address2.com";
username = johnsmith;
},
{
profileUrl = "www.address3.com";
username = alicesmith;
}
)
]
UserResult Model
struct UserResult: Decodable {
let username: String
let profileUrl: String
}
Well you are using Socket.IO library and specifically method
socket.on(clientEvent: .connect) {data, ack in
...
}
defined as
#discardableResult
open func on(clientEvent event: SocketClientEvent, callback: #escaping NormalCallback) -> UUID
using typealias:
public typealias NormalCallback = ([Any], SocketAckEmitter) -> ()
So basically at the and you are being returned data of type [Any] according to documentation.
Since you do not know what is inside your data it is better for you to unwrap objects in your array one by one (instead casting it directly to [UserResult]) and try to find out what Type there are by comparing to some set of known types as some of answers from this question suggest.
I would start with verifying the data structure with example code below , and only move on with casting to various type afterwards:
Lets assume example data1 is your data:
let dict1 = ["profileUrl":"www.address1.com","username":"chrissmith"]
let data1: NSArray = [dict1]
//printed data1:
// (
// {
// profileUrl = "www.address1.com";
// username = chrissmith;
// }
// )
if data1[0] as? [String:String] != nil {
print("We found out that first object is dictionary of [String:String]!")
}
else if data1[0] as? Dictionary<NSObject, AnyObject> != nil {
print("We found out that first object is dictionary of mixed values!")
} else {
print("We found out that first object has different data structure")
}
Hopefully this answer was at least a little bit helpfull, even though not providing direct easy solution for your problem.

Where to store Decoded JSON array from server and how to access it globally in viewControllers?

Currently im creating application which parses JSON from my server. From server I can receive array with JSON models.
Data from this array must be populated in table View.
My question Is simple: where to store decoded array from server, if I want to access it from many viewControllers in my application?
Here is my JSON model, which coming from server.
import Foundation
struct MyModel: Codable {
var settings: Test?
var provider: [Provider]
}
extension MyModel {
struct setting: Codable {
var name: String
var time: Int
}
}
here is how I am decoding it
import Foundation
enum GetResourcesRequest<ResourceType> {
case success([ResourceType])
case failure
}
struct ResourceRequest<ResourceType> where ResourceType: Codable {
var startURL = "https://myurl/api/"
var resourceURL: URL
init(resourcePath: String) {
guard let resourceURL = URL(string: startURL) else {
fatalError()
}
self.resourceURL = resourceURL.appendingPathComponent(resourcePath)
}
func fetchData(completion: #escaping
(GetResourcesRequest<ResourceType>) -> Void ) {
URLSession.shared.dataTask(with: resourceURL) { data, _ , _ in
guard let data = data else { completion(.failure)
return }
let decoder = JSONDecoder()
if let jsonData = try? decoder.decode([ResourceType].self, from: data) {
completion(.success(jsonData))
} else {
completion(.failure)
}
}.resume()
}
}
This is an example of CategoriesProvider. It just stores categories in-memory and you can use them across the app. It is not the best way to do it and not the best architecture, but it is simple to get started.
class CategoriesProvider {
static let shared = CategoriesProvider()
private(set) var categories: [Category]?
private let categoryRequest = ResourceRequest<Category>(resourcePath: "categories")
private let dataTask: URLSessionDataTask?
private init() {}
func fetchData(completion: #escaping (([Category]?) -> Void)) {
guard categories == nil else {
completion(categories)
return
}
dataTask?.cancel()
dataTask = categoryRequest.fetchData { [weak self] categoryResult in
var fetchedCategories: [Category]?
switch categoryResult {
case .failure:
print("error")
case .success(let categories):
fetchedCategories = categories
}
DispatchQueue.main.async {
self?.categories = fetchedCategories
completion(fetchedCategories)
}
}
}
}
I suggest using URLSessionDataTask in order to cancel a previous task. It could happen when you call fetchData several times one after another. You have to modify your ResourceRequest and return value of URLSession.shared.dataTask(...)
Here more details about data task https://www.raywenderlich.com/3244963-urlsession-tutorial-getting-started#toc-anchor-004 (DataTask and DownloadTask)
Now you can fetch categories in CategoriesViewController in this way:
private func loadTableViewData() {
CategoriesProvider.shared.fetchData { [weak self] categories in
guard let self = self, let categories = categories else { return }
self.categories = categories
self.tableView.reloadData()
}
}
In the other view controllers, you can do the same but can check for the 'categories' before making a fetch.
if let categories = CategoriesProvider.shared.categories {
// do something
} else {
CategoriesProvider.shared.fetchData { [weak self] categories in
// do something
}
}
If you really want to avoid duplicate load data() calls, your simplest option would be to cache the data on disk (CoreData, Realm, File, etc.) after parsing it the first time.
Then every ViewController that needs the data, can just query your storage system.
Of course the downside of this approach is the extra code you'll have to write to manage the coherency of your data to make sure it's properly managed across your app.
make a global dictionary array outside any class to access it on every viewcontroller.

Extracting data from API (JSON format) doesn't save data outside of function call

I am trying to get an array of temperatures in a given time period from an API in JSON format. I was able to retrieve the array through a completion handler but I can't save it to another variable outside the function call (one that uses completion handler). Here is my code. Please see the commented area.
class WeatherGetter {
func getWeather(_ zip: String, startdate: String, enddate: String, completion: #escaping (([[Double]]) -> Void)) {
// This is a pretty simple networking task, so the shared session will do.
let session = URLSession.shared
let string = "api address"
let url = URL(string: string)
var weatherRequestURL = URLRequest(url:url! as URL)
weatherRequestURL.httpMethod = "GET"
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
if let error = error {
// Case 1: Error
// We got some kind of error while trying to get data from the server.
print("Error:\n\(error)")
}
else {
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
let weather = try JSON(data: data!)
let conditions1 = weather["data"]
let conditions2 = conditions1["weather"]
let count = conditions2.count
for i in 0...count-1 {
let conditions3 = conditions2[i]
let conditions4 = conditions3["hourly"]
let count2 = conditions4.count
for j in 0...count2-1 {
let conditions5 = conditions4[j]
let tempF = conditions5["tempF"].doubleValue
let windspeed = conditions5["windspeedKmph"].doubleValue
temps.append(tempF)
winds.append(windspeed)
}
}
completion([temps, winds])
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
// The data task is set up...launch it!
dataTask.resume()
}
}
I am calling this method from my view controller class. Here is the code.
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
}
//Here it prints out an empty array
print(weatherData)
}
The issue is that API takes some time to return the data, when the data is return the "Completion Listener" is called and it goes inside the "getWeather" method implementation, where it prints the data of array. But when your outside print method is called the API hasn't returned the data yet. So it shows empty array. If you will try to print the data form "weatherData" object after sometime it will work.
The best way I can suggest you is to update your UI with the data inside the "getWeather" method implementation like this:
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
// Update your UI here.
}
//Here it prints out an empty array
print(weatherData)
}
It isn't an error, when your controller get loaded the array is still empty because your getWeather is still doing its thing (meaning accessing the api, decode the json) when it finishes the callback will have data to return to your controller.
For example if you were using a tableView, you will have reloadData() to refresh the UI, after you assign data to weatherData
Or you could place a property Observer as you declaring your weatherData property.
var weatherData:[Double]? = nil {
didSet {
guard let data = weatherData else { return }
// now you could do soemthing with the data, to populate your UI
}
}
now after the data is assigned to wheaterData, didSet will be called.
Hope that helps, and also place your jsonParsing logic into a `struct :)

Checking an entire struct has been received via GKMatchDelegate didReceiveData

I'm working on a Multiplayer Swift (XCode 7.3 / iOS 9.2) game using GameKit.
I'm using a custom struct as the information sent via GameKit. Copy of the struct is below. frustrationNetworkMessage is a String enum & Card is a seperate class (can post code if needed)
struct frustrationNetworkMessage {
var messageType : FrustrationNetworkMessageType
var cards : [Card]
var actingPlayer : String
init(messageType : FrustrationNetworkMessageType, cards : [Card], actingPlayer : String) {
self.messageType = messageType
self.cards = cards
self.actingPlayer = actingPlayer
}
My issue is in the 'match(match: GKMatch, didReceiveData data: NSData, fromRemotePlayer player: GKPlayer)' method. I've worked out this is being called once some data is received:
private var receivedData : NSMutableData
func decode<T>(data: NSData) -> T {
let pointer = UnsafeMutablePointer<T>.alloc(sizeof(T.Type))
data.getBytes(pointer, length: sizeof(T))
return pointer.move()
}
func match(match: GKMatch, didReceiveData data: NSData, fromRemotePlayer player: GKPlayer) {
print("Match / did receive data: size of data - \(sizeofValue(data))")
receivedData.appendData(data)
if let receivedNetworkMessage : frustrationNetworkMessage = decode(receivedData) {
print("Match / did receive data: \(receivedNetworkMessage.messageDetails())")
When I run the above code it crashes at the final print statement with EXC_BAD_ACCESS (which I understand is it complaining it's trying to access part of the struct that has not been received).
As an example:
Message summary at sent -> sendNetworkMessage: Message type: startNewGame with actingPlayer: G:118279601 & card count: 1 - message size: 40
Message length first time match didReceiveData is called -> Match / did receive data: size of data - 8
How can I check I've gotten the entire struct before acting on it? I had thought of updating the struct to include a length field (in theory I should always receive that in one go - so could use it to check the packet length).
You can use a background process like this to check your data structure continuously?
newQueue = NSOperationQueue()
let operation2 = NSBlockOperation(block: {
})
operation2.completionBlock = {
print("Operation 2 completed")
}
let operation1 = NSBlockOperation(block: {
})
operation1.completionBlock = {
self.newQueue.addOperation(operation2)
}
operation1.qualityOfService = .UserInitiated
newQueue.addOperation(operation1)
}
Initialise the variables and then check their length; this will save/stop it crashing.
I've found an answer for the minute (issue has now moved elsewhere in the code)
struct frustrationNetworkMessage {
var messageType : FrustrationNetworkMessageType
var card : Card
var actingPlayer : String
init(messageType : FrustrationNetworkMessageType, card : Card, actingPlayer : String) {
self.messageType = messageType
self.card = card
self.actingPlayer = actingPlayer
}
init(data : NSData) {
let messageTypeLength = sizeofValue(FrustrationNetworkMessageType)
let cardLength = sizeofValue(Card)
let messageTypeRange = NSMakeRange(0, messageTypeLength)
let cardLengthRange = NSMakeRange(messageTypeLength, messageTypeLength + cardLength)
let actingPlayerRange = NSMakeRange(messageTypeLength + cardLength, data.length)
self.messageType = .didDiscardCard
self.card = Card.emptyCard()
data.getBytes(&self.messageType, range: messageTypeRange)
data.getBytes(&self.card, range: cardLengthRange)
let actingPlayerData = data.subdataWithRange(actingPlayerRange)
self.actingPlayer = NSString(data: actingPlayerData, encoding: NSUTF8StringEncoding) as! String
}
func archive()->NSData {
var localMessageType = messageType.rawValue
var localCard = card
let returnValue = NSMutableData(bytes: &localMessageType, length: sizeofValue(self.messageType.rawValue))
returnValue.appendData(NSData(bytes: &localCard, length: sizeofValue(self.card)))
returnValue.appendData(actingPlayer.dataUsingEncoding(NSUTF8StringEncoding)!)
return returnValue
}
What I've found through trial an error (and a lot of follow-up reading):
match didReceiveData only seems to get called when it has received an entire packet (so don't need to handle receiving partial data)
Where possible avoid encoding class data types as NSData (through digging I found I was only encoding a local address, works fine if passing locally - not so much over the network). Close to all of my data types as either basic ones (all enums are now of type UInt8) or structs.

Convert array of custom object to AnyObject in Swift

In my app I am doing something like this:
struct Record {
var exampleData : String
}
class ExampleClass : UIViewController {
let records = [Record]()
override func viewDidLoad() {
super.viewDidLoad()
let data = NSKeyedArchiver.archivedDataWithRootObject(self.records) as! NSData
}
...
}
But in the last line of viewDidLoad() I got this error:
Argument type '[Record]' does not conform to expected type 'AnyObject'
How can I fix this? Thanks.
If you want to keep struct, you can encode data using withUnsafePointer(). Here's an example, which I adapted from this Gist:
import UIKit
enum EncodingStructError: ErrorType {
case InvalidSize
}
func encode<T>(var value: T) -> NSData {
return withUnsafePointer(&value) { p in
NSData(bytes: p, length: sizeofValue(value))
}
}
func decode<T>(data: NSData) throws -> T {
guard data.length == sizeof(T) else {
throw EncodingStructError.InvalidSize
}
let pointer = UnsafeMutablePointer<T>.alloc(1)
data.getBytes(pointer, length: data.length)
return pointer.move()
}
enum Result<T> {
case Success(T)
case Failure
}
I added some error handling and marked the method as throws. Here's one way you can use it, in a do…catch block:
var res: Result<String> = .Success("yeah")
var data = encode(res)
do {
var decoded: Result<String> = try decode(data)
switch decoded {
case .Failure:
"failure"
case .Success(let v):
"success: \(v)" // => "success: yeah"
}
} catch {
print(error)
}
The error handling I added will not decode if the NSData length doesn't match the type size. This can commonly happen if you write the data to disk, the user updates to a newer version of the app with a different-sized version of the same type, and then the data is read in.
Also note that sizeof() and sizeofValue() may return different values on different devices, so this isn't a great solution for sending data between devices (NSJSONSerialization might be better for that).
AnyObject means any reference type object, primarily a class. A struct is a value type and cannot be passed to a function needing an AnyObject. Any can be used to accept value types as well as reference types. To fix your code above, change struct Record to class Record. But I have a feeling you may want to use a struct for other reasons. You can create a class wrapper around Record that you can convert to and from to use for functions that need an AnyObject.
I did a similar thing:
static func encode<T>(value: T) -> NSData {
var val = value
return withUnsafePointer(to: &val) { pointer in
NSData(bytes: pointer, length: MemoryLayout.size(ofValue: val))
}
}
static func decode<T>(data: NSData) -> T {
guard data.length == MemoryLayout<T>.size.self else {
fatalError("[Credential] fatal unarchiving error.")
}
let pointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
data.getBytes(pointer, length: data.length)
return pointer.move()
}

Resources