Engine Framework

repository·main·Indexed 19 days ago

https://github.com/nathantannar4/engine

A performance-driven framework for developing SwiftUI frameworks and applications. It provides idiomatic SwiftUI APIs and Views, featuring tools for static type-erasure via ViewAlias and AnyShape, conditional modifiers with StyleContext, version-specific views via VersionedView, and the @StyledView macro for custom view styles.

Tokens
2.8K
Snippets
11
Records
13
Agent score
19%

What's inside Engine

  1. Implement custom view styles with ViewStyle

    main

    For more control, such as implementing a root body that applies to all styles (e.g., a mandatory ViewModifier), implement the ViewStyle and ViewStyledView protocols manually instead of using the macro.

    • ViewStyle: Defines the Configuration and the makeBody(configuration:) method.
    • ViewStyledView: Defines the Configuration and a defaultStyle.
    public protocol ViewStyle {
        associatedtype Configuration
        associatedtype Body: View
    
        @ViewBuilder
        func makeBody(configuration: Configuration) -> Body
    }
    
    public protocol ViewStyledView: View {
        associatedtype Configuration
        var configuration: Configuration { get }
    
        associatedtype DefaultStyle: ViewStyle where DefaultStyle.Configuration == Configuration
        static var defaultStyle: DefaultStyle { get }
    }
  2. Configure CI for Engine (Xcode Cloud, GitHub Actions, Fastlane)

    main

    Because EngineMacros includes a Swift macro, CI environments may require user validation that causes builds to fail. To resolve this in your CI configuration, pass the -skipMacroValidation flag to your xcodebuild command.

    xcodebuild ... -skipMacroValidation
  3. Install Engine in Xcode Projects

    main

    To add Engine to an existing Xcode project, use the built-in Swift Package Manager interface:

    1. Open your project in Xcode.
    2. Select File -> Swift Packages -> Add Package Dependency.
    3. Enter the repository URL: https://github.com/nathantannar4/Engine.
  4. Install Engine in Swift Package Manager Projects

    main

    Add Engine as a dependency in your Package.swift file. You can optionally include EngineMacros to enable macro support.

    let package = Package(
        //...
        dependencies: [
            .package(url: "https://github.com/nathantannar4/Engine"),
        ],
        targets: [
            .target(
                name: "YourPackageTarget",
                dependencies: [
                    .product(name: "Engine", package: "Engine"),
                    .product(name: "EngineMacros", package: "Engine"), // Optional
                ],
                //...
            ),
            //...
        ],
        //...
    )
    let package = Package(
        //...
        dependencies: [
            .package(url: "https://github.com/nathantannar4/Engine"),
        ],
        targets: [
            .target(
                name: "YourPackageTarget",
                dependencies: [
                    .product(name: "Engine", package: "Engine"),
                    .product(name: "EngineMacros", package: "Engine"), // Optional
                ],
                //...
            ),
            //...
        ],
        //...
    )
  5. Type-erase shapes with AnyShape

    main

    AnyShape provides a backwards-compatible way to perform type erasure for SwiftUI Shapes. Additionally, you can use the @ShapeBuilder result builder with clipShape or contentShape to construct shapes from closures.

    @frozen
    public struct AnyShape: Shape {
        @inlinable
        public init<S: Shape>(shape: S)
    }
    
    extension View {
        @inlinable
        public func clipShape<S: Shape>(
            style: FillStyle = FillStyle(),
            @ShapeBuilder shape: () -> S
        ) -> some View
    }
  6. Handle multiple SwiftUI versions with VersionedView

    main

    To avoid the performance overhead and complexity of if #available(...) blocks (which can force the use of AnyView), use VersionedView and VersionedViewModifier. These allow you to define different body implementations for different iOS/macOS/etc. versions statically.

    struct ContentView: VersionedView {
        @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
        var v4Body: some View {
            Grid { /* ... */ }
        }
    
        var v1Body: some View {
            CustomGridView { /* ... */ }
        }
    }
    
    struct UnderlineModifier: VersionedViewModifier {
        @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
        func v4Body(content: Content) -> some View {
            content.underline()
        }
    
        func v1Body(content: Content) -> some View {
            content.background(Rectangle().frame(height: 1))
        }
    }
  7. Return views from descendants with ViewOutputKey

    main

    A ViewOutputKey allows a descendant view to return one or more views to a parent view.

    • ViewOutputKey: Supports returning multiple views (as a ViewOutputList).
    • ViewOutputAlias: A streamlined version that only supports returning a single view.
    @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
    public protocol ViewOutputKey {
        associatedtype Content: View = AnyView
        typealias Value = ViewOutputList<Content>
        static func reduce(value: inout Value, nextValue: () -> Value)
    }
  8. Use StaticConditionalContent for performance-critical gating

    main

    When a view or modifier is conditional based on a constant (like a #if DEBUG flag), use StaticConditionalContent or StaticConditionalModifier. This informs the compiler that the condition is static, avoiding the performance penalties associated with standard @ViewBuilder if/else blocks.

    struct IsDebug: StaticCondition {
        static var value: Bool {
            #if DEBUG
            return true
            #else
            return false
            #endif
        }
    }
    
    struct ProfileView: View {
        var body: some View {
            StaticConditionalContent(IsDebug.self) {
                NewProfileView()
            } otherwise: {
                LegacyProfileView()
            }
        }
    }
  9. Use StyleContext for conditional modifiers

    main

    A StyleContext allows you to conditionally apply ViewModifiers based on the environment (like being inside a ScrollView or List) without using AnyView. This is performed statically for better performance.

    • Use StyleContextModifier(context:) to apply a context to a view hierarchy.
    • Use StyleContextConditionalModifier(predicate:modifier:) to apply a modifier only when the predicate matches the current context.
    // Applying a modifier only when inside a ScrollView
    Text("Hello")
        .modifier(
            StyleContextConditionalModifier(predicate: .scrollView) {
                BackgroundModifier(color: .blue)
            }
        )
  10. Transform views with the @StyledView macro

    main

    The @StyledView macro allows you to transform nearly any View into one that supports ViewStyle. To use it, attach the macro to a type that conforms to the StyledView protocol. Note that Xcode's syntax highlighting may not work for types generated by this macro.

    To implement a custom style, create a type conforming to a specific style protocol (e.g., LabeledViewStyle) and implement makeBody(configuration:).

    import EngineMacros
    
    @StyledView
    struct LabeledView<Label: View, Content: View>: StyledView {
        var label: Label
        var content: Content
    
        var body: some View {
            HStack {
                label
                content
            }
        }
    }
  11. Transform views into collections with VariadicViewAdapter

    main

    The VariadicViewAdapter allows you to transform a single view into a collection of subviews. This is useful for building components like custom pickers where you need to iterate over the children of a container.

    struct PickerView<Selection: Hashable, Content: View>: View {
        @Binding var selection: Selection
        @ViewBuilder var content: Content
    
        var body: some View {
            VariadicViewAdapter {
                content
            } content: { source in
                ForEachSubview(source) { index, subview in
                    // Access subviews via source
                    Button { selection = subview.id(as: Selection.self)! } label: { subview }
                }
            }
        }
    }
  12. Type-erase views with ViewAlias

    main

    A ViewAlias allows for static type-erasure of a source view. This is more performant than AnyView because it is guaranteed to be static. You can use the .viewAlias(Alias.Type) { ... } modifier to resolve a source view through its alias.

    public protocol ViewAlias: View where Body == Never {
        associatedtype DefaultBody: View = EmptyView
        @MainActor @ViewBuilder var defaultBody: DefaultBody { get }
    }
    
    extension View {
        @inlinable
        public func viewAlias<Alias: ViewAlias, Source: View>(
            _ : Alias.Type,
            @ViewBuilder source: () -> Source
        ) -> some View
    }