Swift中如何从监听器数组解绑闭包类型监听器(主题引擎场景)
Hey there! Let's break down how to solve the closure unsubscription problem for your ThemeManager and Box setup. The core issue here is that Swift closures don't natively support equality checks, so we can't just remove a specific closure from an array directly. Instead, we'll wrap each closure in an identifiable object to track and unsubscribe it later.
Step 1: Create a Wrapper Class for Your Theme Listeners
First, we'll define a simple class to wrap our theme update closure along with a unique ID. This ID will let us find and remove the exact listener when needed:
import Foundation class ThemeListener { let uniqueId: UUID let updateHandler: (Theme) -> Void init(handler: @escaping (Theme) -> Void) { self.uniqueId = UUID() self.updateHandler = handler } }
(Note: Replace Theme with your actual theme type if it's named differently.)
Step 2: Update the Box Class to Use the Wrapper
Modify your Box class to store an array of ThemeListener instances instead of raw closures. We'll add subscribe (which returns the listener for later unsubscription) and unsubscribe methods:
class Box<T> { private var listeners: [ThemeListener] = [] var value: T { didSet { notifyAllListeners() } } init(value: T) { self.value = value } // Subscribe to value changes, returns the listener to hold onto for unsubscribing func subscribe(to handler: @escaping (Theme) -> Void) -> ThemeListener { let listener = ThemeListener(handler: handler) listeners.append(listener) // Optional: Immediately trigger the handler with the current value handler(value as! Theme) return listener } // Unsubscribe using the listener instance returned from subscribe() func unsubscribe(_ listener: ThemeListener) { listeners.removeAll { $0.uniqueId == listener.uniqueId } } private func notifyAllListeners() { listeners.forEach { $0.updateHandler(value as! Theme) } } }
(If your Box is generic over your Theme type directly, you can remove the force cast and adjust the handler type to (T) -> Void for cleaner code.)
Step 3: Update BaseViewController to Manage Subscription Lifecycle
In your BaseViewController, store the listener instance and unsubscribe when the view controller is no longer needed (to avoid memory leaks and unnecessary updates):
class BaseViewController: UIViewController { private var themeSubscription: ThemeListener? override func viewDidLoad() { super.viewDidLoad() setupThemeSubscription() } private func setupThemeSubscription() { themeSubscription = ThemeManager.shared.currentThemeBox.subscribe { [weak self] theme in guard let self = self else { return } self.applyTheme(theme) } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // Clean up the subscription when the view controller is leaving the screen if let listener = themeSubscription { ThemeManager.shared.currentThemeBox.unsubscribe(listener) themeSubscription = nil } } // Subclasses override this to apply theme styles func applyTheme(_ theme: Theme) { // Default implementation (optional) } }
Key Notes:
- Avoid Memory Leaks: Always use
[weak self]in the closure to prevent a retain cycle between the view controller and the Box/ThemeManager. - Unique Identification: Using
UUIDensures each listener has a unique identifier, so we never accidentally remove the wrong one. - Lifecycle Management: Unsubscribing in
viewWillDisappear(ordeinit, depending on your needs) ensures we don't keep references to view controllers that are no longer in use.
This approach is clean, maintainable, and solves the core problem of unsubscribing closure-based listeners in Swift.
内容的提问来源于stack exchange,提问作者anoop4real

