Response transformer exceptions - siesta-swift

With the service I'm working with, most of the responses are in XML, but a few are plain text. What's the best way to set that up?
Currently I have this:
// Root should return plain text, don't try to transform it
configureTransformer("/") { (content: String, entity) -> String? in
return content
}
// Most data is XML
configureTransformer("**") { (content: NSData, entity) -> NSXMLDocument? in
return try? NSXMLDocument(data: content, options: 0)
}
configureTransformer("**/properties/*") {
(content: NSData, entity) -> String? in
return String(data: content, encoding: NSUTF8StringEncoding)
}
..but when I query the root URL, which will be plain text, I get an error because the NSData -> NSXMLDocument transformer can't be applied.
Edit: Maybe what I really want is to apply the XML transformer when the content-type is application/xml. Is there a way to do that?

Based on what I see in Service.init(), I did this, and it's working pretty well:
func XMLResponseTransformer(
transformErrors: Bool = true) -> Siesta.ResponseTransformer
{
return Siesta.ResponseContentTransformer(transformErrors: transformErrors) {
(content: NSData, entity: Siesta.Entity) throws -> NSXMLDocument in
return try NSXMLDocument(data: content, options: 0)
}
}
configure(description: "xml") {
$0.config.pipeline[.parsing].add(XMLResponseTransformer(),
contentTypes: [ "*/xml" ])
}

Related

can I manually choose to get data from response cache or from server in Alamofire?

I am new in using Alamofire and iOS development in general. I am fetching data from server using the code below
func getDataFromServer(completion: #escaping ( _ dataString: String) -> Void) {
let url = "http://app10.com/index.php/Data"
let parameters : [String:Any] = ["someData": "xxx"]
AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers:nil).responseJSON { (response) in
switch response.result {
case .failure(let errorResponse) :
let errorMessage = "error message here: \(errorResponse.localizedDescription)"
print(errorMessage)
completion("")
case .success(let value) :
let json = JSON(value)
let dataString = json["data"].stringValue
completion(dataString)
}
}
}
as you can see from the code above, I am trying to get dataString from server. but what I want to achieve is something like this
if meetSomeCondition {
// get 'dataString' from response cache from alamofire
} else {
// fetch 'dataString' from server, using the code above
}
can I do something like that using only alamofire?
I am sorry, I am newbie, but I can do something like that if I use Firebase Firestore, so maybe alamofire has feature something like that
I have tried to search but I can't find it in Stackoverflow
The underlying URLSession utilises a URLCache. Ensure your URLSession uses a URLCache and your server uses an appropriate response to instruct the client to cache the response.
In order to get the cached response, if any, the method according HTTP would be to set a cache control request header:
Cache-Control: only-if-cached (see RFC 5.2.1.7 only-if-cached)
That is, you would need to set this additional header when you create the request. Then execute the request as usual and you would either get the cached response or a status code 504 if there is none. Exactly what you would need.
Now, when you try this you realise, it unfortunately won't work as expected (bummer!). The reasons for this is manifold and it would be futile to go into detail.
The approach you may try next is to set the cache control policy of the URLRequest as follows:
urlRequest.cachePolicy = .returnCacheDataDontLoad
You can look up the meaning of it here: returnCacheDataDontLoad
Please read the documentation and the source documentation as well carefully, since it's not implemented in every iOS version!
That seems to be a working solution, however when using Alamofire you need to access the underlying URLRequest in order to set the cache policy (you may search on SO how to accomplish this) - and you need to be sure Alamofire does not alter your cache policy under the hood.
Alamofire does not provide cache directly.
It is easy to achieve it yourself.
use NSCache to keep our memory footprint low,
use time stamp, to invalidate stale data
here is an example:
// by writing a thin wrapper around NSCache, we can create a much more flexible Swift caching API
// — that enables us to store structs and other value types, and let us use any Hashable key type
final class Cache<Key: Hashable, Value> {
private let wrapped = NSCache<WrappedKey, Entry>()
private let dateProvider: () -> Date
private let entryLifetime: TimeInterval
init(dateProvider: #escaping () -> Date = Date.init,
entryLifetime: TimeInterval = 12 * 60 * 60) {
self.dateProvider = dateProvider
self.entryLifetime = entryLifetime
}
func insert(_ value: Value, forKey key: Key) {
let date = dateProvider().addingTimeInterval(entryLifetime)
let entry = Entry(value: value, expirationDate: date)
wrapped.setObject(entry, forKey: WrappedKey(key))
}
func value(forKey key: Key) -> Value? {
guard let entry = wrapped.object(forKey: WrappedKey(key)) else {
return nil
}
guard dateProvider() < entry.expirationDate else {
// Discard values that have expired
removeValue(forKey: key)
return nil
}
return entry.value
}
func removeValue(forKey key: Key) {
wrapped.removeObject(forKey: WrappedKey(key))
}
}
private extension Cache {
final class WrappedKey: NSObject {
let key: Key
init(_ key: Key) { self.key = key }
override var hash: Int { return key.hashValue }
override func isEqual(_ object: Any?) -> Bool {
guard let value = object as? WrappedKey else {
return false
}
return value.key == key
}
}
}
private extension Cache {
final class Entry {
let value: Value
let expirationDate: Date
init(value: Value, expirationDate: Date) {
self.value = value
self.expirationDate = expirationDate
}
}
}
extension Cache {
subscript(key: Key) -> Value? {
get { return value(forKey: key) }
set {
guard let value = newValue else {
// If nil was assigned using our subscript,
// then we remove any value for that key:
removeValue(forKey: key)
return
}
insert(value, forKey: key)
}
}
}
call like this:
// global singleton
let cache = Cache<Int, Int>()
// ...
override func viewDidLoad() {
super.viewDidLoad()
cache.insert(5, forKey: 1)
print(cache[1])
}
Persistent caching is also easy.
use Codable to do serialize the data.
use FileManager to persist the data file to disk

