Pharos

Pharos 是一个 Swift 的观察者模式框架,它利用了 propertyWrapper。在使用响应式编程设计应用程序时,它可以提供很大的帮助。 在底层,它使用 Chary 作为 DispatchQueue 的实用工具。

Codacy Badge build test SwiftPM Compatible Version License Platform

示例

要运行示例项目,请克隆该仓库,并首先从 Example 目录运行 pod install

要求

仅支持 Swift Package Manager

安装

Cocoapods

Pharos 可通过 CocoaPods 获得。 要安装它,只需将以下行添加到你的 Podfile 中

pod 'Pharos'

通过 XCode 安装 Swift Package Manager

通过 Package.swift 安装 Swift Package Manager

在你的 Package.swift 中添加作为目标依赖项

dependencies: [
    .package(url: "https://github.com/hainayanda/Pharos.git", .upToNextMajor(from: "4.0.0"))
]

在你的目标中将其用作 Pharos

 .target(
    name: "MyModule",
    dependencies: ["Pharos"]
)

作者

Nayanda Haberty, hainayanda@outlook.com

许可证

Pharos 在 MIT 许可证下可用。 有关更多信息,请参见 LICENSE 文件。


基本用法

你所需要的只是一个你想观察的属性,并为其添加 @Subject propertyWrapper。

class MyClass {
    @Subject var text: String?
}

要观察文本中发生的任何更改,请使用其 projectedValue 来获取其 `Observable`,并传递闭包订阅者。

class MyClass {
    @Subject var text: String?
    
    func observeText() {
        $text.observeChange { changes in
            print(changes.new)
            print(changes.old)
        }.retain()
    }
}

每次文本中发生任何设置时,它都会调用带有其更改的闭包,其中包括旧值和新值。 只要该值为 Equatable,你就可以忽略任何不更改值的设置。

class MyClass {
    @Subject var text: String?
    
    func observeText() {
        $text.distinct()
            .observeChange { changes in
                print(changes.new)
                print(changes.old)
            }.retain()
    }
}

如果你希望观察者使用当前值运行,只需触发它即可。

class MyClass {
    @Subject var text: String?
    
    func observeText() {
        $text.observeChange { changes in
            print(changes.new)
            print(changes.old)
        }.retain()
        .fire()
    }
}

如果你想忽略观察旧值,请使用 observe 代替。

class MyClass {
    @Subject var text: String?
    
    func observeText() {
        $text.observe { newValue in
            print(newValue)
        }.retain()
    }
}

你始终可以通过访问 observable 属性来检查当前值。

class MyClass {
    @Subject var text: String = "my text"
    
    func printCurrentText() {
        print(text)
    }
}

控制订阅者的保留

默认情况下,如果你观察 Observable 并以 retain() 结束,则闭包将由 Observable 本身保留。 如果 Observable 被 ARC 删除,它将自动被 ARC 删除。 如果你想使用自定义对象保留闭包,你可以这样做

class MyClass {
    @Subject var text: String?
    
    func observeText() {
        $text.observeChange { changes in
            print(changes.new)
            print(changes.old)
        }
        .retained(by: self)
    }
}

在上面的示例中,闭包将由 MyClass 实例保留,如果该实例被 ARC 删除,则将被删除。

如果你想手动处理保留,你始终可以使用 Retainer 来保留观察者

class MyClass {
    @Subject var text: String?
    
    var retainer: Retainer = .init()
    
    func observeText() {
        $text.observeChange { changes in
            print(changes.new)
            print(changes.old)
        }
        .retained(by: retainer)
    }
    
    func discardManually() {
        retainer.discardAllRetained()
    }
    
    func discardByCreateNewRetainer() {
        retainer = .init()
    }
    
}

有很多方法可以丢弃由 Retainer 管理的订阅者

你可以随时使用各种 retain 方法来控制你想要保留多长时间。

class MyClass {

    @Subject var text: String?
    
    var retainer: Retainer = .init()
    
