Modeling structured data

Say you have a bunch of items in your app, and each item has properties. Leanplum gives you the flexibility to model one property of the object at a time, one object at a time, or the entire structure at once, depending on how you'd like to set up your code.

For example, if you want to create a structure like this:

Powerups: {
    Speed: {
        Price: 10,
        Duration: 5,
        OrderInStore: 0
    },
    Power: {
        Price: 15,
        Duration: 5,
        OrderInStore: 1
    }
}

You could model each individual field like so:

// Names or groups with a '.' are grouped automatically.

var speedPrice = Var(name: "Powerups.Speed.Price", integer:10)
var speedDuration = Var(name: "Powerups.Speed.Duration", integer:5)
var speedOrder = Var(name: "Powerups.Speed.OrderInStore", integer:0)

var powerPrice = Var(name: "Powerups.Power.Price", integer:15)
var powerDuration = Var(name: "Powerups.Power.Duration", integer:5)
var powerOrder = Var(name: "Powerups.Power.OrderInStore", integer:1)
// Names or groups with an '_' are grouped automatically.
DEFINE_VAR_FLOAT(Powerups_Speed_Price, 10);
DEFINE_VAR_FLOAT(Powerups_Speed_Duration, 5);
DEFINE_VAR_FLOAT(Powerups_Speed_OrderInStore, 0);

DEFINE_VAR_FLOAT(Powerups_Power_Price, 15);
DEFINE_VAR_FLOAT(Powerups_Power_Duration, 5);
DEFINE_VAR_FLOAT(Powerups_Power_OrderInStore, 1);
// Names or groups with a '.' are grouped automatically.
@Variable(name="Powerups.Speed.Price") double speedPrice = 10;
@Variable(name="Powerups.Speed.Duration") double speedDuration = 5;
@Variable(name="Powerups.Speed.OrderInStore") int speedOrder = 0;

@Variable(name="Powerups.Power.Price") double powerPrice = 15;
@Variable(name="Powerups.Power.Duration") double powerDuration = 5;
@Variable(name="Powerups.Power.OrderInStore") int powerOrder = 1;
// Names with a '.' are grouped automatically.
Var<int> speedPrice = Var<int>.Define("Powerups.Speed.Price", 10);
Var<int> speedDuration = Var<int>.Define("Powerups.Speed.Duration", 5);
Var<int> speedOrder = Var<int>.Define("Powerups.Speed.Order in Store", 0);

Var<int> powerPrice = Var<int>.Define("Powerups.Power.Price", 15);
Var<int> powerDuration = Var<int>.Define("Powerups.Power.Duration", 5);
Var<int> powerOrder = Var<int>.Define("Powerups.Power.Order in Store", 1);

Or you could model each object:

var powerUpsSpeed = Var(name: "Powerups.Speed", dictionary: [
  "Price": 10,
  "Duration": 5,
  "OrderInStore": 0
])
var powerUpsPower = Var(name: "Powerups.Power", dictionary: [
  "Price": 15,
  "Duration": 5,
  "OrderInStore": 1
])
DEFINE_VAR_DICTIONARY_WITH_OBJECTS_AND_KEYS(Powerups_Speed, @10, @"Price", @5, @"Duration", @0, @"OrderInStore", nil);
DEFINE_VAR_DICTIONARY_WITH_OBJECTS_AND_KEYS(Powerups_Power, @15, @"Price", @5, @"Duration", @1, @"OrderInStore", nil);
// Using the Google Guava library for brevity.
@Variable(group="Powerups") Map<String, Object> speed =
    ImmutableMap.of("Price", 10.0, "Duration", 5.0, "OrderInStore", 0);
@Variable(group="Powerups") Map<String, Object> duration =
    ImmutableMap.of("Price", 15.0, "Duration", 5.0, "OrderInStore", 1);
Var<Dictionary<string, object>> speed = Var<Dictionary<string, object>>.Define(
    "Powerups.Speed", new Dictionary<string, object>>() {
        { "Price", 10 },
        { "Duration", 5 },
        { "OrderInStore", 0 } });
Var<Dictionary<string, object>> power = Var<Dictionary<string, object>>.Define(
    "Powerups.Power", new Dictionary<string, object>>() {
        { "Price", 15 },
        { "Duration", 5 },
        { "OrderInStore", 1 } });

Or the entire structure:

