Trouble using NSDictionary convenience init in Swift - ios

I seem to be having a problem trying to create an NSDictionary in Swift using one of the convenience intializers. My confusion though lies in the fact that the error says my function signature is wrong, yet I'm using the function signature that XCode autocompleted for me.
My Code:
var query = NSDictionary(objects: [kSecClass, kSecAttrService, kSecAttrAccount, kSecReturnData], forKeys: [kSecClassGenericPassword, "healthBIT.lastSync", key, true])
The XCode provided signature:
var query = NSDictionary(objects: <#[AnyObject]#>, forKeys: <#[AnyObject]#>)
The error when compiling is: Extra argument 'forKeys' in call
What am I missing here? Am I just too sleep deprived to see the obvious? Or is it just a stupid mistake derived from my relative inexperience with Swift?
PS: I am trying to use NSDictionary here instead of a normal Swift dict because Swift dicts can't store mixed types, and I need to pass this to the underlying C based Keychain API.

After a discussion in chat, it turned out the problem is about the key variable referenced from the code sample provided in the question, which comes from a for loop:
for key in lastSync {
...
}
the error is that key is a (key, value) tuple, and that's causing issues when using it in a NSDictionary (objc types cannot handle swift specific features, such as generics, tuples, etc.).
The problem is solved by expanding the tuple accordingly:
for (key, value) in lastSync {
...
}
or, if value is not used:
for (key, _) in lastSync {
...
}

Related

Determining Swift Types That Can Be Stored in UserDefaults

I am in the beginning stages of developing an open-source utility for storing state in the Bundle UserDefaults.
I'm encountering an issue when I add non-Codable data types to my Dictionary of [String: Any].
I need to be able to vet the data before trying to submit it, because the UserDefaults.set(_:) method won't throw any errors. It just crashes.
So I want to make sure that the Dictionary that I'm submitting is kosher.
I can't just check if it's Codable, because it can sometimes say that it isn't, when the struct is actually good. (It's a Dictionary<String, Any>, and I can cram all kinds of things in there).
I need to validate that the Dictionary can produce a plist. If this were ObjC, I might use one of the NSPropertyListSerialization methods to test the Dictionary, but it appears as if this set of methods is not available to Swift.
According to the UserDefaults docs, there are a specific set of types and classes that are "plist-studly."
I think testing each type in the list is unacceptable. I need to see if I can find a way to test that won't be screwed the first time Apple updates an OS.
Is there a good way to test a Dictionary<String, Any> to see if it will make UserDefaults.set(_:) puke?
The Property List type set of UserDefaults is very limited. The supported types are
NSString → Swift String
NSNumber → Swift Int, Double or Bool
NSDate → Swift Date
NSData → Swift Data
Arrays and dictionaries containing the 4 value types.
Any is not supported unless it represents one of the 4 value or 2 collection types.
Property List compliant collection types can be written to UserDefaults with PropertyListSerialization (even in Swift).
There are two protocols to serialize custom types to Data
Codable can serialize structs and classes.
NSCoding can serialize subclasses of NSObject.
All types in the structs/classes must be encodable and decodable (means conform to the protocol themselves).
The APIs of PropertyListSerialization / PropertyListEncoder/-Decoder and NSKeyed(Un)Archiver provide robust error handling to avoid crashes.
UPDATE[1]: And, just because I like to share, here's the actual completed project (MIT License, as is most of my stuff)
UPDATE: This is the solution I came up with. Even though I greenchecked vadian's excellent answer, I decided to get a bit more picky.
Thanks to matt pointing out that I was looking under the wrong sofa cushions for the keys, I found the Swift variant of NSPropertyListSerialization, and I use that to vet the top level of the tree. I suspect that I'll need to refactor it into a recursive crawler before I'm done, but this works for now.
Here's the code for the _save() method at the time of this writing. It works:
/* ################################################################## */
/**
This is a private method that saves the current contents of the _values Dictionary to persistent storage, keyed by the value of the "key" property.
- throws: An error, if the values are not all codable.
*/
private func _save() throws {
#if DEBUG
print("Saving Prefs: \(String(describing: _values))")
#endif
// What we do here, is "scrub" the values of anything that was added against what is expected.
var temporaryDict: [String: Any] = [:]
keys.forEach {
temporaryDict[$0] = _values[$0]
}
_values = temporaryDict
if PropertyListSerialization.propertyList(_values, isValidFor: .xml) {
UserDefaults.standard.set(_values, forKey: key)
} else {
#if DEBUG
print("Attempt to set non-codable values!")
#endif
// What we do here, is look through our values list, and record the keys of the elements that are not considered Codable. We return those in the error that we throw.
var valueElementList: [String] = []
_values.forEach {
if PropertyListSerialization.propertyList($0.value, isValidFor: .xml) {
#if DEBUG
print("\($0.key) is OK")
#endif
} else {
#if DEBUG
print("\($0.key) is not Codable")
#endif
valueElementList.append($0.key)
}
}
throw PrefsError.valuesNotCodable(invalidElements: valueElementList)
}
}