    func observeTextOnce() {
        $text.observeChange { changes in
            print(changes.new)
            print(changes.old)
        }
        .retainUntilNextState()
    }
    
    func observeTextTenTimes() {
        $text.observeChange { changes in
            print(changes.new)
            print(changes.old)
        }
        .retainUntil(nextEventCount: 10)
    }
    
    func observeTextForOneMinutes() {
        $text.observeChange { changes in
            print(changes.new)
            print(changes.old)
        }
        .retain(for: 60)
    }
    
    func observeTextUntilFoundMatches() {
        $text.observeChange { changes in
            print(changes.new)
            print(changes.old)
        }
        .retainUntil {
            $0.new == "found!"
        }
    }
    
}

明智地使用此 retain 功能,因为如果你不了解 ARC 的工作原理,它可能会引入 retain cycle。

UIControl

只要在 iOS 中,你就可以通过调用 observeEventChange 来观察 UIControl 中的事件,或者如果你想观察特定事件,或者更具体地说是 touchUpInside 事件,则可以使用 whenDidTriggered(by:),或 whenDidTapped

myButton.observeEventChange { changes in
  print("new event: \(changes.new) form old event: \(changes.old)")
}.retain()

myButton.whenDidTriggered(by: .touchDown) { _ in
  print("someone touch down on this button")
}.retain()

myButton.whenDidTapped { _ in
  print("someone touch up on this button")
}.retain()

Bindable

你可以通过访问 bindables 中的 observables 来观察受支持的 UIView 属性中的更改。

class MyClass {
    var textField: UITextField = .init()
    
    func observeText() {
        textField.bindables.text.observeChange { changes in
            print(changes.new)
            print(changes.old)
        }.retain()
    }
}

你始终可以绑定两个 Observables 以相互通知

class MyClass {
    var textField: UITextField = .init()
    @Subject var text: String?
    
    func observeText() {
        $text.bind(with: textField.bindables.text)
            .retain()
    }
}

在上面的示例中,每次设置 text 时,它都会自动设置 textField.text,并且当设置 textField.text 时,它会自动设置 text

过滤订阅

你可以通过传递一个返回 Bool 值的闭包来过滤值,该值指示应忽略该值。

class MyClass {
    @Subject var text: String
    
    func observeText() {
        $text.ignore { $0.isEmpty }
            .observeChange { changes in
                print(changes.new)
                print(changes.old)
            }.retain()
    }
}

在上面的示例中,当新值为空时,observeChange 闭包将不会运行。

ignore 相反的是 filter

class MyClass {
    @Subject var text: String
    
    func observeText() {
        $text.filter { $0.count > 5 }
            .observeChange { changes in
                print(changes.new)
                print(changes.old)
            }.retain()
    }
}

在上面的示例中,仅当新值大于 5 时,observeChange 闭包才会运行。

节流

有时你只是想延迟一些观察,因为如果值来的太快,它可能会成为你的一些业务逻辑的瓶颈,例如当你调用 API 或其他操作时。 它会在闭包触发时自动使用最新值。

class MyClass {
    @Subject var text: String?
    
    func observeText() {
        $text.throttled(by: 1)
            .observeChange { changes in
                print(changes.new)
                print(changes.old)
            }
            .retain()
    }

    func test() { 
        text = "this will trigger the observable and block observer for next 1 second"
        text = "this will be stored in pending but will be replaced by next set"
        text = "this will be stored in pending but will be replaced by next set too"
        text = "this will be stored in pending and be used at next 1 second"
    }
}

添加 DispatchQueue

你可以添加 DispatchQueue 以确保你的 observable 在正确的线程上运行。 如果未提供 DispatchQueue,它将使用来自通知程序的线程。

class MyClass {
    @Subject var text: String?
    
    func observeText() {
        $text.dispatch(on: .main)
            .observeChange { changes in
                print(changes.new)
                print(changes.old)
            }
            .retain()
    }
}

它将使在此 dispatch 调用之后的所有订阅者都在给定的 DispatchQueue 中异步运行。