var powerups = Var(name: "Powerups", dictionary: [
  "Speed": [
    "Price": 10,
    "Duration": 5,
    "OrderInStore": 0
  ],
  "Power": [
    "Price": 15,
    "Duration": 5,
    "OrderInStore": 1
  ]
])
LPVar* powerups;
static void __attribute__((constructor)) initObjects() {
    @autoreleasepool {
        powerups = [LPVar define:@"Powerups" withDictionary:@{
            @"Speed": @{@"Price" : @10, @"Duration": @5, @"OrderInStore": 0},
            @"Power": @{@"Price" : @15, @"Duration": @5, @"OrderInStore": 1}
        }];
    }
}
@Variable
public static Map<String, Map<String, Object>> powerups = new HashMap<String, Map<String, Object>>() {
    {   put("Speed", new HashMap<String, Object>() {
            { put("Price", 10.0); put("Duration", 5.0); put("OrderInStore", "OrderInStore"); }
        });
        put("Power", new HashMap<String, Object>() {
            { put("Price", 15.0); put("Duration", 5.0); put("OrderInStore", "OrderInStore"); }
        });
    }};
Var<Dictionary<string, Dictionary<string, object>>> speed = Var<Dictionary<string, Dictionary<string, object>>>.Define(
    "Powerups", new Dictionary<string, Dictionary<string, object>>() {
        { "Speed", new Dictionary<string, object>() {
            { "Price", 10 },
            { "Duration", 5 },
            { "Order in Store", 0 } } },
        { "Power", new Dictionary<string, object>() {
            { "Price", 15 },
            { "Duration", 5 },
            { "Order in Store", 1 } } }
        });

All of the above declarations will show up identically on our dashboard. Leanplum understands to group variables that use underscores (ObjC) or dots (Swift) in their names. The last method is convenient because instead of specifying the JSON data in code, you could potentially load it from a file and then convert it to a dictionary.

When the variable values are ready, you can get different slices of the variables using objectForKey[Path].

Using objectForKey:

NSDictionary* allPowerups = [powerups objectForKeyPath:nil];
NSDictionary* speedPowerup = [powerups objectForKey:@"Speed"];
float speedPrice = [[powerups objectForKeyPath:@"Speed", @"Price", nil] floatValue];
Map<String, Object> allPowerups = powerups.objectForKeyPath();
Map<String, Object> speedPowerup = powerups.objectForKeyPath("Speed");
float speedPrice = powerups.objectForKeyPath("Speed", "Price");

Variable Groups with Files

If you have defined groups with nested files from the Dashboard, those files will not be automatically downloaded. However, you can still access the file name, which is unique, and download the file using the SDK. The same logic can be used for downloading files from message's Advanced Data.
Example structure:

Use the following code to download the image or retrieve it from the device if already downloaded:

func getImageFile(_ fileName: String) -> UIImage? {
    let fl = LPFileManager.fileValue(fileName, withDefaultValue: fileName)
    if let file = fl {
        let img = UIImage.init(contentsOfFile: file)
        return img
    }
    return nil
}

let weapons = Var(name: "weapons", dictionary: [:])
let icon = weapons.object(forKeyPathComponents: ["daggers", "magic_dagger", "icon"]) as? String
        
if let fileName = icon {
// try to download the image
// if the image is already downloaded it will not trigger the onComplete callback
    let willDownload = LPFileManager.maybeDownloadFile(fileName, defaultValue:
"", onComplete: {
        self.img = self.getImageFile(fileName)
    })
    if !willDownload {
        self.img = self.getImageFile(fileName)
    }
}
/**
 * Get the image from the file system
 */
- (UIImage*)getImageFile:(NSString*)fileName {
    NSString *fileValue = [LPFileManager fileValue:fileName withDefaultValue:fileName];
    if (fileValue) {
        UIImage *img = [UIImage imageWithContentsOfFile:fileValue];
        return img;
    }
    return nil;
}

if (fileName){
        // Try to download the image
        BOOL willDownload = [LPFileManager maybeDownloadFile:fileName defaultValue:nil onComplete:^{
            UIImage *img = [self getImageFile:fileName];
        }];
        
        // If the image is already downloaded it will not trigger the onComplete callback
        if (!willDownload) {
            UIImage *img = [self getImageFile:fileName];
        }
    }
Var<HashMap<String, Map<String, Object>>> weapons = Var.define("weapons", new HashMap<String, Map<String, Object>>());

String icon = (String) weapons.objectForKeyPath("daggers", "magic_dagger", "icon");
    if (icon != null) {
        FileManager.DownloadFileResult result = FileManager.maybeDownloadFile(false, icon, null, null,
                    new Runnable() {
                        @Override
                        public void run() {
                            setImage(icon);
                        }
            });
        if (result != FileManager.DownloadFileResult.DOWNLOADING) {
            setImage(icon);
        }
}
        
private Uri getImageFileUri(String fileName) {
        java.io.File imgFile = new java.io.File(FileManager.fileValue(fileName));
        return Uri.fromFile(imgFile);
}

private void setImage(String fileName) {
        Uri imageUri = getImageFileUri(fileName);
        ImageView myImage = getView().findViewById(R.id.varFileImage);
        myImage.setImageURI(imageUri);
}

Result: