While I am adding them from a plugin, I ran into a similar issue as you, and like you, I didn't like the "recreate the layout from scratch" method the linked answer used. I know this question is old, but it seems the most apt place to put my solution to this issue and contribute to more code samples.
After some experimenting, I got a solution that works (whether it's "the Craft way," I'm not totally sure, and I'm sure some of it could be cleaned up, but it's not like there's a lot of documentation on that front, as we've both found).
This code creates the fields, a field group, and a tab, then adds the tab and fields to the User Layout:
// Create the field group
$groupModel = new FieldGroupModel();
$groupModel->name = 'CustomGroupName';
craft()->fields->saveGroup($groupModel);
// I haven't yet found a way to get the group ID without looping through all of the groups. saveGroup() returns a boolean and doesn't appear to update the id attribute of the original model instance.
$groups = craft()->fields->getAllGroups();
foreach($groups as $group) {
if($group->name != 'CustomGroupName') {
continue;
}
$groupModel = $group;
}
// Create the tab
$tabModel = new FieldLayoutTabModel();
// Get the desired layout (in this case, the User layout) and use the tab's setLayout to link the two
$layout = craft()->fields->getLayoutByType(ElementType::User);
$tabModel->setLayout($layout);
$tabModel->name = 'CustomTabName';
// Create the fields. I have a protected class member that contains the fields in a 'fieldName' => 'FieldName' format.
foreach($this->fields as $field => $name) {
$fieldModel = new FieldModel();
$fieldModel->groupId = $groupModel->id;
$fieldModel->name = Craft::t($name);
$fieldModel->handle = $field;
$fieldModel->translatable = false;
$fieldModel->type = 'PlainText';
craft()->fields->saveField($fieldModel);
}
// Get all of the group's fields. This could also be done by saving an array of the fields in the loop above, but this seems a bit cleaner to me, and since we already have the group object, saves us an object/variable declaration.
$fields = $groupModel->getFields();
// Set the tab's fields. This gives us a custom grouping in the layout editor.
$tabModel->setFields($fields);
// Get the existing tabs (and, by extension, fields), since we don't want to nuke the layout, just add to it. I couldn't find any sort of "addTab" type function, so this is how we have to do it.
$layoutTabs = $layout->getTabs();
// getTabs() returns an array, so simply add our new tab to the array.
$layoutTabs[] = $tabModel;
// Now, set the tabs back in the layout.
$layout->setTabs($layoutTabs);
The reversal is exactly that--a reversal:
$group = null;
// Delete the fields
foreach($this->fields as $field => $name) {
$fieldModel = craft()->fields->getFieldByHandle($field);
if($fieldModel) {
// Grab the group from one of the fields. Presumably (in my case), they're all in the same group. This could be refined for a less ham-fisted approach. Unfortunately, it's either this, or doing the loop-through-all-groups method we did in the create section, that I've found.
$group = $fieldModel->group;
craft()->fields->deleteField($fieldModel);
}
}
// Delete the field group
if($group) {
$groupModel = craft()->fields->getGroupById($group->id);
// I made sure it was empty, since I don't want to delete the group if more fields were added.
if($groupModel && empty($groupModel->getFields())) {
craft()->fields->deleteGroupById($groupModel->id);
}
}
// Delete the layout tab
// Get the layout...
$layout = craft()->fields->getLayoutByType(ElementType::User);
// Get the tabs...
$tabs = $layout->tabs;
// Find and delete the tab we want, in the same fashion, except now we're unsetting the array instead of setting it.
foreach($tabs as $index => $tab) {
if($tab->name != "CustomTabName") {
continue;
}
unset($tabs[$index]);
}
$layout->setTabs($tabs);