Contents

reers/rhea

[中文文档](README_CN.md)

Requirements

Xcode 26.4 + (Swift 6.3+)

For older Xcode versions (16.0-26.3), please use version 2.2.8 which uses the experimental @_section and @_used attributes.

iOS 13.0+, macOS 10.15+, tvOS 13.0+, visionOS 1.0+, watchOS 7.0+

swift-syntax 601.0.1+

Basic Usage

import RheaExtension

#rhea(time: .customEvent, priority: .veryLow, repeatable: true, func: { _ in
    print("~~~~ customEvent in main")
})

#rhea(time: .homePageDidAppear, async: true, func: { context in
    // This will run on a background thread
    print("~~~~ homepageDidAppear")
})

#rhea(time: .load) { _ in
    print("load with trailing closure")
}

#load {
    print("use load directly")
}

#premain {
    print("use premain directly")
}

#appDidFinishLaunching {
    print("use appDidFinishLaunching directly")
}

class ViewController: UIViewController {
    
    #load {
        DispatchQueue.global().async {
            print("~~~~ load nested in main")
        }
    }

    #rhea(time: .homePageDidAppear) { context in
        print("homePageDidAppear with trailing closure \(context.param)")
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        Rhea.trigger(event: .homePageDidAppear, param: self)
    }
}

The framework provides three callback timings:

  1. OC + load (Strongly Discouraged) Using this timing can significantly slow down app launch as it blocks the entire loading process. Prefer using async, or just use .premain or .appDidFinishLaunching for initialization tasks whenever possible.
  2. constructor (premain)
  3. appDidFinishLaunching ()

These three timings are triggered internally by the framework, and there's no need for external trigger calls.

Additionally, users can customize timings and triggers, configure execution priorities for the same timing, and whether they can be repeatedly executed. ⚠️⚠️⚠️ However, note that the variable name of custom timing must exactly match its rawValue String, otherwise Swift Macro cannot process it correctly.

/// Registers a callback function for a specific Rhea event.
///
/// This macro is used to register a callback function to a section in the binary,
/// associating it with a specific event time, priority, and repeatability.
///
/// - Parameters:
///   - time: A `RheaEvent` representing the timing or event name for the callback.
///           This parameter also supports direct string input, which will be
///           processed by the framework as an event identifier.
///   - priority: A `RheaPriority` value indicating the execution priority of the callback.
///               Default is `.normal`. Predefined values include `.veryLow`, `.low`,
///               `.normal`, `.high`, and `.veryHigh`. Custom integer priorities are also
///               supported. Callbacks for the same event are sorted and executed based
///               on this priority.
///   - repeatable: A boolean flag indicating whether the callback can be triggered multiple times.
///                 If `false` (default), the callback will only be executed once.
///                 If `true`, the callback can be re-triggered on subsequent event occurrences.
///   - async: A boolean flag indicating whether the callback should be executed asynchronously.
///            If `false` (default), the callback will be executed on the main thread.
///            If `true`, the callback will be executed on a background thread. Note that when
///            `async` is `true`, the execution order based on `priority` may not be guaranteed.
///            Even when `async` is set to `false`, users can still choose to dispatch their tasks
///            to a background queue within the callback function if needed. This provides
///            flexibility for handling both quick, main thread operations and longer-running
///            background tasks.
///   - func: The callback function of type `RheaFunction`. This function receives a `RheaContext`
///           parameter, which includes `launchOptions` and an optional `Any?` parameter.
///
/// - Note: When triggering an event externally using `Rhea.trigger(event:param:)`, you can include
///         an additional parameter that will be passed to the callback via the `RheaContext`.
///
/// ```swift
/// #rhea(time: .load, priority: .veryLow, repeatable: true, func: { _ in
///     print("~~~~ load in Account Module")
/// })
///
/// #rhea(time: .registerRoute, func: { _ in
///     print("~~~~ registerRoute in Account Module")
/// })
///
/// // Use a StaticString as event directly
/// #rhea(time: "ACustomEventString", func: { _ in
///     print("~~~~ custom event")
/// })
///
/// // Example of using async execution
/// #rhea(time: .load, async: true, func: { _ in
///     // This will run on a background thread
///     performHeavyTask()
/// })
///
/// // Example of manually dispatching to background queue when async is false
/// #rhea(time: .load, func: { _ in
///     DispatchQueue.global().async {
///         // Perform background task
///     }
/// })
/// ```
/// - Note: ⚠️⚠️⚠️ When extending ``RheaEvent`` with static constants, ensure that
///   the constant name exactly matches the string literal value. This practice
///   maintains consistency and prevents confusion.
///
@freestanding(declaration)
public macro rhea(
    time: RheaEvent,
    priority: RheaPriority = .normal,
    repeatable: Bool = false,
    async: Bool = false,
    func: RheaFunction
) = #externalMacro(module: "RheaTimeMacros", type: "WriteTimeToSectionMacro")

