The problem this doc solves
The framework supports three distinct patterns for “user taps something, a form opens with values pre-filled from the underlying data row.” They publish different keys on the row, target different iOS resolvers, and are easy to mix up. This doc lays them side by side so plugin authors can pick the right one, and so future iOS work knows where each path lands.
It also documents an open gap in pattern C (content_type: 'form') — the prefill values aren’t currently resolved on the iOS side. Tracked at the bottom.
Vocabulary
- Row — the dict a content class adds to its
get_data_for_app()return array. One row per list item / card / cell - Form payload — the cached form definition in
$local_app_form_array, shared across all rows that reference it - Form index — 1-based position in
$local_app_form_array, obtained viaapply_filters( 'mam_gf_get_form_from_cache', 0, $form_id ) - Prefill values — the key-value pairs the form should be populated with when it opens for this specific row. Keyed by
populate_with_keyslugs
The form payload is shared. Prefill is per-row. Every pattern below is a different mechanism for the per-row part.
Pattern A — Tabbar button (open_form_array)
The tabbar button across the bottom opens a sheet containing one or more forms. Each form gets its own prefill dict. Used by mam-geodirectory for Edit Staff and Edit Events.
Row / button keys
$tabbar_button['icon'] = 'white_person';
$tabbar_button['action'] = 'open_form_array';
$tabbar_button['source'] = [
[
'custom_form' => $form_index, // 1-based index into $local_app_form_array
'custom_form_values' => $form_values, // [slug => value] keyed by populate_with_key
'custom_form_icon' => 'white_person',
'custom_form_name' => $staff->display_name,
'no_persist' => 'yes',
// optional:
'custom_form_excluded_fields' => [ '<field_id>', ... ],
],
// more form entries...
];
iOS resolver site
tab_bar_manager button handler, open_form_array action branch. Walks source and presents each as a card → opens form on tap, passing custom_form_values as the populate dict.
Known-working reference
mam-suite/mam-geodirectory/includes/mam_gd_tab_bar_buttons.php lines 361-449 (Edit Staff), 720-900 (Edit Events).
Pattern B — custom_form_array on a row
Each row publishes one or more form entries inline. The mobile list renders each as an icon/button. Used by mam-qrlife for the Edit button on profile cards.
Row keys
$row['custom_form_array'][] = [
'custom_form' => $form_index,
'custom_form_values' => $populate_values, // [slug => value] keyed by populate_with_key
'custom_form_icon' => 'black_edit.png',
'custom_form_name' => 'Edit',
'no_persist' => 'yes',
];
$row['edit_form_id'] = $form_index; // optional, for "row has primary edit form"
$row['layout_button_3_key'] = $form_index; // optional, layout-button binding
$row['layout_button_3_key_type'] = 'form';
iOS resolver site
Detail-view “custom forms” rendering. Iterates custom_form_array; for each entry, opens the form referenced by custom_form and passes custom_form_values as the populate dict.
Known-working reference
mam-use-case/active/mam-qrlife/includes/content-class-profiles.php lines 510-525.
Pattern C — content_type: 'form' row tap
The whole row is tap-to-open-form. No button, no icon — the row itself is the affordance. Used by mam-aquaman-boat-booking for the Anchor Times list.
Row keys
$row['content_type'] = 'form';
$row['form_id'] = (string) $form_index;
$row['layout_button_3_key'] = (string) $form_index;
$row['layout_button_3_key_type'] = 'form';
// Prefill values published at top level (current convention, see §gap below)
$row['anchor_time'] = '09:00';
$row['capt_one'] = '425';
$row['capt_two'] = '423';
$row['booking_date'] = '2026-05-29';
iOS resolver path
The row tap flows through three hops:
ListingSelectionRouter.mopenDetailListingWIthID:…fromVC:— looks up the tapped row by ID; whencontent_type == "form"it calls[VCLauncher launchVCWithType:@"form" … selectedItem:item_info …], passing the row dict as theselectedItemargument.VCLauncher.mlaunchVCWithType:, the[vc_type isEqualToString:@"form"]branch (around line 169) — builds avc_form_manager, setswf_formfromoptions[@"wf_form_id"], and now also setscontroller.selectedItem = selectedItemso the form-manager’s existing populate fallback can read row keys directly. (Previously onlyselectedDirectorywas set, leaving theselectedItemproperty nil.)vc_form_manager.mpicks upselectedItem[key]for each field withpopulate_with_key.
// VCLauncher.m, form branch
vc_form_manager *controller = [[vc_form_manager alloc] init];
controller.button_id = currentItem;
controller.back_to_vc_id = @"main_menu";
controller.form_source = formSource;
controller.selectedDirectory = selectedItem;
controller.selectedItem = selectedItem; // ← the fix
controller.subFormParentId = [NSString stringWithFormat:@"%ld", (long)currentItem];
controller.fromLeftMenu = isFromLeftMenu;
NSInteger wf_form_id = [options[@"wf_form_id"] integerValue];
if (wf_form_id > 0)
controller.wf_form = [general_functions getFormToOpen:wf_form_id];
Note: vc_directory_listing.m performForm: contains a [vc_type isEqualToString:@"form"] branch too, but no caller invokes performForm: with that vc_type — the row-tap path bypasses it entirely via ListingSelectionRouter → VCLauncher.
Side-by-side
| Pattern A (tabbar) | Pattern B (custom_form_array) |
Pattern C (content_type: 'form') |
|
|---|---|---|---|
| Tap target | Tabbar button | Row’s edit icon | Whole row |
| Form index lives at | tabbar.source[N].custom_form |
row.custom_form_array[N].custom_form |
row.form_id |
| Prefill values live at | tabbar.source[N].custom_form_values |
row.custom_form_array[N].custom_form_values |
Top-level row keys |
| iOS resolver | Tabbar handler (populateWithKeyValues) |
Detail-view custom forms (populateWithKeyValues) |
vc_form_manager.selectedItem fallback |
| Status | ✅ Working in geodirectory | ✅ Working in qrlife | ✅ Working in aquaman-boat-booking (as of 2026-05-20) |
Resolved: the content_type: 'form' prefill path
Root cause
vc_form_manager.m:1378-1385 already had the right resolver for Pattern C, sitting unused:
}else if(field[@"populate_with_key"]) {
NSString *key = field[@"populate_with_key"];
if (populateWithKeyValues && populateWithKeyValues[key]) {
storedValue = populateWithKeyValues[key]; // Pattern A/B path
} else if (!populateWithKeyValues && selectedItem[key]) {
storedValue = selectedItem[key]; // ← Pattern C path
}
}
The form-manager exposes @property (copy, nonatomic) NSDictionary *selectedItem; in vc_form_manager.h:92. When opened via Pattern A/B, populateWithKeyValues is set and the first branch runs. When opened via Pattern C, populateWithKeyValues is nil and the selectedItem[key] fallback was designed to handle it — but VCLauncher.m‘s form branch was only assigning the row to selectedDirectory, leaving selectedItem nil. The fallback was never reachable.
Fix #1 — VCLauncher.m launchVCWithType: form branch
Added one line after the existing controller.selectedDirectory = selectedItem; assignment:
controller.selectedItem = selectedItem;
That’s the entire structural change. No PHP changes needed — content classes were already publishing the top-level row keys correctly. The resolver picks them up via the selectedItem[key] branch.
Fix #2 — DropdownFieldView.m initial label display (latent)
Investigation surfaced a secondary bug: DropdownFieldView.m:63-65 set the closed-dropdown text directly from storedValue. For values that are post IDs ('425'), users saw the raw ID until tapping the dropdown — at which point the picker’s onSelect callback rewrites the text to the label.
The bug was masked in Patterns A/B because geodirectory’s storedValues happen to be label-like strings ('Manager', 'Staff'). It became visible in Pattern C with post-ID values.
Fix: walk _choices looking for a value match and use the matching choice’s text as the display value. Falls back to the raw storedValue when no choice matches (preserves the existing behavior for free-text use cases or label-equals-value scenarios).
NSString *displayText = storedValue ?: @"";
if (storedValue.length > 0) {
for (NSDictionary *choice in _choices) {
if ([choice[@"value"] isEqual:storedValue]) {
displayText = choice[@"text"] ?: storedValue;
break;
}
}
}
self.textFieldView.text = displayText;
This improves all three patterns wherever a dropdown’s value and label differ.
Acceptance test (verified)
Open the aquaman-boat-booking Anchor Times list, tap May 29, 2026:
- ✅ Anchor Time field shows “9:00 AM” pre-selected
- ✅ Basin Cruiser shows “Captain Phillips” pre-selected
- ✅ Group Tubing shows “Captain Ron” pre-selected
- ✅ Ski Boat One shows “Captain Phillips”, Ski Boat Two shows “Captain Ron”
- ✅
booking_datehidden field carries2026-05-29 - ✅ Submitting the form lands at
mam_for_gravity_forms_form_result_form_<ID>with the slug-keyed values
Why this matters for future Pattern C plugins
Plugins using content_type: 'form' row taps now have a working prefill path with zero ceremony. The contract is:
- Publish populate values as top-level keys on the row, matching the slugs declared in
mam_gf_get_form_settings(orpopulate_with_keyon the field definitions) - Set
content_type = 'form'andform_id = <cache_index> - The form opens with each field whose
populate_with_keymatches a row key pre-filled to that row’s value
No custom_form_array wrapping needed for tap-the-row use cases — that pattern stays reserved for the edit-icon / tabbar flows.
See also
- End-to-end cookbook — §5a covers the three patterns in less depth
- Hook reference —
mam_populate_gravity_form,mam_gf_get_form_from_cache - Forms manager overview — system-wide context
Metadata
| Field | Value |
|---|---|
| Article type | Reference / Gap Analysis |
| Plugin slug | mam-main |
| Applies to plugin version | 2.1.11+ |
| Category | Forms Manager |
| Audience | PHP developer + iOS developer |
| Last verified | 2026-05-20 |
| Related | End-to-end cookbook, Hook reference |
