NativeScript / ios-jsc

NativeScript for iOS using JavaScriptCore

Home Page:http://docs.nativescript.org/runtimes/ios

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unable to properly marshall data in iOS when using NSArray / NSDictionary

Tyler-V opened this issue · comments

Environment
Provide version numbers for the following components (information can be retrieved by running tns info in your project folder or by inspecting the package.json of the project):

  • CLI: 6.4.0
  • Cross-platform modules:
  • Android Runtime: 6.4.1
  • iOS Runtime: 6.4.2
  • Plugin(s):
    implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:8.6.2'
    pod 'Mapbox-iOS-SDK', '~> 5.6.1'

Describe the bug
When attempting to call methods in the mapbox sdk, I am unable to marshall the data into the correct format shown in the examples depicted in Mapbox's documentation for iOS.

For creating a NSDictionary of [NSNumber: UIColor], in this example provided:

let colorDictionary: [NSNumber: UIColor] = [
0.0: .clear,
0.01: .white,
0.15: UIColor(red: 0.19, green: 0.30, blue: 0.80, alpha: 1.0),
0.5: UIColor(red: 0.73, green: 0.23, blue: 0.25, alpha: 1.0),
1: .yellow
]
heatmapLayer.heatmapColor = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($heatmapDensity, 'linear', nil, %@)", colorDictionary)

To create this in my plugin, I have to:

export const expressionStops = (expression: (number | MapboxColor)[][]) => {
  const stops = [];
  const values = [];
  for (let i = 0; i < expression.length; i++) {
    stops.push(marshall(expression[i][0]));
    values.push(marshall(expression[i][1]));
  }
  let nsDictionary = new (NSDictionary as any)(values, stops);
  let nsArray = NSArray.arrayWithArray([nsDictionary]);
  return nsArray;
};

  setHeatmapColor(layer: MGLHeatmapStyleLayer, stops: (number | MapboxColor)[][]) {
    layer.heatmapColor = NSExpression.expressionWithFormatArgumentArray(
      "mgl_interpolate:withCurveType:parameters:stops:($heatmapDensity, 'linear', nil, %@)",
      expressionStops(stops)
    );
  }

Note, the expected input is [zoomLevel, color ...]

[
0.0: .clear,
0.01: .white,
0.15: UIColor(red: 0.19, green: 0.30, blue: 0.80, alpha: 1.0),
0.5: UIColor(red: 0.73, green: 0.23, blue: 0.25, alpha: 1.0),
1: .yellow
]

What I have to do to get this to work is to recreate it as:

  let nsDictionary = new (NSDictionary as any)([color1, color2, color3, ...], [1, 2, 3, ...]);
  let nsArray = NSArray.arrayWithArray([nsDictionary]);

When I call the same plugin method in Android:

heatmapColor(
interpolate(
linear(), heatmapDensity(),
literal(0), rgba(33, 102, 172, 0),
literal(0.2), rgb(103, 169, 207),
literal(0.4), rgb(209, 229, 240),
literal(0.6), rgb(253, 219, 199),
literal(0.8), rgb(239, 138, 98),
literal(1), rgb(178, 24, 43)
)
),

This simply works as expected and in-line with the plugin documentation,

export const expressionStops = (stops: (number | MapboxColor)[][]) => {
  const array = [];
  for (let input of stops) {
    const _stop = marshall(input[0]);
    const _value = marshall(input[1]);
    array.push(stop(_stop, _value));
  }
  return array;
};

  setHeatmapColor(layer: any, stops: (number | MapboxColor)[][]) {
    const _heatmapColor = heatmapColor(interpolate(linear(), heatmapDensity(), expressionStops(stops)));
    layer.setProperties([_heatmapColor]);
  }

To Reproduce
Try to create an NSDictionary of type [NSNumber: UIColor], the values provided are backwards.

Expected behavior
I should be able to create an NSDictionary where the key/value is properly mapped and doesn't require two arrays in reverse order which contain just keys and just values.

Sample project
I would be more than happy to invite you into my plugin's project which has a branch already setup and a demo-angular app which will let you bootstrap and debug this issue with minimal effort to see for yourself. Please let me know otherwise this should be easily reproducible given the steps listed above.

It looks like you're converting the Swift example you were provided into TypeScript, but have accidentally misinterpreted it. If you switch the example you linked into Objective-C, it might be clearer what the type definition should be in order to get this marshalling to work:

const colorDictionary: { [key: NSNumber]: UIColor } = {
  0.0: UIColor.clear,
  0.01: UIColor.white,
  ...
};

@Tyler-V also check this thread for creating an NSDictionary

@NickIliev thanks for the resource.

I am using the 'only' working way known to create a NSDictionary as indicated by this comment in that thread you linked above. The other suggestions either throw type errors or crash at runtime.

Unfortunately, if we want to represent the following as a NSDictionary...

{
    AVFormatIDKey: '.mp3',
    AVEncoderBitRateKey: 16,
    AVSampleRateKey: 44100,
    AVNumberOfChannelsKey: 2,
}

We have to use the following:

new (NSDictionary as any)(['.mp3', 16, 44100.0, 2], ['AVFormatIDKey', 'AVEncoderBitRateKey', 'AVSampleRateKey','AVNumberOfChannelsKey']);

