SettingsKit

repository·main·Indexed 43 days ago

https://github.com/aeastr/settingskit

A declarative SwiftUI framework for building complex settings interfaces with built-in search, navigation, and customizable styles for iOS and macOS. It features a hybrid architecture using a metadata node tree and a view registry to provide interactive search results and supports both sidebar and single-column navigation styles.

Tokens
3.4K
Snippets
12
Records
18
Agent score
5%

What's inside SettingsKit

  1. The Node Tree and Metadata

    main

    When you define settings using SettingsGroup and views with .indexed(), SettingsKit builds an internal node tree. Each node represents a group or an indexed view and stores the following properties:

    PropertyPurpose
    UUIDStable, hash-based identifier for navigation and registry lookup
    TitleDisplay text used for search results
    IconSF Symbol name
    TagsAdditional keywords used to improve search relevance
    Presentation ModeDetermines if the item is a navigation link or an inline section
    ChildrenNested groups and items

    Note: Nodes do not store view content; they only store the metadata required to find and identify views.

  2. How search scoring and results work

    main

    SettingsKit's search implementation uses an intelligent scoring system to rank results based on relevance. The process follows these steps:

    1. Normalization: Removes spaces and special characters, and converts text to lowercase.
    2. Tree Traversal: Recursively searches all nodes in the tree by title and tags.
    3. Scoring: Matches are ranked using the following weights:
      • Exact match: 1000
      • Starts with: 500
      • Contains: 300
      • Tag match: 100
    4. Result Grouping: Matches are grouped by their parent group.
    5. View Lookup: The system retrieves the actual view builders from the SettingsNodeViewRegistry to ensure results are interactive.
  3. How SettingsKit's hybrid architecture works

    main

    SettingsKit uses a hybrid architecture to provide powerful search capabilities while maintaining reactive SwiftUI views. It separates the system into three distinct layers:

    1. Metadata Layer (Node Tree): An internal tree of SettingsNode objects. These nodes store only metadata (Titles, Icons, Tags, Structure, and Presentation Mode) and do not store actual SwiftUI view content. This allows for fast indexing and searching.
    2. View Layer (Registry): A SettingsNodeViewRegistry that maps unique node IDs to view builder closures. This allows the system to retrieve and render actual interactive controls even when they are accessed via search results.
    3. Rendering Layer (SwiftUI): The final layer that uses either the direct SwiftUI hierarchy (for normal navigation) or the registry (for search results) to display interactive components with live state observation.
  4. Understand Stable IDs and hash-based UUID generation

    main

    To maintain navigation state, view identity, and animation stability, SettingsKit uses hash-based UUIDs instead of random UUIDs.

    Node UUIDs are generated by combining the node's title and icon into a hasher. This ensures that the same setting always produces the same ID across multiple makeNodes() calls, which is critical for matching search results to the correct views in the registry.

    var hasher = Hasher()
    hasher.combine(title)
    hasher.combine(icon)
    let hashValue = hasher.finalize()
    // Convert hash to UUID bytes...
  5. How SettingsKit works

    main

    SettingsKit uses a hybrid architecture to balance performance and interactivity:

    • Metadata Layer: Builds a lightweight node tree of titles, icons, and tags. This powers search and navigation without the overhead of full views.
    • View Registry: Indexed views and CustomSettingsGroup register their view builders in a global registry. This allows search results to render interactive controls instead of static text.
    • Rendering: Standard navigation uses SwiftUI's direct view hierarchy. Search results use the registry to instantiate fresh views with live state bindings.
  6. Choose between Sidebar and Single Column navigation styles

    main

    SettingsKit supports two primary navigation patterns that operate on the same indexed tree:

    • Uses a split-view layout with a sidebar and a detail pane.
    • macOS: Uses destination-based NavigationLink to ensure proper control updates.
    • iOS: Uses selection-based navigation.
    • The detail pane utilizes its own NavigationStack for nested groups.

    Single Column Style (NavigationStack)

    • Uses a linear push-navigation hierarchy.
    • Inline groups are rendered as section headers.
    • Search results are pushed onto the navigation stack.
  7. Install SettingsKit via Swift Package Manager

    main

    Add SettingsKit as a dependency in your Package.swift or Xcode project to use its declarative SwiftUI settings framework.

    dependencies: [
        .package(url: "https://github.com/aeastr/SettingsKit.git", from: "1.0.0")
    ]
  8. Use iOS Settings-style colored icons

    main

    To achieve the look of the iOS Settings app with colored icon backgrounds, use the icon: ViewBuilder with the SettingsIcon component.

    SettingsGroup("Battery") {
        Text("94%")
    } icon: {
        SettingsIcon("battery.100", color: .green)
    }
  9. Create a SettingsContainer root

    main

    A SettingsContainer serves as the root of your settings hierarchy. You define your settings structure within the settingsBody property, which returns some SettingsContent.

    struct AppSettings: SettingsContainer {
        var settingsBody: some SettingsContent {
            // Your settings groups here
        }
    }
  10. Organize settings with SettingsGroup

    main

    Use SettingsGroup to organize related settings. Groups can be presented in two ways:

    1. Navigation group (default): Appears as a tappable row that navigates to a new view.
    2. Inline group: Appears as a section header within the current view using the .inline parameter.

    Groups can also accept an icon: ViewBuilder to provide custom icons or iOS-style colored icons using SettingsIcon.

    // Navigation group
    SettingsGroup("Display", systemImage: "sun.max") {
        // Settings items...
    }
    
    // Inline group
    SettingsGroup("Quick Settings", .inline) {
        // Settings items...
    }
  11. Customize search logic with SettingsSearch

    main

    While search is automatic, you can provide a custom implementation by conforming to SettingsSearch and passing it to .settingsSearch().

    struct FuzzySearch: SettingsSearch {
        func search(nodes: [SettingsNode], query: String) -> [SettingsSearchResult] {
            // Your custom search implementation
        }
    }
    
    MySettings(settings: settings)
        .settingsSearch(FuzzySearch())
  12. Use the View Registry to render interactive search results

    main

    The SettingsNodeViewRegistry is responsible for mapping node IDs to view builders. This mechanism allows search results to render actual interactive controls (like Toggles or Pickers) rather than just static text labels.

    When a view is wrapped with .indexed(), it is registered in the shared registry. Custom groups can also register their own views via makeNodes().

    // When .indexed() wraps a view:
    SettingsNodeViewRegistry.shared.register(id: viewID) {
        AnyView(Toggle("Enable", isOn: $settings.notificationsEnabled))
    }
    
    // When CustomSettingsGroup.makeNodes() is called:
    SettingsNodeViewRegistry.shared.register(id: customGroupID) {
        AnyView(YourCompletelyCustomView())
    }
    
    // Later, in search results:
    if let view = SettingsNodeViewRegistry.shared.view(for: viewID) {
        view  // Renders the actual Toggle with live state binding
    }