Unlock Scalable iOS Plugins: Inside ZHPluginSDK’s Architecture and Message System
This article introduces ZHPluginSDK, a powerful Swift‑based plugin framework that supports type‑safe registration, flexible message dispatch, multi‑container composition, synchronous messaging, and environment variables, providing a comprehensive guide with architecture diagrams, feature lists, and extensive code examples for mobile developers.
ZHPluginSDK is a powerful, generic plugin system built with Swift that uses a special registration and message dispatch mechanism to efficiently deliver any data structure within the system. It includes a view‑management system that enables rapid composition of complex UI for large business modules.
1. Framework Overview
ZHPluginSDK offers multiple dimensions of plugin containers, allowing developers to create independent plugins or containers based on business topology and dynamically aggregate them into complex business flows.
1.1 Feature Highlights
Plugins are registered by type, hiding instance objects and enforcing coupling.
Plugin messages can carry any data type.
Message types are preserved during transmission, eliminating the need for type checks.
Both asynchronous and synchronous message mechanisms are supported.
Runtime‑based implicit registration and precise dispatch.
PropertyWrapper provides syntactic sugar for elegant message listening.
Message interceptors and relays.
Tree‑structured plugin topology with SwiftUI‑like environment variables.
UI plugin view‑management mechanism.
Custom plugin priority.
ContainerComposer aggregates multiple plugin containers.
1.2 Architecture Overview
1.2.1 Simple System
In simple scenarios, a single container with multiple plugins implements business logic.
1.2.2 Complex System
Complex scenarios (e.g., live rooms, documents) may require multiple modules. Multiple PluginContainers and ContainerComposer form a tree topology, enabling flexible aggregation of independent modules.
2. Architecture Design
2.1 ContainerComposer Multi‑Container Composition
2.1.1 Design Motivation
Using a single PluginContainer for complex business leads to drawbacks: plugins are flat and cannot represent fine‑grained module hierarchy, and a single ViewContainer becomes cumbersome when many plugins need layout management.
2.1.2 Composition Diagram
2.1.3 Mechanism Implementation
// MARK: - TestMessage
struct TestMessage: PluginMessage {}
func test() {
let composerA = PluginContainerComposer()
let containerB = PluginContainer()
let composerB = PluginContainerComposer()
composerA.compose(containerB)
composerA.compose(composerB)
composerA.send(message: TestMessage())
}Both PluginContainerComposer and PluginContainer inherit from ComposibleContainer, allowing nested aggregation similar to folders (composer) and files (container).
// MARK: - ComposibleContainer
protocol ComposibleContainer: AnyObject {
var containerAssociatedKey: ContainerAssociatedKey? { get set }
func receive(containerMessage: any PluginMessage, containerId: String)
}
extension ComposibleContainer {
var id: String { objectIdentifier(self) }
} // MARK: - PluginContainerComposer
public final class PluginContainerComposer: ComposibleContainer {
private var composedContainers: [ComposibleContainer] = []
var containerAssociatedKey: ContainerAssociatedKey?
func compose(containers: [ComposibleContainer]) {
for container in containers {
if id == container.id { assertionFailure("cannot compose itself"); return }
let deallocHook = DeallocHook { [weak self, weak container] in
guard let self, let container else { return }
self.composedContainers.removeAll { $0.id == container.id }
}
container.containerAssociatedKey = .init(associatedContainer: self, deallocHook: deallocHook)
composedContainers.append(container)
}
}
func receive(containerMessage: PluginMessage, containerId: String) {
for container in composedContainers where container.id != containerId {
container.receive(containerMessage: containerMessage, containerId: id)
}
containerAssociatedKey?.associatedContainer?.receive(containerMessage: containerMessage, containerId: id)
}
public func send(message: any PluginMessage) { receive(containerMessage: message, containerId: id) }
public func compose(_ containers: PluginContainer...) { compose(containers: containers) }
public func compose(_ multiContainers: PluginContainerComposer...) { compose(containers: multiContainers) }
}2.2 MessageRelay (Message Intermediary)
2.2.1 Design Motivation
Existing MessageObserver limits reception to the plugin itself, making it hard for nested components to listen. MessageRelay allows any object to act as a relay point, enabling plugins to forward messages through a MessageSender.
// MARK: - MessageRelay Protocol
public protocol MessageRelay: AnyObject {
func messageRelayConnected(messageSender: any MessageSender)
} // MARK: - MessageSender Protocol
public protocol MessageSender {
var publisher: AnyObserver<any PluginMessage> { get }
func send(message: any PluginMessage)
@discardableResult func relayMessages(to messageRelay: any MessageRelay) -> Result<Void, Error>
}MessageRelay does not have the same priority as plugins and is weakly referenced; it is removed from the registry upon deallocation.
2.2.2 MessageRelay Diagram
2.2.3 Implementation
// MARK: - MessageRelay Protocol
public protocol MessageRelay: AnyObject {
func messageRelayConnected(messageSender: any MessageSender)
}2.3 Synchronous Message Mechanism
2.3.1 Design Motivation
Some scenarios require immediate responses, such as querying a frequently changing state or performing routing decisions based on a response.
2.3.2 Approaches
Attach a callback to the message payload.
Wrap the synchronous call as an EnvironmentValue with an optional block property.
2.3.3 API Design
Define a SyncPluginMessage protocol with an associated Response type.
public protocol SyncPluginMessage {
associatedtype Response
}Create a synchronous message by conforming to this protocol.
struct RouteToMessage: SyncPluginMessage {
typealias Response = Bool
let destinationUrl: String
}Send a synchronous message using a sender that returns an array of responses, with a dispatch strategy (first or all).
func send<Message: SyncPluginMessage>(message: Message, strategy: SyncMessageDispatchStrategy) -> [Message.Response]Convenient overload returns the first response.
public func send<Message: SyncPluginMessage>(message: Message) -> Message.Response? {
send(message: message, strategy: .first(1)).first
}2.3.4 Listening
Plugins can listen to synchronous messages using @SyncMessageObserver, specifying the plugin type and message type.
class RouteManagerPlugin: Plugin {
@SyncMessageObserver<RouteManagerPlugin, RouteToMessage>
private var routeToMessage = { base, message in
base.route(to: message.destinationUrl)
}
}3. Architecture Practice
3.1 Framework Dependencies
Swift generic and strong‑type system for registration and listening.
RxSwift integration for reactive message handling.
3.2 Case Study – Live Room
A live‑room controller contains ChatViewController and InputBarViewController as plugins. Plugins are registered in a PluginContainer, UI slots are provided via PluginViewContainer, and messages such as SendTextMessage are sent and observed.
// Plugin definition
public protocol Plugin {
init?(params: Any?)
func pluginInitialized(messageSender: any MessageSender, name: String?, slotViewAccessor: @escaping (String?) -> PluginSlotView?)
func queryView(with viewId: String) -> UIView?
func pluginDeinit()
}
// Example plugin implementation
class InputBarViewController: UIViewController, Plugin {
private var messageSender: MessageSender?
required init?(params: Any?) { super.init(nibName: nil, bundle: nil) }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func pluginInitialized(messageSender: MessageSender, name: String?, slotViewAccessor: @escaping (String?) -> PluginSlotView?) {
self.messageSender = messageSender
guard let viewAccessor = slotViewAccessor(nil), let parentVC = viewAccessor.slotViewController else { return }
willMove(toParent: parentVC)
parentVC.addChild(self)
viewAccessor.slotView.addSubview(view)
view.snp.makeConstraints { $0.edges.equalToSuperview() }
}
func queryView(with viewId: String) -> UIView? { nil }
}
// Sending a message
func send(text: String) {
messageSender?.send(message: SendTextMessage(text: text))
}
// Receiving a message
class ChatViewController: UIViewController, Plugin {
@MessageObserver private var receiveTextMessage: SendTextMessage?
private let disposeBag = DisposeBag()
func pluginInitialized(messageSender: MessageSender, name: String?, slotViewAccessor: @escaping (String?) -> PluginSlotView?) {}
func queryView(with viewId: String) -> UIView? { nil }
func viewDidLoad() {
super.viewDidLoad()
$receiveTextMessage.bind(onNext: { message in
print("receive text message: \(message.text)")
}).disposed(by: disposeBag)
}
}3.3 Advanced Usage
Plugin initialization parameters via PluginParamProvider.
Multiple plugins of the same type distinguished by name.
Message observing priority to control dispatch order.
Message interceptors for logging, transformation, or early termination.
Environment variables set with setEnvironment, scoped to downstream containers.
// Registering an interceptor
pluginContainer.register(messageInterceptor: .listen { message in
// custom logic
})
// Setting an environment object
pluginContainer.setEnvironment(liveRoomInfo)The article provides a complete guide to building, composing, and using ZHPluginSDK for modular, scalable iOS applications.
Zhihu Tech Column
Sharing Zhihu tech posts and exploring community technology innovations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