Swift: How to save a Decodable.Protocol object to a variable?

In my application, several controllers have a very similar code structure, the differences are minimal, so for optimization I decided to create a basis for these controllers, and inherit each specific controller from the basis.
I have a function for sending network requests and processing a response, I pass the response structure as a parameter to this function, so that the function returns a ready-made response structure to me. Each such structure is Decodable.
An example of such a structure:
struct APIAnswerUserActivity: Decodable {
let status: String
let code: Int?
let data: [UserActivity]?
let total: Int?
}
Function for network requests, an object (structure) of the Decodable.Protocol type is accepted as a jsonType parameter:
public func networkRequest<T: Decodable> (
url: String,
timeout: Double = 30,
method: URLMethods = .GET,
data: [String : String]? = nil,
files: [URL]? = nil,
jsonType: T.Type,
success: #escaping (T) -> Void,
failure: #escaping (APIError) -> Void
) -> URLSessionDataTask { ... }
There are several parameters in the main controller that I override through override in the child controllers, one of these parameters should be an object of type Decodable for the general function to receive data correctly. The JSON structures of the response are very similar, but still slightly different, a common structure for them cannot be created, because the data is still a little different.
If in the main controller do this:
public var decodableType: Decodable.Type {
return APIAnswerUserActivity.self
}
That will work, and it is possible to redefine types, but the network function does not accept this, it needs the Decodable.Protocol object. If the type decodable.Protocol is specified for the variable decodableType, then it is no longer possible to add APIAnswerUserActivity.self, which is quietly accepted when the networkRequest function is called.
How to be in this situation? I hope that I managed to correctly and clearly state the essence of my problem. Thanks!
#Владислав Артемьев, I'm still not sure that I completely understand the problem because you haven't shared the code that takes the Decodable class. But the issues seems to be about how to pass a Decodable class.
I hope the following can help clarify how you can impose the right constraint on the generic and how you should declare the variable. You can paste it into a playground and experiment.
import Foundation
struct FakeToDo: Decodable {
var userId: Int
var id: Int
var title: String
var completed: Bool
}
enum URLMethods {
case GET
case POST
}
func networkRequest<T: Decodable> (
url: String,
timeout: Double = 30,
method: URLMethods = .GET,
data: [String : String]? = nil,
files: [URL]? = nil,
jsonType: T.Type,
success: #escaping (T) -> Void,
failure: #escaping (Error) -> Void
) -> URLSessionDataTask {
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { (data, response, error) in
if error != nil {
failure(error!)
return
}
guard let data = data else { return }
let decoder = JSONDecoder()
guard let value = try? decoder.decode(T.self, from: data) else { return }
// get back on the main queue for UI
DispatchQueue.main.async {
success(value)
}
})
return task
}
class Example<T> where T: Decodable {
let type: T.Type
init(_ type: T.Type) {
self.type = type
}
public var decodableType: T.Type {
return type
}
}
let decodableType = Example(FakeToDo.self).decodableType
let url = "https://jsonplaceholder.typicode.com/todos/1"
let task = networkRequest(url: url, jsonType: decodableType,
success: { value in print(value) },
failure: { error in print(error) })
task.resume()

Game Center - sending and receiving data with Swift

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.

EXC_BAD_ACCESS using Generics in Swift

