📬 A lightweight implementation of an observable sequence that you can subscribe to.

Overview

Header

Swift Version CI Status Code Coverage Version License Platform

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. However I needed something that was syntactically closer to 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

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+

Projects targeting iOS >= 13.0

In case your minimum required version is greater equal iOS 13.0, I highly recommend using Combine instead of adding Lightweight Observable as a dependency. If you rely on having a current and previous value in your subscription closure, please have a look at this extension: Combine+Pairwise.swift.

Integration

CocoaPods

CocoaPods 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:

pod 'LightweightObservable', '~> 2.0'
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:

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 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.

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.

let userLocationSubject = PublishSubject<CLLocation>()

// ...

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.

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.

var formattedTime: Observable<String> {
    formattedTimeSubject
}
lazy var formattedTime: Observable<String> = 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

func subscribe(_ observer: @escaping Observer) -> Disposable

Use this method to subscribe to an observable via a closure:

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

func bind<Root: AnyObject>(to keyPath: ReferenceWritableKeyPath<Root, Value>, on object: Root) -> Disposable

It is also possible to use Swift's KeyPath feature to bind an observable directly to a property:

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:

// 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, this pod contains a typealias called DisposeBag, which is an array of Disposable).

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:

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:

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.

XCTAssertEqual(viewModel.formattedTime.value, "4:20")

Sample code

Using the given approach, your view-model could look like this:

class TimeViewModel {
    // MARK: - Public properties

    /// The current time as a formatted string (**immutable**).
    var formattedTime: Observable<String> {
        formattedTimeSubject
    }

    // MARK: - Private properties

    /// The current time as a formatted string (**mutable**).
    private let formattedTimeSubject: Variable<String> = 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:

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.

Comments
  • Minimum deployment target 15

    Minimum deployment target 15

    Hi! Can you please lower the minimum deployment target? I am using GradientLoadingBar which uses latest version of LightweightObservable, and my project has it's target set to iOS 13. p.s. i am using carthage

    Thanks

    bug 
    opened by alinfarcas12 3
  • Compilation error with Xcode 11

    Compilation error with Xcode 11

    Hi,

    Using the master branch (and GradientLoadingBar), i still have 2 errors:

    Initializer does not override a designated initializer from its superclass 'Observable<T>' initializer is inaccessible due to 'private' protection level

    in Observable.swift, line 106

    Thanks

    opened by skrew 2
  • Compiling error

    Compiling error

    CompileSwiftSources normal x86_64 com.apple.xcode.tools.swift.compiler (in target 'LightweightObservable' from project 'Pods') on Xcode Version 11.0 beta 4 (11M374r)

    opened by EAlsharkawy 2
  • Add combine support

    Add combine support

    Open Tasks

    • [x] Check CI
    • [x] Check CocoaPods integration works
    • [x] Check Carthage integration works
    • [x] Check SPM integration works
    • [x] Update documentation for Combine extension
    • [x] Update README.md
    opened by fxm90 1
  • 🐛 :: Append closure to observers before executing

    🐛 :: Append closure to observers before executing

    When subscribing to a Variable<T> the initial value triggers the provided closure. If the closure makes changes to the underlying value of the same Variable<T> the closure does not re-trigger on its first run.

    Sample Code Showcasing Issue

    enum ExampleEnum {
        case initialValue
        case secondaryValue
    }
    
    let exampleVariable = Variable<ExampleEnum>(.initialValue)
    
    let disposable = exampleVariable.subscribe({ new, old in
        if new == .initialValue {
            print("InitialValue")
            exampleVariable.value = .secondaryValue
        }
        if new == .secondaryValue {
            print("SecondaryValue")
        }
    })
    

    Expected Output InitialValue SecondaryValue

    Actual Output InitialValue

    Solution The proposed changes make it so the closure is added to the observer list before it's executed for the first time

    bug 
    opened by dudek-j 1
Owner
Felix M.
iOS & Frontend Developer 📲 https://twitter.com/_fxm90 🐦 https://codepen.io/fxm90 🖥 https://instagram.com/fxm90 📸
Felix M.
RxReduce is a lightweight framework that ease the implementation of a state container pattern in a Reactive Programming compliant way.

About Architecture concerns RxReduce Installation The key principles How to use RxReduce Tools and dependencies Travis CI Frameworks Platform Licence

RxSwift Community 125 Jan 29, 2022
Open source implementation of Apple's Combine framework for processing values over time.

OpenCombine Open-source implementation of Apple's Combine framework for processing values over time. The main goal of this project is to provide a com

OpenCombine 2.4k Jan 2, 2023
Implementation of the repository pattern in Swift, using generics.

Store Simple, powerful and elegant implementation of the repository pattern, using generics. Why? ?? There are a couple of ways to implement the Repos

Narek Mailian 2 Aug 31, 2022
Simple and lightweight Functional Reactive Coding in Swift for the rest of us

The simplest Observable<T> implementation for Functional Reactive Programming you will ever find. This library does not use the term FRP (Functional R

Jens Ravens 1.1k Jan 3, 2023
Lightweight observations and bindings in Swift

What is Hanson? Hanson is a simple, lightweight library to observe and bind values in Swift. It's been developed to support the MVVM architecture in o

Blendle 526 Oct 18, 2022
Simple, lightweight swift bindings

Bindy Just a simple bindings. Installation Add pod 'Bindy' to your podfile, and run pod install SPM is supported too. Usage For now, Bindy has a coupl

Maxim Kotliar 25 Dec 12, 2022
A lightweight, event-driven architectural framework

Trellis Trellis features a declarative DSL that simplifies service bootstrapping: let cluster = try await Bootstrap { Group { Store(model:

Valentin Radu 25 Aug 16, 2022
A lightweight Elm-like Store for SwiftUI

ObservableStore A simple Elm-like Store for SwiftUI, based on ObservableObject. ObservableStore helps you craft more reliable apps by centralizing all

Subconscious 28 Nov 8, 2022
An array class implemented in Swift that can be observed using ReactiveCocoa's Signals

ReactiveArray An array class implemented in Swift that can be observed using ReactiveCocoa's Signals. Installation Carthage Add the following to your

Wolox 53 Jan 29, 2022
📬 A lightweight implementation of an observable sequence that you can subscribe to.

Features Lightweight Observable is a simple implementation of an observable sequence that you can subscribe to. The framework is designed to be minima

Felix M. 133 Aug 17, 2022
Soundable allows you to play sounds, single and in sequence, in a very easy way

Overview Soundable is a tiny library that uses AVFoundation to manage the playing of sounds in iOS applications in a simple and easy way. You can play

Luis Cárdenas 89 Nov 21, 2022
Publish–subscribe design pattern implementation framework, with an ability to publish events by topic.

TopicEventBus Publish–subscribe design pattern implementation framework, with ability to publish events by topic. (NotificationCenter extended alterna

Matan Abravanel 55 Nov 29, 2021
Publish–subscribe design pattern implementation framework, with an ability to publish events by topic.

TopicEventBus Publish–subscribe design pattern implementation framework, with ability to publish events by topic. (NotificationCenter extended alterna

Matan Abravanel 55 Nov 29, 2021
Very simple Observable and Publisher implementation for iOS apps.

Very simple Observable and Publisher implementation for iOS apps.

Igor Kulman 7 Jun 11, 2022
A clock for iOS based off of the famous fibonacci sequence

Fibonacc iClock Fibonacc iClock is a fibonacci clock implementation for iOS. This project is based off of Thiago Sá's implementation of Philippe Chrét

null 10 Dec 16, 2022
SwiftEventBus - A publish/subscribe EventBus optimized for iOS

Allows publish-subscribe-style communication between components without requiring the components to explicitly be aware of each other

César Ferreira 1k Jan 6, 2023
TopicEventBus is Easy to use, type safe way of implementing Publish–subscribe design pattern.

TopicEventBus Publish–subscribe design pattern implementation framework, with ability to publish events by topic. (NotificationCenter extended alterna

Matan Abravanel 55 Nov 29, 2021
A publish/subscribe EventBus optimized for iOS

SwiftEventBus Allows publish-subscribe-style communication between components without requiring the components to explicitly be aware of each other Fe

César Ferreira 1k Dec 15, 2022
Observable is the easiest way to observe values in Swift.

Observable is the easiest way to observe values in Swift. How to Create an Observable and MutableObservable Using MutableObservable you can create and

Robert-Hein Hooijmans 368 Nov 9, 2022
Rx is a generic abstraction of computation expressed through Observable Element interface,

Rx is a generic abstraction of computation expressed through Observable<Element> interface, which lets you broadcast and subscribe to values and other events from an Observable stream.

ReactiveX 23.1k Dec 31, 2022