Add CodeSnippets to XCode for greater efficiency.

~/Library/Developer/Xcode/UserData/CodeSnippets/

<img width="555" alt="截屏2025-02-08 20 26 22" src="https://github.com/user-attachments/assets/4db5a273-9084-4be5-8803-49674c9d9f5b" />

Performance

When tested on an iPhone 15 Pro in Release mode, with 3000 registered macros, it takes about 20 milliseconds to read the registered functions from the section. Additionally, the performance loss caused by executing the print function after 3000 dispatches is about 1.5 milliseconds. For older models like the iPhone 8, it takes 98 milliseconds to read 3000 registered functions. Overall, such performance is sufficient for any ultra-large app.

Usage

  1. Setting breakpoints inside functions requires macro expansion first; otherwise, the breakpoints won't take effect.

[CleanShot 2025-07-30 at 16 08 14@2x]

  1. If the passed-in function is relatively complex, an error may occur. You can encapsulate it into a function and then call it:
func complexFunction(context: RheaContext) {
    // Complex business logic
    performComplexTask()
    handleMultipleOperations()
}

#rhea(time: .load) { context in
    complexFunction(context: context)
}

Project Integration

### Example Project: https://github.com/Asura19/RheaExample

Since business needs to customize events, like this:
```swift
extension RheaEvent {
    public static let homePageDidAppear: RheaEvent = "homePageDidAppear"
    public static let registerRoute: RheaEvent = "registerRoute"
    public static let didEnterBackground: RheaEvent = "didEnterBackground"
}
```
The recommended approach is to wrap this framework in another layer, named RheaExtension for example
```
BusinessA    BusinessB
    ↓           ↓
RheaExtension
     ↓
  RheaTime
```

Additionally, RheaExtension can not only customize event names but also encapsulate business logic for timing events
```
#rhea(time: .appDidFinishLaunching, func: { _ in
    NotificationCenter.default.addObserver(
        forName: UIApplication.didEnterBackgroundNotification,
        object: nil,
        queue: .main
    ) { _ in
        Rhea.trigger(event: .didEnterBackground)
    }
})
```
External usage
```
#rhea(time: .didEnterBackground, repeatable: true, func: { _ in
    print("~~~~ app did enter background")
})
```

### Swift Package Manager
```swift
// Package.swift
let package = Package(
    name: "RheaExtension",
    platforms: [.iOS(.v13)],
    products: [
        .library(name: "RheaExtension", targets: ["RheaExtension"]),
    ],
    dependencies: [
        .package(url: "https://github.com/reers/Rhea.git", from: "2.3.0")
    ],
    targets: [
        .target(
            name: "RheaExtension",
            dependencies: [
                .product(name: "RheaTime", package: "Rhea")
            ]
        ),
    ]
)

// RheaExtension.swift
// After @_exported, other business modules and main target only need to import RheaExtension
@_exported import RheaTime

extension RheaEvent {
    public static let homePageDidAppear: RheaEvent = "homePageDidAppear"
    public static let registerRoute: RheaEvent = "registerRoute"
    public static let didEnterBackground: RheaEvent = "didEnterBackground"
}
```

```swift
// Business Module Account
// Package.swift
let package = Package(
    name: "Account",
    platforms: [.iOS(.v13)],
    products: [
        .library(
            name: "Account",
            targets: ["Account"]),
    ],
    dependencies: [
        .package(name: "RheaExtension", path: "../RheaExtension")
    ],
    targets: [
        .target(
            name: "Account",
            dependencies: [
                .product(name: "RheaExtension", package: "RheaExtension")
            ]
        ),
    ]
)
// Business Module Account usage
import RheaExtension

#rhea(time: .homePageDidAppear, func: { context in
    print("~~~~ homepageDidAppear in main")
})
```