如果它已经在同一个 DispatchQueue 中,你可以通过使用 observe(on:) 使其同步。

class MyClass {
    @Subject var text: String?
    
    func observeText() {
        $text.observe(on: .main)
            .observeChange { changes in
                print(changes.new)
                print(changes.old)
            }
            .retain()
    }
}

映射值

你可以通过使用映射将 Subject 中的值映射到另一种类型。 映射将创建一个具有映射类型的新 Observable。

class MyClass {
    @Subject var text: String
    
    func observeText() {
        $text.mapped { $0.count }
            .observeChange { changes in
                print("text character count is \(changes.new)")
            }.retain()
    }
}

你始终可以在映射期间映射并忽略错误或 nil。 成功映射后始终会调用 Did set 闭包。

class MyClass {
    @Subject var text: String?
    
    func observeText() {
        $text.compactMapped { $0?.count }
            .observeChange { changes in
                // this will not run if text is nil
                print("text character count is \(changes.new)")
            }.retain()
    }
}

Observable 块

你始终可以使用 ObservableBlock 从代码块创建 Observable

let myObservableFromBlock = ObservableBlock { accept in
    runSomethingAsync { result in
        accept(result)
    }
}

myObservableFromBlock.observeChange { changes in
    print(changes)
}.retain()

Publisher

publisher 是仅用于发布值的 Observable。

let myPublisher = Publisher<Int>()

...
...

// it will then publish 10 to all of its subscribers
myPublisher.publish(10)

将值 relay 到另一个 Observable

你可以将来自任何 Observable 的值 relay 到另一个 Observable。

class MyClass {
    @Subject var text: String?
    @Subject var count: Int = 0
    @Subject var empty: Bool = true
    
    func observeText() {
        $text.compactMap { $0?.count }
            .relayChanges(to: $count)
            .retain()
    }
}

你始终可以通过访问 relayables 将值 relay 到任何 NSObject Bearer Observables。 它使用 dynamicMemberLookup,因此所有对象的可写属性都将在此处可用。

class MyClass {
    var label: UILabel = UILabel()
    @Subject var text: String?
    
    func observeText() {
        $text.relayChanges(to: label.relayables.text)
            .retain()
    }
}

合并 Observable

你可以合并尽可能多的 observables,只要它们的类型主题类型相同即可

class MyClass {
    @Subject var subject1: String = ""
    @Subject var subject2: String = ""
    @Subject var subject3: String = ""
    @Subject var subject4: String = ""
    
    func observeText() {
        $subject1.merged(with: $subject2, $subject3, $subject4)
            .observeChange { changes in
                // this will run if any of merged observable is set
                print(changes.new)
            }.retain()
    }
}

组合 Observable

你可以将最多 4 个 observables 组合为一个,并在设置其中任何一个 observable 时进行观察

class MyClass {
    @Subject var userName: String = ""
    @Subject var fullName: String = ""
    @Subject var password: String = ""
    @Subject var user: User = User()
    
    func observeText() {
        $userName.combine(with: $fullName, $password)
            .mapped { 
                User(
                    userName: $0.new.0 ?? "", 
                    fullName: $0.new.1 ?? "", 
                    password: $0.new.2 ?? ""
                )
            }.relayChanges(to: $user)
            .retain()
    }
}

它将生成一个包含所有组合值的 Observable,但它是可选的,因为当触发其中一个 observable 时,某些值可能不存在。 为了确保仅在所有组合值都可用时才触发它,你可以使用 compactCombine 代替

class MyClass {
    @Subject var userName: String = ""
    @Subject var fullName: String = ""
    @Subject var password: String = ""
    @Subject var user: User = User()
    
    func observeText() {
        $userName.compactCombine(with: $fullName, $password)
            .mapped { 
                User(
                    userName: $0.new.0, 
                    fullName: $0.new.1, 
                    password: $0.new.2
                )
            }.relayChanges(to: $user)
            .retain()
    }
}

在所有 observable 发出值之前,它不会被触发。


贡献

你懂的。 只需克隆并进行 pull request 即可