Subclass AFNetworking JSON Response Serialization to insert JSON response body in to error data in Swift 2 - swift

AFNetworking does not return the JSON response body when it fails (e.g. 400 status code), therefore you must subclass AFJSONResponseSerializer and fill in the error object with such. This is the recommended here and an example is here
Up until Swift 2, I used the following code to achieve such:
import Foundation
let JSONResponseSerializerWithDataKey : String = "JSONResponseSerializerWithDataKey"
let JSONResponseUndefinedAPIFailureReason : String = "UNKNOWN_ERROR"
class JSONResponseSerializerWithData: AFJSONResponseSerializer
{
override func responseObjectForResponse (response: NSURLResponse, data: NSData, error: AutoreleasingUnsafeMutablePointer
<NSError?>) -> AnyObject
{
var json : AnyObject = super.responseObjectForResponse(response, data: data, error: error) as AnyObject
if (error.memory? != nil)
{
var errorValue = error.memory!
var userInfo = errorValue.userInfo
if let errorDetail = json["detail"] as? String
{
userInfo![JSONResponseSerializerWithDataKey] = errorDetail
}
else
{
userInfo![JSONResponseSerializerWithDataKey] = JSONResponseUndefinedAPIFailureReason
}
error.memory = NSError(domain: errorValue.domain, code: errorValue.code, userInfo: userInfo)
}
return json
}
}
Start with Swift 2, a new type of Error handling was introduced.
The signature of the above function is now:
override func responseObjectForResponse(response: NSURLResponse!, data: NSData!) throws -> AnyObject
I am having trouble achieving the same as above inside a do-catch statement as it seems the failure does not invoke the catch statement, and thus there is no access to the error object. Further, new ErrorTypes are essentially empty and don't contain
This is what I've tried, but the catch statement is never called:
class JSONResponseSerializerWithData: AFJSONResponseSerializer
{
override func responseObjectForResponse(response: NSURLResponse!, data: NSData!) throws -> AnyObject
{
do
{
return try super.responseObjectForResponse(response, data: data)
}
catch
{
let nsError = (error as NSError)
var userInfo = nsError.userInfo
if let errorDetail = userInfo["detail"] as? String
{
userInfo[JSONResponseSerializerWithDataKey] = errorDetail
}
else
{
userInfo[JSONResponseSerializerWithDataKey] = JSONResponseUndefinedAPIFailureReason
}
throw NSError(domain: nsError.domain, code: nsError.code, userInfo: userInfo)
}
}
}
I've tried stepping through the AFNetworking2 Library an the body of the response is there, so I could sub-class it in Objective-C rather than Swift, but would prefer doing such if possible.
Am I handling this incorrectly with a do-catch statement? Any pointers in the right direction would be greatly appreciated.

After more digging, I have found that the issue is as described here.
The error is not thrown, only filled, therefore catch is never called. To induce this old behaviour you can add "NS_SWIFT_NOTHROW" to the end of the Objective-C signature in the header file as described here
This will change the signature to:
override func responseObjectForResponse(response: NSURLResponse?, data: NSData?, error: NSErrorPointer) -> AnyObject?
Which can then be used like before.

Related

How to get data from NSDocument's read method

