Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Tyler-V opened this issue Mar 20, 2020 · 12 comments
Closed

Comments

@Tyler-V
Copy link

Tyler-V commented Mar 20, 2020

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.

@Tyler-V Tyler-V changed the title Unable to properly marshall data in iOS when using interop.Pointer / NSArray / NSDictionary Unable to properly marshall data in iOS when using NSArray / NSDictionary Mar 20, 2020
@facetious
Copy link
Contributor

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,
  ...
};

@NickIliev
Copy link

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

@Tyler-V
Copy link
Author

Tyler-V commented Mar 20, 2020

@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.

@Tyler-V
Copy link
Author

Tyler-V commented Mar 21, 2020

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 NickIliev transferred this issue from NativeScript/NativeScript Mar 23, 2020
@mbektchiev
Copy link
Contributor

mbektchiev commented Mar 23, 2020

@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().

@mbektchiev
Copy link
Contributor

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,
    });

@mbektchiev
Copy link
Contributor

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)

@mbektchiev
Copy link
Contributor

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

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

@Tyler-V
Copy link
Author

Tyler-V commented Mar 23, 2020

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

@Tyler-V
Copy link
Author

Tyler-V commented Mar 23, 2020

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?

@mbektchiev
Copy link
Contributor

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.

@Tyler-V
Copy link
Author

Tyler-V commented Mar 24, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants