
## Features
Lightweight Observable is a simple implementation of an observable sequence that you can subscribe to. The framework is designed to be minimal meanwhile convenient. The entire code is only ~100 lines (excluding comments). With Lightweight Observable you can easily set up UI-Bindings in an MVVM application, handle asynchronous network calls and a lot more.
##### Credits
The code was heavily influenced by [roberthein/observable](https://github.com/roberthein/Observable). However I needed something that was syntactically closer to [RxSwift](https://github.com/ReactiveX/RxSwift), which is why I came up with this code, and for re-usability reasons afterwards moved it into a CocoaPod.
##### Migration Guide
If you want to update from version 1.x.x, please have a look at the [Lightweight Observable 2.0 Migration Guide
](Documentation/Lightweight%20Observable%202.0%20Migration%20Guide.md)
### Example
To run the example project, clone the repo, and open the workspace from the Example directory.
### Requirements
- Swift 5.0
- Xcode 10.2+
- iOS 9.0+
### Integration
##### CocoaPods
[CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate Lightweight Observable into your Xcode project using CocoaPods, specify it in your `Podfile`:
```ruby
pod 'LightweightObservable', '~> 2.0'
```
##### Carthage
[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate Lightweight Observable into your Xcode project using Carthage, specify it in your `Cartfile`:
```ogdl
github "fxm90/LightweightObservable" ~> 2.0
```
Run carthage update to build the framework and drag the built `LightweightObservable.framework` into your Xcode project.
##### Swift Package Manager
The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but Lightweight Observable does support its use on supported platforms.
Once you have your Swift package set up, adding Lightweight Observable as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
```swift
dependencies: [
.package(url: "https://github.com/fxm90/LightweightObservable", from: "2.0.0")
]
```
### How to use
The framework provides three classes `Observable`, `PublishSubject` and `Variable`:
- `Observable`: An observable sequence that you can subscribe to, but not change the underlying value (immutable). This is useful to avoid side-effects on an internal API.
- `PublishSubject`: Subclass of `Observable` that starts empty and only emits new elements to subscribers (mutable).
- `Variable`: Subclass of `Observable` that starts with an initial value and replays it or the latest element to new subscribers (mutable).
#### – Create and update a `PublishSubject`
A `PublishSubject` starts empty and only emits new elements to subscribers.
```swift
let userLocationSubject = PublishSubject()
// ...
userLocationSubject.update(receivedUserLocation)
```
#### – Create and update a `Variable`
A `Variable` starts with an initial value and replays it or the latest element to new subscribers.
```swift
let formattedTimeSubject = Variable("4:20 PM")
// ...
formattedTimeSubject.value = "4:21 PM"
```
#### – Create an `Observable`
Initializing an observable directly is not possible, as this would lead to a sequence that will never change. Instead you need to cast a `PublishSubject` or a `Variable` to an Observable.
```swift
var formattedTime: Observable {
formattedTimeSubject
}
```
```swift
lazy var formattedTime: Observable = formattedTimeSubject
```
#### – Subscribe to changes
A subscriber will be informed at different times, depending on the corresponding subclass of the observable:
- `PublishSubject`: Starts empty and only emits new elements to subscribers.
- `Variable`: Starts with an initial value and replays it or the latest element to new subscribers.
##### – Closure based subscription
**Declaration**
```swift
func subscribe(_ observer: @escaping Observer) -> Disposable
```
Use this method to subscribe to an observable via a closure:
```swift
formattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in
self?.timeLabel.text = newFormattedTime
}
```
Please notice that the old value (`oldFormattedTime`) is an optional of the underlying type, as we might not have this value on the initial call to the subscriber.
**Important:** To avoid retain cycles and/or crashes, **always** use `[weak self]` when self is needed by an observer.
##### - KeyPath based subscription
**Declaration**
```swift
func bind(to keyPath: ReferenceWritableKeyPath, on object: Root) -> Disposable
```
It is also possible to use Swift's KeyPath feature to bind an observable directly to a property:
```swift
formattedTime.bind(to: \.text, on: timeLabel)
```
#### – Memory Management (`Disposable` / `DisposeBag`)
When you subscribe to an `Observable` the method returns a `Disposable`, which is basically a reference to the new subscription.
We need to maintain it, in order to properly control the life-cycle of that subscription.
Let me explain you why in a little example:
> Imagine having a MVVM application using a service layer for network calls. A service is used as a singleton across the entire app.
>
> The view-model has a reference to a service and subscribes to an observable property of this service. The subscription-closure is now saved inside the observable property on the service.
>
> If the view-model gets deallocated (e.g. due to a dismissed view-controller), without noticing the observable property somehow, the subscription-closure would continue to be alive.
>
> As a workaround, we store the returned disposable from the subscription on the view-model. On deallocation of the disposable, it automatically informs the observable property to remove the referenced subscription closure.
In case you only use a single subscriber you can store the returned `Disposable` to a variable:
```swift
// MARK: - Using `subscribe(_:)`
let disposable = formattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in
self?.timeLabel.text = newFormattedTime
}
// MARK: - Using a `bind(to:on:)`
let disposable = dateTimeViewModel
.formattedTime
.bind(to: \.text, on: timeLabel)
```
In case you're having multiple observers, you can store all returned `Disposable` in an array of `Disposable`. (To match the syntax from [RxSwift](https://github.com/ReactiveX/RxSwift), this pod contains a typealias called `DisposeBag`, which is an array of `Disposable`).
```swift
var disposeBag = DisposeBag()
// MARK: - Using `subscribe(_:)`
formattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in
self?.timeLabel.text = newFormattedTime
}.disposed(by: &disposeBag)
formattedDate.subscribe { [weak self] newFormattedDate, oldFormattedDate in
self?.dateLabel.text = newFormattedDate
}.disposed(by: &disposeBag)
// MARK: - Using a `bind(to:on:)`
formattedTime
.bind(to: \.text, on: timeLabel)
.disposed(by: &disposeBag)
formattedDate
.bind(to: \.text, on: dateLabel)
.disposed(by: &disposeBag)
```
A `DisposeBag` is exactly what it says it is, a bag (or array) of disposables.
#### – Observing `Equatable` values
If you create an Observable which underlying type conforms to `Equatable` you can subscribe to changes using a specific filter. Therefore this pod contains the method:
```swift
typealias Filter = (NewValue, OldValue) -> Bool
func subscribe(filter: @escaping Filter, observer: @escaping Observer) -> Disposable {}
```
Using this method, the observer will only be notified on changes if the corresponding filter matches.
This pod comes with one predefined filter method, called `subscribeDistinct`. Subscribing to an observable using this method, will only notify the observer if the new value is different from the old value. This is useful to prevent unnecessary UI-Updates.
Feel free to add more filters, by extending the `Observable` like this:
```swift
extension Observable where T: Equatable {}
```
#### – Getting the current value synchronously
You can get the current value of the `Observable` by accessing the property `value`. However it is always better to subscribe to a given observable! This **shortcut** should only be used during **testing**.
```swift
XCTAssertEqual(viewModel.formattedTime.value, "4:20")
```
### Sample code
Using the given approach, your view-model could look like this:
```swift
class TimeViewModel {
// MARK: - Public properties
/// The current time as a formatted string (**immutable**).
var formattedTime: Observable {
formattedTimeSubject
}
// MARK: - Private properties
/// The current time as a formatted string (**mutable**).
private let formattedTimeSubject: Variable = Variable("\(Date())")
private var timer: Timer?
// MARK: - Initializer
init() {
// Update variable with current time every second.
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] _ in
self?.formattedTimeSubject.value = "\(Date())"
})
}
```
And your view controller like this:
```swift
class TimeViewController: UIViewController {
// MARK: - Outlets
@IBOutlet private var timeLabel: UILabel!
// MARK: - Private properties
/// The view model calculating the current time.
private let timeViewModel = TimeViewModel()
/// The dispose bag for this view controller. On it's deallocation, it removes the
/// subscription-closures from the corresponding observable-properties.
private var disposeBag = DisposeBag()
// MARK: - Public methods
override func viewDidLoad() {
super.viewDidLoad()
timeViewModel
.formattedTime
.bind(to: \.text, on: timeLabel)
.disposed(by: &disposeBag)
}
```
Feel free to check out the example application as well for a better understanding of this approach 🙂
### Author
Felix Mau (me(@)felix.hamburg)
### License
LightweightObservable is available under the MIT license. See the LICENSE file for more info.