That can't be right? But it works.

Thanks @facetious,

Unfortunately, that creates an object and when passed into that method it crashes as I suspect it is expecting a fully marshalled NSDictionary instantiated prior to execution.

Building off my comment above, colorDictionary2 (below) is the only thing that I've found that works. I just find it odd that you have to build it backwards, this is going to mess a lot of people up.

    const colorDictionary1: { [key: number]: UIColor } = {
      0.0: UIColor.blueColor,
      0.25: UIColor.brownColor,
      0.5: UIColor.redColor,
      0.75: UIColor.greenColor,
      1: UIColor.orangeColor,
    };

    const colorDictionary2 = [
      new (NSDictionary as any)([UIColor.blueColor, UIColor.brownColor, UIColor.redColor, UIColor.greenColor, UIColor.orangeColor], [0.0, 0.25, 0.5, 0.75, 1]),
    ];

    layer.heatmapColor = NSExpression.expressionWithFormatArgumentArray(
      "mgl_interpolate:withCurveType:parameters:stops:($heatmapDensity, 'linear', nil, %@)",
      colorDictionary2
    );

@NickIliev thanks for the resource.

I am using the 'only' working way known to create a NSDictionary as indicated by this comment in that thread you linked above. The other suggestions either throw type errors or crash at runtime.

Unfortunately, if we want to represent the following as a NSDictionary...

{
    AVFormatIDKey: '.mp3',
    AVEncoderBitRateKey: 16,
    AVSampleRateKey: 44100,
    AVNumberOfChannelsKey: 2,
}

We have to use the following:

new (NSDictionary as any)(['.mp3', 16, 44100.0, 2], ['AVFormatIDKey', 'AVEncoderBitRateKey', 'AVSampleRateKey','AVNumberOfChannelsKey']);

That can't be right? But it works.

This is actually another way of writing NSDictionary.alloc().initWithObjectsForKeys([<objects>], [<keys>]) and is perfectly fine way of constructing it. You can also use NSDictionary.dictionaryWithObjectsForKeys().

I guess that the most natural syntax that you're looking for is this:

NSDictionary.dictionaryWithDictionary({
      0.0: UIColor.blueColor,
      0.25: UIColor.brownColor,
      0.5: UIColor.redColor,
      0.75: UIColor.greenColor,
      1: UIColor.orangeColor,
    });

And actually specifying an object directly also works as expected according to my tests:

const colorDictionary1 = {
      0.0: UIColor.blueColor,
      0.25: UIColor.brownColor,
      0.5: UIColor.redColor,
      0.75: UIColor.greenColor,
      1: UIColor.orangeColor,
    };

const heatmapColor = NSExpression.expressionWithFormatArgumentArray(
      "mgl_interpolate:withCurveType:parameters:stops:($heatmapDensity, 'linear', nil, %@)",
      [colorDictionary1]);

I guess that you've forgotten to wrap the argument inside an array as is required by expressionWithFormatArgumentArray. This should throw an Objective-C exception and if you've tested it on a simulator you've hit a long standing and quite unpleasant known issue of {N} iOS (#1044)

The actual exception I got in this case (forgotten array) was:

Error: Insufficient arguments for conversion characters specified in format string.
expressionWithFormatArgumentArray([native code])

While it does let me create a heatmapColor with the last example provided @mbektchiev, it does not like the input provided when applying the heatmapColor expression.

image

The other input provided may be what it is looking for since it needs a dictionary but I am getting typing errors trying to instantiate it.

image

Similar to the last issue you helped me with, and thank you for that again @mbektchiev, I need to construct this object dynamically since my input is of type

setHeatmapColor(layer: MGLHeatmapStyleLayer, stops: (number | MapboxColor)[][]) {

I need to be able to iterate over the stops and construct a dictionary dynamically based on that input of type stops: (number | any)[][], I will have to make a similar helper function to the last issue to achieve this.

What is the best way to go about this?

I would suggest you try something like this:

function toHeatmapDictionary(a: object): NSDictionary<NSNumber, UIColor> {
    const keys = Object.getOwnPropertyNames(a);
    let dict = new NSMutableDictionary<NSNumber, UIColor>({ capacity: keys.length });

    for (let key of keys) {
        dict.setObjectForKey(<any>a[key], <NSNumber><unknown>(NSString.stringWithString(key).intValue));
    }

    return dict;
}

const colorDictionary1 = {
    0.0: UIColor.blueColor,
    0.25: UIColor.brownColor,
    0.5: UIColor.redColor,
    0.75: UIColor.greenColor,
    1: UIColor.orangeColor,
  };

const nsDict = toHeatmapDictionary(colorDictionary1);
const heatmapColor = NSExpression.expressionWithFormatArgumentArray(
    "mgl_interpolate:withCurveType:parameters:stops:($heatmapDensity, 'linear', nil, %@)",
    [nsDict]);

The whole gymnastics aims to convert the NSDictionary's keys to NSNumber objects. By default, the runtime marshals object keys as strings and this seems to be the reason for the error you've been receiving with the first attempt.

You're an absolute wizard @mbektchiev can't thank you enough, that worked great.