Spezi

repository·main·Indexed 42 days ago

https://github.com/stanfordspezi/spezi

An open-source framework for building interoperable digital health applications using a modular architecture in Swift. Spezi utilizes Standards to orchestrate data flow and Modules to provide reusable functionality, featuring a constraint mechanism for requirements enforcement and a platform-agnostic approach to application lifecycle management. It integrates with SwiftUI via the SpeziAppDelegate and provides specialized property wrappers like @Model for state management and @Application for application instance access.

Tokens
8.3K
Snippets
21
Records
44
Agent score
54%

What's inside Spezi

  1. Overview of the Spezi Framework

    main

    Spezi is an open-source framework designed for the rapid development of modern, interoperable digital health applications. It utilizes a module-based approach to build software, allowing developers to compose applications from reusable subsystems.

    To explore the framework's functionality, it is recommended to use the Spezi Template Application, which demonstrates how various modules integrate into a single, extensible application.

  2. What is a Spezi Module and how does it fit into the ecosystem?

    main

    A Module is the fundamental building block of the Spezi ecosystem. Modules can range from standalone functionality (using frameworks like Swift or SwiftUI) to deeply embedded components providing complex UI and data management.

    To integrate into the Spezi ecosystem, the simplest method is to conform to the Swift Module protocol. While not strictly required, using this protocol is the standard way to ensure compatibility. Spezi is a modularized ecosystem where each software subsystem is typically embedded in a Swift Package, which may contain multiple targets and modules.

  3. How default initialization of dependencies works

    main

    Modules are typically initialized using the options provided during the SpeziAppDelegate/configuration phase.

    If you declare a dependency on a module that has not been explicitly configured by the user, Spezi will fall back to initializing that dependency using the instance provided to the @Dependency property wrapper.

    To make a module eligible for this fallback behavior, it should adopt the DefaultInitializable protocol, which mandates a default initializer.

    class ExampleModuleDependency: Module, DefaultInitializable {
        init() {
            // default options ...
        }
    }
    
    class ExampleModule: Module {
        // This dependency uses the default init if the module wasn't externally configured.
        @Dependency var exampleModuleDependency = ExampleModuleDependency()
    }
  4. How Spezi Modules and Standards work together

    main

    Spezi's architecture is built on two primary abstractions:

    1. Standard: Defines the key coordinator that orchestrates data flow in an application by meeting requirements defined by modules. It acts as the glue between different parts of the system.
    2. Module: Defines a software subsystem that provides distinct and reusable functionality.

    Key Module Behaviors:

    • Constraints: Modules can use a constraint mechanism to enforce specific requirements for the Standard used in the software.
    • Dependencies: Modules can define dependencies on each other to reuse functionality.
    • Communication: Modules communicate by offering and collecting information.
    • Protocols: Modules may conform to various protocols to access Spezi features like lifecycle management or to trigger SwiftUI view updates using Swift's observable mechanisms.
  5. Use Standard Constraints to enforce requirements

    main

    Modules can use constraints to ensure the application's Standard instance meets specific requirements. This is a two-step process:

    1. Define a Standard Constraint: Create a protocol that inherits from Standard.
    2. Enforce the Constraint: Use the @StandardActor property wrapper within your module to access the Standard instance that conforms to your specific protocol.

    Important: You cannot access the @StandardActor during the module's init. It becomes available only after the configure() method has been called.

    // 1. Define the constraint
    protocol ExampleConstraint: Standard {
        // Define required capabilities here
    }
    
    // 2. Enforce and use the constraint
    class ExampleModule: Module {
        @StandardActor var standard: any ExampleConstraint
    
        override func configure() {
            // Accessing 'standard' is safe here, but not in init()
            print("Standard is ready")
        }
    }
  6. Communicate between Modules using @Provide and @Collect

    main

    You can establish data flow between different Module instances without creating a direct dependency hierarchy by using the @Provide and @Collect property wrappers.

    • @Provide: Used by a module to share a specific value. Values must be assigned within the module's init() and cannot be changed after initialization.
    • @Collect: Used by a module to gather all values provided by other modules for a specific type.

    Lifecycle Requirements:

    1. Initialization: All @Provide values must be initialized within the module's init method.
    2. Configuration: Data from all @Provide properties becomes available only after configure() is called on the modules.
    3. Access: You must only access @Collect properties after configure() has been invoked. Accessing them before this will cause a runtime crash.
    class ModuleA: Module {
        @Provide var someGreeting = "Hello"
    }
    
    class ModuleB: Module {
        @Provide var someGreeting: [String]
    
        init() {
            someGreeting = ["Hola", "Hallo"]
        }
    }
    
    class ModuleC: Module {
        @Collect var allGreetings: [String]
    
        func configure() {
            // Accessing @Collect is safe here
            print("All the greetings we received: \(allGreetings)") // prints "Hello", "Hola", "Hallo" in any order
        }
    }
  7. How the Spezi Standard constraint mechanism works

    main

    Spezi modules can enforce specific requirements on the Standard instance used within an application via a constraint mechanism. This ensures that any module integrated into a project has access to the necessary capabilities or data provided by the Standard actor.

    To implement constraints, a module defines a protocol that inherits from the Standard protocol:

    protocol ExampleConstraint: Standard {
        // ...
    }

    As an application developer, you must ensure the Standard instance provided in your Configuration satisfies these constraints. You can do this by either:

    1. Defining a custom Standard: Create an actor that conforms to both Standard and the required constraint protocol.
    2. Extending an existing Standard: Use Swift extensions to make a predefined Standard conform to the required protocol.

    Once configured, you can access the current Standard instance within a Module using the @StandardActor property wrapper, or within SwiftUI views using the @Environment property wrapper (provided the view conforms to EnvironmentAccessible).

    protocol ExampleConstraint: Standard {
        // ...
    }
  8. Handle notifications by adopting NotificationHandler

    main

    To respond to notification actions or manage incoming notifications while the app is in the foreground, adopt the NotificationHandler protocol in your Module or Standard.

    Implement the following methods:

    • handleNotificationAction(_:): To respond to specific user actions taken on a notification.
    • receiveIncomingNotification(_:): To handle notifications received while the app is running in the foreground.
    • receiveRemoteNotification(_:): To fetch additional content from a remote server when a remote notification is received (useful for background updates).
  9. Configure the Standard in SpeziAppDelegate

    main

    The Standard instance is defined within the Configuration object inside your app delegate (which must conform to SpeziAppDelegate). This is where you inject your custom or extended Standard into the Spezi ecosystem.

    var configuration: Configuration {
        Configuration(standard: ExampleStandard()) { 
            // ...
        }
    }
  10. Satisfy module constraints by extending an existing Standard

    main

    If you are using a predefined Standard type that does not yet meet the requirements of a specific module, you can use Swift extensions to add the necessary conformance to the constraint protocol.

    extension ExistingStandard: ExampleConstraint {
        // Implement required properties and methods from ExampleConstraint
    }