```swift
// Main target usage
import RheaExtension

#rhea(time: .premain, func: { _ in
    Rhea.trigger(event: .registerRoute)
})
```

Additionally, you can directly pass `StaticString` as time key.
```
#rhea(time: "ACustomEventString", func: { _ in
    print("~~~~ custom event")
})
```

### CocoaPods

Add to Podfile:

```ruby
pod 'RheaTime'
```

Since CocoaPods doesn't support using Swift Macro directly, you can compile the macro implementation into binary for use. The integration method is as follows, requiring `s.pod_target_xcconfig` to load the binary plugin:
```swift
// RheaExtension podspec
Pod::Spec.new do |s|
  s.name             = 'RheaExtension'
  s.version          = '0.1.0'
  s.summary          = 'A short description of RheaExtension.'
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC
  s.homepage         = 'https://github.com/bjwoodman/RheaExtension'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'bjwoodman' => 'x.rhythm@qq.com' }
  s.source           = { :git => 'https://github.com/bjwoodman/RheaExtension.git', :tag => s.version.to_s }
  s.ios.deployment_target = '13.0'
  s.source_files = 'RheaExtension/Classes/**/*'

  s.dependency 'RheaTime', '2.3.0'

  # Copy following config to your pod
  s.pod_target_xcconfig = {
    'OTHER_SWIFT_FLAGS' => '-Xfrontend -load-plugin-executable -Xfrontend ${PODS_ROOT}/RheaTime/MacroPlugin/RheaTimeMacros#RheaTimeMacros'
  }
end
```

```swift
Pod::Spec.new do |s|
  s.name             = 'Account'
  s.version          = '0.1.0'
  s.summary          = 'A short description of Account.'
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC
  s.homepage         = 'https://github.com/bjwoodman/Account'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'bjwoodman' => 'x.rhythm@qq.com' }
  s.source           = { :git => 'https://github.com/bjwoodman/Account.git', :tag => s.version.to_s }
  s.ios.deployment_target = '13.0'
  s.source_files = 'Account/Classes/**/*'
  s.dependency 'RheaExtension'
  
  # Copy following config to your pod
  s.pod_target_xcconfig = {
    'OTHER_SWIFT_FLAGS' => '-Xfrontend -load-plugin-executable -Xfrontend ${PODS_ROOT}/RheaTime/MacroPlugin/RheaTimeMacros#RheaTimeMacros'
  }
end
```

Alternatively, if not using `s.pod_target_xcconfig` and `s.user_target_xcconfig`, you can add the following script in podfile for unified processing:
```ruby
post_install do |installer|
  installer.pods_project.targets.each do |target|
    rhea_dependency = target.dependencies.find { |d| ['RheaTime'].include?(d.name) }
    if rhea_dependency
      puts "Adding Rhea Swift flags to target: #{target.name}"
      target.build_configurations.each do |config|
        swift_flags = config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)']
        
        plugin_flag = '-Xfrontend -load-plugin-executable -Xfrontend ${PODS_ROOT}/RheaTime/MacroPlugin/RheaTimeMacros#RheaTimeMacros'
        
        unless swift_flags.join(' ').include?(plugin_flag)
          swift_flags.concat(plugin_flag.split)
        end
        
        
        end
        
        config.build_settings['OTHER_SWIFT_FLAGS'] = swift_flags
      end
    end
  end
end
```
<p><strong>⚠️ Important:</strong> If you encounter <code>rsync</code> permission errors with Xcode 14+, disable User Script Sandboxing:</p>
<p>In your project's <strong>Build Settings</strong>, search for <code>User Script Sandboxing</code> and set <code>ENABLE_USER_SCRIPT_SANDBOXING</code> to <code>No</code>. This resolves CocoaPods script execution issues caused by Xcode's stricter sandbox restrictions.</p>

Code usage is the same as SPM.

Note

⚠️ In theory, wrapping rhea macros could enable more convenient macros for use cases like route registration, plugin registration, module initialization, or specific encapsulations of rhea's time functionality. However, this appears to be currently blocked by a potential Swift bug. I've submitted an issue to the Swift repository and am awaiting a response.

Author

Asura19, x.rhythm@qq.com

License

Rhea is available under the MIT license. See the LICENSE file for more info.

Package Metadata

Repository: reers/rhea

Default branch: main

README: README.md