Skip to content

feat: Interface Builder storyboard/XIB structured diffing #37

Description

@conorluddy

Intro

Add a storyboard_diff.py script that performs structured diffing of Interface Builder .storyboard and .xib files. Rather than treating these as opaque XML blobs (which produce noisy, unreadable git diffs), the script would parse the XML into semantic components — view controllers, views, constraints, outlets, actions, segues — and report meaningful changes in a token-optimised format.

Value Proposition

  • For AI agents: IB files produce enormous XML diffs that waste context. A structured diff reduces a 200-line XML diff to 3-5 lines of semantic changes like Added: UIButton "Submit" in LoginViewController, Changed: constraint leading 16→24 on nameLabel.
  • For developers: Storyboard merge conflicts are notoriously painful. A semantic diff helps understand what actually changed before resolving conflicts.
  • For code review: PR reviewers can quickly understand IB changes without mentally parsing XML.
  • Complements existing tooling: Pairs naturally with visual_diff.py — one catches pixel-level changes, the other catches structural changes.

Prerequisites

  • Python 3.9+ (already required by the project)
  • xml.etree.ElementTree from Python stdlib (no external dependencies needed)
  • Xcode's ibtool is available via xcrun ibtool but has limited diff capabilities — it supports --export for property extraction and --previous-file / --localize-incremental for localization deltas, but no general-purpose structural diff. Our script fills this gap.

Assumptions

  • Target users are working with UIKit projects that still use storyboards/XIBs. This includes:
    • Legacy codebases (significant installed base)
    • Capacitor/Cordova hybrid apps (which ship with storyboards by default)
    • Enterprise apps with long maintenance cycles
    • Teams gradually migrating from UIKit to SwiftUI
  • SwiftUI adoption is growing, but storyboards remain common in production apps as of 2026. The feature has a narrowing but still substantial audience.
  • Input files are valid IB XML documents (the script should fail gracefully on malformed input).

Suggested Implementation

Class Structure

Follow the existing VisualDiffer pattern from visual_diff.py:

class StoryboardDiffer:
    """Performs structured comparison between Interface Builder documents."""

    def __init__(self):
        pass

    def parse(self, file_path: str) -> dict:
        """Parse a .storyboard or .xib into a semantic model."""
        # Extract: scenes, viewControllers, views (recursive),
        # constraints, outlets, actions, segues, dependencies
        pass

    def diff(self, baseline_path: str, current_path: str) -> dict:
        """Compare two IB documents and return structured changes."""
        pass

Semantic Model

The parser should extract these elements from the IB XML:

Element XML Path Key Attributes
View Controllers //scene/objects/viewController id, customClass, storyboardIdentifier
Views //view (recursive) id, contentMode, translatesAutoresizingMaskIntoConstraints
Constraints //constraint id, firstItem, firstAttribute, secondItem, constant
Outlets //outlet property, destination, id
Actions //action selector, destination, eventType
Segues //segue id, destination, kind, identifier
Dependencies //plugIn, //deployment identifier, version

CLI Interface

# Compare two files
python scripts/storyboard_diff.py baseline.storyboard current.storyboard

# With options
python scripts/storyboard_diff.py old.xib new.xib --json --verbose

Flags: --json, --verbose, --ignore-ids (ignore internal IB object IDs which change on re-save).

Progressive Disclosure

Default output (3-5 lines, ~10 tokens):

Storyboard diff: 3 changes
  Added: UIButton in LoginViewController
  Changed: constraint constant 16→24 on nameLabel
  Removed: segue "showDetail" from MainViewController

Verbose output (full details):

Storyboard diff: baseline.storyboard → current.storyboard
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Scene: LoginViewController (BYZ-38-t0r)
  + View: UIButton (xkd-29-f3k)
      title: "Submit"
      frame: (20, 400, 335, 44)
  ~ Constraint: (abc-12-def)
      attribute: leading
      constant: 16 → 24

Scene: MainViewController (tne-QT-ifu)
  - Segue: "showDetail" (seg-45-xyz)
      kind: show
      destination: DetailViewController

Dependencies: no changes

JSON output:

{
  "baseline": "baseline.storyboard",
  "current": "current.storyboard",
  "summary": {"added": 1, "changed": 1, "removed": 1},
  "changes": [
    {"type": "added", "element": "view", "scene": "LoginViewController", "class": "UIButton", "id": "xkd-29-f3k"},
    {"type": "changed", "element": "constraint", "id": "abc-12-def", "attribute": "constant", "from": 16, "to": 24},
    {"type": "removed", "element": "segue", "scene": "MainViewController", "identifier": "showDetail", "id": "seg-45-xyz"}
  ]
}

Shared Utilities

No new common modules needed — xml.etree.ElementTree from stdlib handles all XML parsing. The script is self-contained.

Exit Codes

  • 0: Files are identical or diff completed successfully with no changes
  • 1: Differences found (useful for CI gating)
  • 2: Error (file not found, malformed XML)

Open Questions

  1. SwiftUI relevance: With SwiftUI adoption accelerating, is the user base for this feature large enough to justify inclusion? Storyboards are still common in production (especially hybrid apps via Capacitor/Cordova, enterprise apps, and legacy codebases), but the trend is away from IB.

  2. ID stability: IB regenerates internal object IDs (BYZ-38-t0r etc.) unpredictably. The --ignore-ids flag helps, but should we default to ignoring IDs and instead match elements by structural position + class?

  3. Constraint diffing depth: Auto Layout constraints reference objects by ID. Should we resolve these to human-readable names (e.g., "leading of submitButton to superview") or keep raw IDs?

  4. ibtool integration: xcrun ibtool --export can extract properties from IB documents in plist format. Should we use this as a preprocessing step for richer data, or is raw XML parsing sufficient? ibtool adds a subprocess dependency but provides validated data.

  5. Scope: Should this also support .nib (compiled) files, or only source formats (.storyboard, .xib)?

  6. Single-file inspection: Should the script also support a single-file mode (python scripts/storyboard_diff.py inspect Main.storyboard) that dumps the semantic model without comparison? This would be useful for AI agents exploring unfamiliar projects.

References

  • ibtool man page — Apple's IB compiler/inspector tool
  • Interface Builder XML format — Document type com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB
  • xml.etree.ElementTree — Python stdlib XML parser
  • Existing pattern: visual_diff.py in this repo for comparison-style script design

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P4Post-launch polish and enhancementsblast:1Single file — one view or one service, no ripple effectsclarity:3Direction known, details TBD — can start with questionsenhancementNew feature or requestneeds-designRequires design work before implementationrisk:2Small surface area, existing patterns, minor regression chancesize:MModerate scope, some design neededtype:featureNew functionality or capability

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions