Contents

aj-bartocci/Storybook-SwiftUI

Storybook like functionality for iOS apps

Project Requirements

  • Swift 5+
  • iOS 10+
  • macOS 11+
  • Xcode 11+

Demo Project

A demo project that targets iOS 11 can be found here.

Demo Videos:

V2:

https://github.com/aj-bartocci/Storybook-iOS-Demo/assets/16612478/65db4be6-d074-40bf-a608-0a6a8cd80a1d

V1:

https://user-images.githubusercontent.com/16612478/185280246-6512760d-1f80-4b46-9a66-e215e3f5f3eb.mp4

Goals

  • Not intrusive

- The previews use objc runtime to dynamically pull in views to render. This means you don't need to change your existing code, simply add a @objc static vars onto the Storybook class to see it render. This means each component file can extend the Storybook class to add components.

  • No building

- The StorybookCollection is simply a SwiftUI view so you can throw it in a PreviewProvider and browse through your app views without having to build the app.

  • Backwards compatible

- You don't need to be using SwiftUI in your production app. Simply mark the previews with @available and you are good to go.

Roadmap

✅ Configurable components

  • To be more like storyboard there should be the ability to configure components on the fly. I.e. setting text values, number values, etc. One possible way could be through reflection.

✅ Be able to ship storybook with staging builds for designers to view alongside the app. Current work for this happening on the experimental branch.

  • This means storybook is no longer behind a DEBUG flag, it is up to you to make sure it does not ship with your production code

🔲 Visual regression testing with snapshots like storybook js: https://storybook.js.org/tutorials/intro-to-storybook/react/en/test/

🔲 TBD...

Upgrading from version 1.x.x to 2.0.0

Version 2.0 has been released with many improvements, however in doing so some of the exisitng functionality may no longer work the same.

  1. MacOS must be 11+, previously it was recommended but still worked back to 10.15. There is now a hard requirement to be 11+
  2. StorybookPage was updated to use a folder system. The old initializers still exist but are deprecated. Without updating the UI will not look as optimal but will still funciton

Controls

With version 2.0.0+ you can render a control panel within the storybook that can modify the current View being looked at. Controls are powered by the StorybookControlType enum which allows you to use prebuilt controls or your own custom ones.

In order to take advantage of controls you must add them to your Storybook context. Since storybook takes advantage of SwiftUI's Environment this means that you can apply controls to individual Views or cascade to every view within Storybook. By default Storybook will wrap all views in a control context that picks up controls from the environment so that you do not need to specify the same controls over and over again.

func storybookSetGlobalControls(_ controls: StorybookControlType...) -> some View

To apply controls to all views within Storybook set the global controls at the root. In this example controls for colorScheme, dynamicType, screenSize, and a custom control will be applied to all views.

Storybook.render()
    .storybookSetGlobalControls(
        .colorScheme,
        .dynamicType,
        .screenSize,
        .custom(StorybookControl(id: "MyCustomControl", view: {
            CustomControl()
        }))
    )

To add individual controls to views you can use another function for adding controls to the context. The following will add a jira documentation link to the control menu for this specific view.

extension Storybook {
    @objc static let someView = StorybookPage(
        folder: "/Design System/Views/Some View",
        view: SomeView()
            .storybookAddControls(
                .documentationLink(
                    title: "Jira", 
                    url: "https://jira.com/123", 
                    icon: .jira
                )
            )
            .storybookTitle("Primary")
    )
}

Example custom Control

Custom controls can easily be added to the Storybook control overlay. Here is an example of a control to change the title of a view.

Important: If the control has state that needs to update that is not text you must update the id when it changes. For example with a toggle you must update the id when the toggle changes. Hopefully there will be a cleaner solution in the future.

// The View used in the App
@available(iOS 13.0, *)
struct SomeView: View {
    let title: String
    
    var body: some View {
        Text(title)
    }
}

// A wrapper around SomeView to control it
@available(iOS 13.0, *)
struct ControlledSomeView: View {
    @State var title = "Hello, World!"
    @State var isToggled = false

    var controlId: String {
        return "SomeViewControl" + isToggled.description
    }
    
    var body: some View {
        SomeView(title: title)
            .storybookAddControls(
                .custom(StorybookControl(
                    id: controlId,
                    view: {
                        VStack {
                            TextField("Title", text: $title)
                            Toggle(isOn: $isToggled) {
                                Text("Some Toggle")
                            }
                        }
                    }
                ))
            )
    }
}

@available(iOS 13.0, *)
extension Storybook {
    @objc static let someView = StorybookPage(
        folder: "/Views",
        view: ControlledSomeView().storybookTitle("Some View")
    )
}

Tags

Tags provide a way to organize and filter your components in Storybook. You can add tags to both pages and individual views, making it easy to find related components across your design system.

Adding Tags to Pages

Tags can be added to a StorybookPage using the tags parameter in the initializer:

extension Storybook {
    @objc static let button = StorybookPage(
        folder: "/Design System/Buttons",
        views: [
            PrimaryButton().storybookTitle("Primary"),
            SecondaryButton().storybookTitle("Secondary")
        ],
        tags: "button", "interactive", "core"
    )
}

Adding Tags to Individual Views

You can also add tags to individual views using the storybookTags modifier:

extension Storybook {
    @objc static let buttons = StorybookPage(
        folder: "/Design System/Buttons",
        views: [
            PrimaryButton()
                .storybookTitle("Primary")
                .storybookTags("primary", "cta"),
            SecondaryButton()
                .storybookTitle("Secondary")
                .storybookTags("secondary")
        ],
        tags: "button", "interactive"
    )
}

You can also apply tags to an array of views:

let views = [
    SomeView().storybookTitle("View 1"),
    SomeView().storybookTitle("View 2")
].storybookTags("common-tag", "shared")

Using StorybookFolder with Tags

The StorybookFolder helper makes it easy to reuse folder paths and tags across multiple pages:

let designSystem = StorybookFolder(
    name: "/Design System",
    tags: "core", "design-system"
)

extension Storybook {
    @objc static let buttons = StorybookPage(
        folder: designSystem.addingPath("/Buttons", tags: "interactive"),
        views: [
            PrimaryButton().storybookTitle("Primary"),
            SecondaryButton().storybookTitle("Secondary")
        ]
    )
}

The addingPath method creates a new folder by appending to the current path and merging tags.

Filtering by Tags

In the Storybook UI, you can filter components by selecting tags from the tags view. This allows you to quickly find all components with specific tags, regardless of where they are in the folder hierarchy.

AI-Assisted Development

For AI-assisted development workflows, a Claude skill is available in the skills/ directory that provides a complete workflow for visually verifying SwiftUI views during development.

The verifying-with-storybook skill includes:

  • Launch argument setup - Configure your app to launch directly into Storybook
  • UUID-based tagging strategy - Use unique tags for instant navigation to specific views
  • Verification workflow - Complete flow from tagging to visual verification
  • Tag management - Best practices for temporary dev tags vs permanent semantic tags

This enables rapid visual verification during development: tag a view with a unique identifier, launch the app to Storybook, and automatically navigate to that specific view.

Models

Storybook

The Storybook class uses objc runtime to mirror it's static properties that are of the type StorybookPage and generate previews for them. In order to add previews to the storybook create an extension on Storybook with a static property pointing to a StorybookPage.

StorybookPage

The StorybookPage class is used to render content you want to appear in the StorybookCollection.

public init(
    folder directory: String,
    view: StoryBookView,
    file: String = #file
)
public init(
    folder directory: String,
    views: [StoryBookView],
    file: String = #file
)

There are 2 initializers for creating a page. The title and file arguments are used when rendering the list of Storybook pages. Sometimes the name of a component in code doesn't accurately describe what it is. So the title can be a more human readable description of the component, while the file tells you where that component is located in code. The file has a default argument which will take the file of wherever the intializer is called from, or you can supply the file manually if you choose (not recommended).

StorybookView

The StoryBookView struct is a SwiftUI wrapper view used for rendering the views within a StorybookPage. You supply a title and view to be rendered.

View has an extension that wraps itself inside StorybookView for a cleaner API.

func storybookTitle(_ title: String, file: String = #file) -> StoryBookView

// Usage 

SomeView().storybookTitle("My View")

StorybookCollection

The StorybookCollection struct is a SwiftUI view that renders all of the Storybook pages. This should be used in a PreviewProvider so that you can easily browse without having to run anything.

A convience function on Storybook called render, makes it a little easier to remember how to render things.

Storybook.render() is the same as StorybookCollection()

Package Metadata

Repository: aj-bartocci/Storybook-SwiftUI

Stars: 41

Forks: 1

Open issues: 0

Default branch: master

Primary language: swift

License: MIT

README: README.md