How to call Objective-C NSExpression( format: ....) from NativeScript?

I am working on expanding the NativeScript-Mapbox plugin to include the ability to render circles that resize as the map is zoomed. I am working off the example on this page.
On iOS, I am running into problem trying to translate this NSExpression call into NativeScript:
layer.circleRadius = [NSExpression expressionWithFormat: #"mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'exponential', 1.75, %#)",
#{#12: #2, #22: #180}];
It looks to me that the NativeScript analogue to this call listed in tns-platform-declarations .. objc!Foundation.d.ts NSExpression definition is:
static expressionWithFormatArgumentArray(expressionFormat: string, _arguments: NSArray<any>): NSExpression;
Is this the correct method?
If so, how do I turn [12: 2, 22: 180] into an NSArray type?
Lacking documentation, I have tried creating an array of arrays and passing it to:
new NSArray( {objects: myArray} );
but it crashes horribly with a segfault.
Clearly, I am missing something.
The swift (not objective-c) object you're dealing with there is not an array, but a dictionary.
import Swift
print(type(of: [12: 2, 22: 180]))
// Dictionary<Int, Int>
The array that it's expecting is an array of all of the format substitution parameters that you want; in this case, that array is of length 1 and contains just the dictionary. You could translate this into your NativeScript JavaScript/TypeScript:
const expr = NSExpression.expressionWithFormatArgumentArray(
"mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'exponential', 1.75, %#)",
[{12: 2, 22: 180}]);
To manually create an Native iOS object; it is almost always:
const x = NSArray.alloc().init();'
or
const y = new NSArray();
However a couple notes;
You almost always want to use NSMutableArray() rather than just plain NSArray as NSArray is static; and Mutable means you can change it. Since you are creating it in JS; you have to add the values to it after you create it so Mutable is the type you almost always need.
You can use new NSMutableArray(); in most cases; however I have ran in to cases where new doesn't work properly vs .alloc().init() on some native iOS objects even though new is basically is a wrapper that does .alloc().init(). I recommend you just always do .alloc().init() just to keep things consistent.;
If the iOS function call specifically takes an NSArray; then you can do things like this:
const myNSArray = NSArray.arrayWithArray([1,2,3,4,5]);
In this example; myNSArray would now be an NSArray object, and the [1,2,3,4,5] was originally a JS array object we passed in. By default calling any iOS function that expects a NSArray will auto-convert any JavaScript Array into a NSArray for you. Marshaling of most data types are automatically done for you.
If the iOS function has no valid types (i.e. a vardef type); or if the function has a variable number of arguments; then NativeScript may not be able to call it at all, and as such will crash with an error when you try.
Finally, if you have other questions on how to do things JS to iOS (or Android) in NativeScript; I would recommend you search on that keyword at http://searchcode.nativescript.rocks and then peruse the source code it finds that is using those specific keywords.
As for the NSExpression part of the question
const expr = NSExpression.expressionWithFormatArgumentArray(
"mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'exponential', 1.75, %#)",
NSArray.arrayWithArray([NSDictionary.dictionaryWithDictionary({12: 2, 22: 180})]));
Because an Array has no specific object type that can go in it, it is safer to force the object type that we are putting into it as a NSDictionary as that is what iOS will be expecting. And just to make sure that what is being passed to the expressWithFormatArgumentArray is an NSArray, we are wrapping the JSArray we created with the NSArray.arrayWithArray() call. You can probably get away just doing [{}] but TS sometimes hates not knowing the types of things.
Ok, just to loop back and finish up this thread, I have /finally/ figured out how to get this to work without crashing.
The method requires an array with an NSDictionary type in it and it appears the NativeScript type definition for NSDictionary is incorrect per this issue and this one.
So after a few days of trial and error, converting the Objective-C call:
layer.circleRadius = [NSExpression expressionWithFormat: #"mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'exponential', 1.75, %#)",
#{#12: #2, #22: #180}];
into TypeScript/NativeScript is completely counterintuitive:
let nsDict = new (NSDictionary as any)( [ 2, 180 ], [ 12, 22 ] );
let nsArray = NSArray.arrayWithArray( [ nsDict ] );
layer.circleRadius = NSExpression.expressionWithFormatArgumentArray(
"mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'exponential', 1.75, %#)", nsArray );
This compiles and works. Note that the keys are in a separate array that follow the values, not the other way around.
If you use the NSDictionary approach suggested above you get a compile error because apparently the typing doesn't support that notation.
Upgrading to the latest NativeScript and core modules did allow it to compile the suggested NSArray() [] reference but the resulting code causes an exception:, namely:
const expr = NSExpression.expressionWithFormatArgumentArray( "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'exponential', 1.75, %#)", [{12: 2, 22: 180}]);
crashes with an exception.
The array, as far as I am able to tell, is not correctly marshalled into an NSDictionary instance. Same for using NSArray.arrayWithArray( [ ... ] );

A NSOperationQueue error I'm trying to figure out from Crashlytics

I got this error on Crashlytics this morning and I can't firgure out what the problem is. It would be awesome to get your opinions about it. I thInk it's most likely a multi threading issue. But I'm not able to pin point exactly what it is.
EDIT: I dug a little deeper and here's the code that's failing:
Also, I've figured out that the error is:
Could not cast value of type '__NSSingleObjectArrayI' (0x1aa60bca0) to 'NSMutableArray' (0x1aa60bd90).
2016-09-22 08:29:34.136764 GrabbnGo[4204:822290] Could not cast value of type '__NSSingleObjectArrayI' (0x1aa60bca0) to 'NSMutableArray' (0x1aa60bd90).
This was working perfectly all this while and it's suddenly causing problems and the app is already on the store :/
json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions()) as? [String: AnyObject]
let str = NSString(data: data!, encoding: NSUTF8StringEncoding)
print(str)
let OrderDictionary = json as NSDictionary
let result = OrderDictionary.objectForKey("result") as! NSMutableArray
OK, this sort of has a unique answer. Basically, you're misusing NSJSONSerialization, and it's a time-bomb bug that eventually bit you.
According to the documentation:
https://developer.apple.com/reference/foundation/jsonserialization
All objects are instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull.
This is a very simple and clear sentence. You should respect it. It says nothing about NSMutableArray, only NSArray. The JSON parser is using whatever compatible (subclass) object for the NSArray that it so chooses. If there's only one item in the array, it appears that the internal type __NSSingleObjectArray is a lot more efficient, probably both in speed and memory.
There is almost certainly a change on the server (or customer behavior) such that result now (often? sometimes? always?) has only 1 item in it, so the JSON parser made a different choice that you're not supposed to care about.
All you need to do is change it to NSArray and construct an NSMutableArray from it if you really need:
let myJSONParsedArray: NSArray = ...
let myMutableArray = NSMutableArray(myJSONParsedArray)
But first learn about why force unwrapping is so dangerous, especially in a network code environment, when you can't trust any data at all, regarding type and value and maliciousness, ever!!

compare two secKey (public keys) in ios Swift

I want to ssl public key pinning in swift, I read lot of examples how to do that, last think who I can't find is How to compare two public keys in SecKey object format.
Example:
let serverPublicKey = SecTrustCopyPublicKey(secTrust) /*return SecKey object from actual SecTrust*/
let clientPublicKey = getLocalPublicKeyFromDer() /*return SecKey from .der local*/
how to compare them? At now I do that and it works:
if(serverPublicKey! as AnyObject).isEqual(clientPublicKey){
/*Key is the same, pinning OK!*/
}
find it way on gitHub: https://github.com/teamcarma/IOS-AlamofireDomain/blob/master/Source/ServerTrustPolicy.swift
but is cast to AnyObject a good idea? How to work isEqual on casted SecKey? Can any explain me?
ps.
Another idea is getting base64 from SecKey - I try and it also works, but it require a KeyChain temp operations and look no profesional.
Cited from the headers:
"Most SecKeychainItem functions will work on an SecKeyRef."*
You may cast SecKeyRef to a SecKeychainItem. If this is a valid operation (that is, the key is a keychain item), you may apply function
SecKeychainItemCreatePersistentReference
and get a CFData object, filled with attributes and data. Then check with memcpyon the bytes or cast it to a NSData object and check with isEqualToData. Don't forget to release the CFData object.
Edit
On iOS, as far as I known, the only reliable approach is to copy the data (or secret) into the keychain, using a temporary key, so that you can find it again, and then extract the data. It's cumbersome, but if you just implement it in a minimalistic way, it should not take more than 30 lines of code. I have a working example.
I The usual disclaimer: Use this at your own risk, and always be careful with security stuff.
iOS10 added:
CFDataRef _Nullable SecKeyCopyExternalRepresentation(SecKeyRef key, CFErrorRef *error)
so you can now create two Data (NSData) objects, then compare those.
Have a look at this answer for just getting the NSData: Can I get the modulus or exponent from a SecKeyRef object in Swift?
You can then compare the two NSData instances using isEqualToData:
I don't have expereince in the domain, but if they are two strings (irrespectiveof their content), you would basically do a simple check:
if(string1 == string2)
{
//condition logic
}

Swift, can't create a dictionary to hold key values?

I want to create a dictionary to hold these values but I get the following error
import Security
var keychainQuery: Dictionary =
[
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: userAccount,
dataFromString: kSecValueData
]
Cannot convert the expression's type 'Dictionary' to type '#lvalue Unmanaged<AnyObject>!'
In most cases with Swift, I've found it much less painful to declare the types of the dictionary. In your case:
var keychainQuery: Dictionary<String, AnyObject> = [
...
]
Should work.
As far as I know for now, building a Keychain query with a Swift Dictionary is not possible.
Keys like kSecClass are of a non-hashable type and so can't be put in a Dictionary (that strictly needs items that share the same protocol btw.).
One of the ways I have seen to circumvent this problem is to use an NSDictionary and pass keys and values as Arrays. This code only seems to work in Xcode 6 Beta 1.
var query = NSMutableDictionary(objects: [kSecClassGenericPassword, service, account, secret], forKeys: [kSecClass, kSecAttrService, kSecAttrAccount, kSecValueData])
Another approach can be seen on this Stackoverflow answer.
Based on that approach you would convert all the key and value constants to Strings using the \() syntax.
The query would then look like this:
var query = ["\(kSecClass)": "\(kSecClassGenericPassword)", "\(kSecAttrService)": service, "\(kSecAttrAccount)": account, "\(kSecValueData)": secretData]
I tested this solution and got an error saying that I'm trying to add already existing keys. So this doesn't seem to work either.

Resources