I'm having difficulty trying to put 'data' onto NSPasteboard. By 'data', I mean a type other than the specific PasteboardType formats: text, HTML, image, etc. (It's actually MIDI data.)
override func copy() -> Any {
let pboard = NSPasteboard.general
pboard.clearContents()
pboard.setData(data, forType: .typeMidi)
return true
}
When I try to put my data in, I get:
Cannot convert value of type '(String) throws -> Data' to expected element type 'NSPasteboardWriting'.
That's probably because I've been trying to use NSDocument's data method, but it turns out that's only for writing out to disk.
The data needs to come from the read function:
override func read(from data: Data, ofType typeName: String) throws {
self.theMIDIPlayer = try AVMIDIPlayer.init(data: data, soundBankURL: nil)
if self.theMIDIPlayer == nil {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}
But it seems that the read function's arguments are not NSDocument's data function (which only relates to writing). I've no idea where the arguments come from.
Adding something like self.myData = data to the read function (in an attempt to get the data in a useful property) produce "Expected pattern" errors.
SOLVED: The problem was a schoolboy error of using copy() instead of copy(_:).
Vadian's now deleted answer was helpful in creating a custom Pasteboard type.
The complete (relevant) code is as follows:
extension NSPasteboard.PasteboardType {
static let typeMidi = NSPasteboard.PasteboardType(rawValue: "public.midi-audio")
}
class Document: NSDocument {
var theMIDIPlayer: AVMIDIPlayer?
var myData: Data?
#IBAction func copy(_: Any) {
let pboard = NSPasteboard.general
pboard.clearContents()
pboard.setData(myData, forType: .typeMidi)
}
override func read(from data: Data, ofType typeName: String) throws {
self.theMIDIPlayer = try AVMIDIPlayer.init(data: data, soundBankURL: nil)
self.myData = data
if self.theMIDIPlayer == nil {
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
}

Ambigous reference to initializer 'init(_:)' when trying to parse Any? object to String in Swift

I'm trying to parse an Any? object to String which contains an HTML Document that comes from a WKWebView after JS Execution.
If I try to print the Html: Any? object everything shows on console, but when I try to store it in a String var to do another stuff with it, appears the error
Ambigous reference to initializer 'init(_:)'
Here is my code:
func getHTML() {
miWEBVIEW.evaluateJavaScript("document.documentElement.outerHTML.toString()", completionHandler: { (html: Any?, error: Error?) in
print(html) // -> This works showing HTML on Console, but need in String to manipulate
return html
})
}
Here is where I call the function in a button event:
let document: Any? = getHTML()
var documentString = String(document) // -> error appears in this line
The problem is that your getTML method returns Void. You can't initialize a String object with it. Besides that WKWebView evaluateJavaScript is an asynchronous method. You need to add a completion handler to your getHTML method:
func getHTML(completion: #escaping (_ html: Any?, _ error: Error?) -> ()) {
webView.evaluateJavaScript("document.documentElement.outerHTML.toString()",
completionHandler: completion)
}
Then you need call your getHTML method like this:
getHTML { html, error in
guard let html = html as? String, error == nil else { return }
self.doWhatever(with: html)
}
func doWhatever(with html: String) {
print(html)
// put your code here
}

CLSLogv logs are not coming in Crashlytics

Its an Ionic app with some code written in native. Its uses cordova-plugin-firebase that for logging Crashlytics.
In the native part for iOS as well, we are trying to use Crashlytics to enable logging. However no matter what I try logs sent using CLSLogv aren't visible in dashboard.
Here is my code.
#objc(ImageUpload) class ImageUpload : CDVPlugin {
var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
//https://docs.fabric.io/apple/crashlytics/enhanced-reports.html#custom-logging-in-swift
private func sendErrorToCrashlytics(error: String) {
NSLog("Error in send error function is \(error)")
CLSLogv("%#", getVaList([error]))
}
#objc(imageUpload:)
func imageUpload(command: CDVInvokedUrlCommand) {
registerBackgroundTask()
func execute() {
let db = SQLiteDatabase()
var recToUpLoad: PayloadModel? = nil
while(toCheck) {
do {
let record = try db.readValues() // THIS METHOD THROWS EXCEPTION
} catch Exceptions.SQLiteError(let error) {
self.sendErrorToCrashlytics(error: error) // IT COMES HERE AFTER EXCEPTION
}
}
}
DispatchQueue(label: "imageUploadPlugin",qos: .background).async
{
execute()
}
}
}
However CLSLogv is not visible at all in Crashlytics. However when I do Crashlytics.sharedInstance().throwException()
, I can see it in the dashboard.
Exceptions is enum
enum Exceptions: Error {
case SQLiteError(message: String)
case JSONError(message: String)
}
Hoping it may help someone. Somehow I couldn't get CLSLogv to work. I ended up creating an NSError object and log that in Crashlytics in catch block.
catch Exceptions.SQLiteError(let error) {
let userInfo = [NSLocalizedDescriptionKey: error.message, "query": error.query]
let errorObj = NSError(domain: "sqlite", code: 400, userInfo: userInfo)
Crashlytics.sharedInstance().recordError(errorObj)
}

Function in completion-block is never executed

I have the following situation:
Classes involved are
ViewController (of type UIViewController)
RequestHandler
DataRequest (imports Alamofire for the request)
Step 1)
override func viewDidLoad() {
super.viewDidLoad()
requestHandler.getData(view: self)
}
So here, my instance of the RequestHandler class is calling its getData()-function.
Step 2)
func getData(view: ViewController) {
let dataRequest = DataRequest()
dataRequest.getDataTree(handler: self) { response in
view.saveData(dataTree: reponse)
}
}
In this getData method inside my class RequestHandler i'm creating an instance of the desired Request-class. In this case an instance of the class DataRequest. This instance then class its function getDataTree() and handles the response over to my ViewController by invoking the saveData()-function.
Step 3)
import Foundation
import SwiftyJSON
import Alamofire
class DataRequest {
func getDataTree(handler: RequestHandler, completion: #escaping ([String:[String:[String:[SomeStruct]]]]) -> ()) {
let requestURL = constants.Url.dataURL
Alamofire.request(requestURL, method: .post, encoding: URLEncoding.default)
.validate()
.responseJSON(completionHandler: { response in
completion(self.getServices(jsonData: JSON(data: response.data!)))
})
}
private func getServices(jsonData: JSON) -> [String:[String:[String:[SomeStruct]]]] {
var serviceDict: [String:[String:[String:[SomeStruct]]]] = [:]
for (k, subJson):(String, JSON) in jsonData {
let key = k
let dict: [String:[String:[SomeStruct]]] = getNames(jsonData: subJson)
serviceDict[key] = dict
}
return serviceDict
}
..followed by declaration of getNames-function etc until in the end, the SomeStruct objects in the array get parsed too.
So now that you got the background-information, my problem is the following:
By debugging I found out that the function getDataTree is called and executed, but the getServices-function never executes, despite being called in the completion-block.
I've only started as a programmer less than a year ago, so my experience is limited. I might aswell just make a really trivial mistake.
Thanks alot for your help already!

Unit Test HKSampleQuery in Swift

When I need to read data from HealthKit this is how my code looks like:
let stepsCount = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
let stepsSampleQuery = HKSampleQuery(sampleType: stepsCount,
predicate: nil,
limit: 100,
sortDescriptors: nil)
{ [unowned self] (query, results, error) in
if let results = results as? [HKQuantitySample] {
self.steps = results
// Update some UI
}
self.activityIndicator.stopAnimating()
}
healthStore?.executeQuery(stepsSampleQuery)
This specific code was extracted from here for demo purpose.
So my question is:
How can I unit test this kind of code ?
I encapsulate this code in a function in a model class that knows nothing about the UI. It works like this:
At the place the you have your
// Update some UI
call a completion closure, that was passed to the function using a parameter.
You call this function from your controller class like this
hkModel.selectSteps() {
[unowned self] (query, results, error) in
// update UI
}
This way you have a clean separation between your query logic in the model class and your UIController code.
Now you can easily write a unit test calling the same method:
func testSteps() {
hkModel.selectSteps() {
[unowned self] (query, results, error) in
// XCTAssert(...)
}
}
The last thing you need is to respect that your test code is called asynchronously:
let stepExpectationEnd = expectationWithDescription("step Query")
hkModel.selectSteps() {
[unowned self] (query, results, error) in
// XCTAssert(...)
stepExpectationEnd.fulfill()
}
waitForExpectationsWithTimeout(10.0) {
(error: NSError?) in
if let error = error {
XCTFail(error.localizedDescription)
}
}
update
Because you asked:
I handle authorization at the test setup. looks like this:
var healthData: HealthDataManager?
override func setUp() {
super.setUp()
healthData = HealthDataManager()
XCTAssert(healthData != nil, "healthDadta must be there")
let authorizationAndAScheduleExpectation = expectationWithDescription("Wait for authorizatiion. Might be manual the first time")
healthData?.authorizeHealthKit({ (success: Bool, error: NSError?) -> Void in
print ("success: \(success) error \(error?.localizedDescription)")
// fails on iPad
XCTAssert(success, "authorization error \(error?.localizedDescription)")
self.healthData?.scheduleAll() {
(success:Bool, error:ErrorType?) -> Void in
XCTAssert(success, "scheduleAll error \(error)")
authorizationAndAScheduleExpectation.fulfill()
}
})
waitForExpectationsWithTimeout(60.0) {
error in
if let error = error {
XCTFail(error.localizedDescription)
}
}
}
The first time you run this code in a simulator, you have to approve authorization manually.
After the first run the tests run without manual intervention.