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

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( [ ... ] );

Related

Accessing CFArray causes crash in Swift

My following code crashes with EXC_BAD_ACCESS, and I do not understand why. My initial understanding is that the memory retention in this case should be automatic, but it seems I am wrong... Maybe someone can help. Thank you! The code is written in Swift 5 and runs on iOS 15.2 in XCode 13.2.1.
Casting to NSArray causes trouble...
let someFont = CGFont("Symbol" as CFString)!
if let cfTags: CFArray = someFont.tableTags {
let nsTags = cfTags as NSArray
print(nsTags.count) // Prints: 16
let tag0 = nsTags[0] // CRASH: Thread 1: EXC_BAD_ACCESS (code=257, ...)
}
Alternatively, using CFArray-API causes also trouble (The crash message is about a misaligned pointer but the root cause seems also the bad access, which occurs e.g. if I replace UInt32.self by UInt8.self, and hence eliminate the alignment problem).
let someFont = CGFont("Symbol" as CFString)!
if let cfTags: CFArray = someFont.tableTags {
print(CFArrayGetCount(cfTags)) // Prints: 16
let tag0Ptr: UnsafeRawPointer = CFArrayGetValueAtIndex(cfTags, 0)!
tag0Ptr.load(as: UInt32.self)// CRASH :Thread 1: Fatal error: load from misaligned raw pointer
}
The issue here is that the CGFont API uses some advanced C-isms in their storage of table tags, which really doesn't translate to Swift: CGFontCopyTableTags() returns a CFArrayRef which doesn't actually contain objects, but integers. (This is technically allowed through CFArray's interface: it accepts void *s in C, into which you can technically stuff any integer which fits in a pointer, even if the pointer value is nonsense...) Swift expects CFArrays and NSArrays to only ever contain valid objects and pointers, and it treats the return values as such — this is why accessing via NSArray also fails (Swift expects an object but the value isn't an object, so it can't be accessed like a pointer, or retained, or any of the things that the runtime might expect to do).
Your second code snippet is closer to how you'll need to access the value: CFArrayGetValueAtIndex appears to return a pointer, but the value you're getting back isn't a real pointer — it's actually an integer stored in the array directly, masquerading as a pointer.
The equivalent to the Obj-C example from the CGFontCopyTableTags docs of
tag = (uint32_t)(uintptr_t)CFArrayGetValue(table, k);
would be
let tag = unsafeBitCast(CFArrayGetValueAtIndex(cfTags, 0), to: UInt.self)
(Note that you need to cast to UInt and not UInt32 because unsafeBitCast requires that the input value and the output type have the same alignment.)
In my simulator, I'm seeing a tag with a value of 1196643650 (0x47535542), which definitely isn't a valid pointer (but I don't otherwise have domain knowledge to validate whether this is the tag you're expecting).

Trouble using NSDictionary convenience init in Swift

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 {
...
}

How do I get a server timestamp from Firebase's iOS API?

I have an iOS app that uses Firebase and currently has a few dictionaries with keys that are NSDate objects. The obvious issue with this is that NSDate draws from the device's system time, which is not universal.
With that, what's the best way to get a server timestamp (similar to Firebase.ServerValue.TIMESTAMP for the Web API) using Firebase's iOS API so that I can sort my dictionary keys chronologically?
I'm also aware of the chronological nature of IDs generated by childByAutoID, but I can't figure out the proper way to sort these in code. While they may be returned in chronological order, any time something like allKeys is called on them, the order goes out the window.
Any help with this issue would be greatly appreciated!
Update: In Firebase 3.0 + Swift, you can use
FIRServerValue.timestamp(). In Objective-C this is [FIRServerValue timestamp].
In Swift, you can now use FirebaseServerValue.timestamp() with Firebase 2.0.3+ (before 3.0).
The equivalent for Firebase.ServerValue.TIMESTAMP in iOS is kFirebaseServerValueTimestamp. Right now, this only works for Objective-C and not Swift.
In Swift, you can create your own global timestamp with
let kFirebaseServerValueTimestamp = [".sv":"timestamp"]
and then you'll be able to use kFirebaseServerValueTimestamp in the same way.
But you can only use this as the value or priority of a node. You won't be able to set it as the key name (although, I don't believe you could in the Web API either).
In general, calling allKeys on a dictionary does not guarantee order. But if you're using childByAutoID at a node, you can get back the right order by ordering the NSArray returned by allKeys lexicographically. Something like this would work:
[ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
NSDictionary *value = snapshot.value;
NSLog(#"Unsorted allKeys: %#", value.allKeys);
NSArray *sortedAllKeys = [value.allKeys sortedArrayUsingSelector:#selector(compare:)];
NSLog(#"Sorted allKeys: %#", sortedArray);
}];
This is similar to sorting an NSArray alphabetically, but when sorting the auto-generated IDs, you do not want localized or case insensitive sort, so you use compare: instead of localizedCaseInsensitiveCompare:
Caveat: Seems like the timestamp is added AFTER your object is persisted in Firebase. This means that if you have a .Value event listener set up on the location your object is persisted to, it will be triggered TWICE. Once for the initial object being stored in the location, and again for the timestamp being added. Struggled with this issue for days :(
Helpful information for anyone else who can't figure out why their event listeners are triggering twice/multiple times!
As of Firebase 4.0 you can use ServerValue.timestamp()
for example:
let ref = Database.database().reference().child("userExample")
let values = ["fullName": "Joe Bloggs", "timestamp": ServerValue.timestamp()] as [String : Any]
ref.updateChildValues(values) { (err, ref) in
if let err = err {
print("failed to upload user data", err)
return
}
}
You can get Time Stamp using FIRServerValue.timestamp().
But, Because of FIRServerValue.timestamp() listener is called two times. Listener will be called two times.

My NSDictionary somehow has multiple values for one key

I have been attempting to debug a issue with my code, and just came upon an odd phenomenon. Found this in the debug area when a breakpoint was triggered:
Am I correct in observing that there are multiple values for this key: #"6898173"??
What are possible causes of this? I do not set those key-value pairs using the string literal, but by getting a substring of a string retrieved and decoded from a GKSession transmission.
I still have this up in the debug area in xcode, incase theres anything else there that might help.
EDIT:
By request, here is the code that would have created one of the two strings (another was created at an earlier time):
[carForPeerID setObject:[[MultiScreenRacerCarView alloc] initWithImage:[UIImage imageNamed:#"simple-travel-car-top_view"] trackNumber:[[[NSString stringWithUTF8String:data.bytes] substringWithRange:range] intValue]] forKey:[[NSString stringWithUTF8String:[data bytes]] substringFromIndex:9]];
The string in data might look something like this:
car00.0146898173
EDIT:
Code that sends the data:
[self.currentSession sendData:[[NSString stringWithFormat:#"car%i%#%#", [(MultiScreenRacerCarView *)[carForPeerID objectForKey:peerID] trackNumber], speed, [(MultiScreenRacerCarView *)[carForPeerID objectForKey:peerID] owner]] dataUsingEncoding:NSUTF8StringEncoding] toPeers:#[(NSString *)[peersInOrder objectAtIndex:(self.myOrderNumber + 1)]] withDataMode:GKSendDataReliable error:nil];
Sorry its hard to read. Its only one line.
What you're seeing is a debugger "feechure". When you have a mutable dictionary and modify it, the debugger may not show you the correct view of the object.
To reliably display the contents of an NSMutableArray or NSMutableDictionary, switch to the console and type po carForPeerID.

Does lexicontext ios sdk provide autocomplete in utf8

Does lexicontext ios sdk provide autocomplete in utf8?
Not out of the box I'm afraid.
But I think it shouldn't be too difficult to add this functionality using the dictionary's search API.
// first obtain a reference to the dictionary
Lexicontext *dictionary = [Lexicontext sharedDictionary];
// later, given a prefix that the user typed, do something like:
NSDictionary *words = [dictionary wordsWithPrefix:prefix];
You should probably start offering to auto-complete for prefixes of at least 2 characters.
Otherwise, you may get very long results. For example, the result for [dictionary wordsWithPrefix:#"a"] returns a result of about 1800 Adjectives, 400 Adverbs, 500 Verbs... and will probably be too slow :)

Resources