Related question: Generic completion handler in Swift
In a Swift app I'm writing, I'm downloading JSON and I want to convert it into model objects. Right now, I'm doing that like this:
func convertJSONData<T: Entity>(jsonData: NSData?, jsonKey: JSONKey, _: T.Type) -> [T]? {
var entities = [T]()
if let data = jsonData {
// Left out error checking for brevity
var json = JSON(data: data, options: nil, error: nil)
var entitiesJSON = json[jsonKey.rawValue]
for (index: String, subJson: JSON) in entitiesJSON {
// Error: EXC_BAD_ACCESS(code=EXC_I386_GPFLT)
let entity = T(json: subJson)
entities.append(entity)
}
}
return entities
}
Each object conforming to Entity implements init(json: JSON). JSON is a type defined in the SwiftyJSON library. That's also the reason the enumeration looks a bit weird.
I call convertJSONData() in this method:
public func performJSONRequest<T where T: Entity>(jsonRequest: JSONRequest<T>) {
var urlString = ...
Alamofire.request(.GET, urlString, parameters: nil, encoding: .JSON).response { (request, response, data, error) -> Void in
var books = self.convertJSONData(data as? NSData, jsonKey: jsonRequest.jsonKey, T.self)
jsonRequest.completionHandler(books, error)
}
}
I get a runtime EXC_BAD_ACCESS(code=EXC_I386_GPFLT) error calling T(json: subJSON). There are no compiler warnings or errors. Although I left out error checking in the above code, there is error checking in the actual code and error is nil.
I'm not sure whether this is a compiler bug or my fault and any help figuring that out is much appreciated.
Several things are going on here, and I suspect the problem lies somewhere in the initializer of the class implementing the Entity protocol.
Assuming the code resembles the following:
protocol Entity {
init(json: JSON)
}
class EntityBase: Entity {
var name: String = ""
required init(json: JSON) { // required keyword is vital for correct type inference
if let nameFromJson = json["name"].string {
self.name = nameFromJson
}
}
func getName() -> String { return "Base with \(name)" }
}
class EntitySub: EntityBase {
convenience required init(json: JSON) {
self.init(json: json) // the offending line
}
override func getName() -> String { return "Sub with \(name)" }
}
The code compiles with self.init(json: json) in the sub-class, but actually trying to initialize the instance using the convenience method results in an EXC_BAD_ACCESS.
Either remove the initializer on the sub-class or simply implement required init and call super.
class EntitySub: EntityBase {
required init(json: JSON) {
super.init(json: json)
}
override func getName() -> String { return "Sub with \(name)" }
}
The method to convert the jsonData to an Entity (modified slightly to specifically return .None when jsonData is nil):
func convertJSONData<T:Entity>(jsonData: NSData?, jsonKey: JSONKey, type _:T.Type) -> [T]? {
if let jsonData = jsonData {
var entities = [T]()
let json = JSON(data: jsonData, options:nil, error:nil)
let entitiesJSON = json[jsonKey.rawValue]
for (index:String, subJson:JSON) in entitiesJSON {
let entity:T = T(json: subJson)
entities.append(entity)
}
return entities
}
return .None
}

AnyObject to array in swift

Following function is given:
class func collection(#response: NSHTTPURLResponse,
representation: AnyObject) -> [City] {
return []
}
So this function should return an array of city objects. I have to somehow transform the representation variable that is of type AnyObject to a city array.
I don't know what the exact type of representation is but I can do things like
println(representation[0])
and it will print the object. Any ideas how to transform representation to [City] array?
Update
Doing
println(representation as [City])
prints nil.
City.swift:
final class City : ResponseCollectionSerializable {
let id: String
let name: String
class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [City] {
return []
}
}
This is just copy and pasted from https://github.com/Alamofire/Alamofire#generic-response-object-serialization It should serialize a JSON response into objects:
#objc public protocol ResponseCollectionSerializable {
class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Self]
}
extension Alamofire.Request {
public func responseCollection<T: ResponseCollectionSerializable>(completionHandler: (NSURLRequest, NSHTTPURLResponse?, [T]?, NSError?) -> Void) -> Self {
let serializer: Serializer = { (request, response, data) in
let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let (JSON: AnyObject?, serializationError) = JSONSerializer(request, response, data)
if response != nil && JSON != nil {
return (T.collection(response: response!, representation: JSON!), nil)
} else {
return (nil, serializationError)
}
}
return response(serializer: serializer, completionHandler: { (request, response, object, error) in
completionHandler(request, response, object as? [T], error)
})
}
}
The representation parameter you're getting back is the result of a call to NSJSONSerialization.JSONObjectWithData..., so it's either a NSArray or a NSDictionary. Since you get a value for representation[0], we know it's an NSArray. Exactly what your code looks like will depend on the JSON (a sample of which you should include in a question like this), but your code will need to be something like (untested code ahead):
class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [City] {
var cities: [City] = []
for cityRep in representation {
// these next two lines should grab the city data using the correct key
let id = cityRep.valueForKey("cityID") as String
let name = cityRep.valueForKey("cityName") as String
// now add the city to our list
cities.append(City(id: id, name: name))
}
return cities
}
Assuming (and although I hate to make assumptions, your question is a bit vague about the details) that the representation is either an NSData object that represents the response, or an Array that you have created from the response.
In my experience, such a response is an array of dictionaries that you can use to create city objects. So you need to write a function that transforms this dictionary into a City object. Something with the signature:
parser (AnyObject) -> City
Now, you could just iterate through the array, apply this function to each dictionary, collect the results into an Array and return the result.
But you could be classier and map your function over the array and return the result.

Resources