{"id":19106564,"url":"https://github.com/fxm90/lightweightobservable","last_synced_at":"2025-08-20T16:33:00.882Z","repository":{"id":34895772,"uuid":"183866852","full_name":"fxm90/LightweightObservable","owner":"fxm90","description":"📬 A lightweight implementation of an observable sequence that you can subscribe to.","archived":false,"fork":false,"pushed_at":"2022-09-18T11:09:10.000Z","size":132459,"stargazers_count":135,"open_issues_count":0,"forks_count":9,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-12-17T06:03:47.414Z","etag":null,"topics":["cocoapods","ios","mvvm","observable","observer","reactive-programming","rx","swift"],"latest_commit_sha":null,"homepage":"https://github.com/fxm90/LightweightObservable/","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fxm90.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-04-28T06:37:06.000Z","updated_at":"2024-08-23T01:15:07.000Z","dependencies_parsed_at":"2022-08-08T02:15:43.638Z","dependency_job_id":null,"html_url":"https://github.com/fxm90/LightweightObservable","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fxm90%2FLightweightObservable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fxm90%2FLightweightObservable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fxm90%2FLightweightObservable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fxm90%2FLightweightObservable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fxm90","download_url":"https://codeload.github.com/fxm90/LightweightObservable/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230438185,"owners_count":18225870,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cocoapods","ios","mvvm","observable","observer","reactive-programming","rx","swift"],"created_at":"2024-11-09T04:08:43.381Z","updated_at":"2024-12-19T13:08:33.327Z","avatar_url":"https://github.com/fxm90.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Header][header]\n\n\u003cp align=\"center\"\u003e\n\t\u003cimg src=\"https://img.shields.io/badge/Swift-5.0-green.svg?style=flat\" alt=\"Swift Version\" /\u003e\n\t\u003cimg src=\"https://img.shields.io/github/workflow/status/fxm90/LightweightObservable/Continuous%20Integration\" alt=\"CI Status\" /\u003e\n\t\u003cimg src=\"https://img.shields.io/codecov/c/github/fxm90/LightweightObservable.svg?style=flat\" alt=\"Code Coverage\" /\u003e\n\t\u003cimg src=\"https://img.shields.io/cocoapods/v/LightweightObservable.svg?style=flat\" alt=\"Version\" /\u003e\n\t\u003cimg src=\"https://img.shields.io/cocoapods/l/LightweightObservable.svg?style=flat\" alt=\"License\" /\u003e\n\t\u003cimg src=\"https://img.shields.io/cocoapods/p/LightweightObservable.svg?style=flat\" alt=\"Platform\" /\u003e\n\u003c/p\u003e\n\n## Features\n\nLightweight 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.\n\n##### Credits\nThe 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.\n\n##### Migration Guide\nIf you want to update from version 1.x.x, please have a look at the [Lightweight Observable 2.0 Migration Guide\n](Documentation/Lightweight%20Observable%202.0%20Migration%20Guide.md)\n\n### Example\nTo run the example project, clone the repo, and open the workspace from the Example directory.\n\n### Requirements\n- Swift 5.5\n- Xcode 13.2+\n- iOS 9.0+\n\n### Projects targeting iOS \u003e= 13.0\nIn case your minimum required version is greater equal iOS 13.0, I recommend using [Combine](https://developer.apple.com/documentation/combine) instead of adding `Lightweight Observable` as a dependency. \n\nIf you rely on having a current and previous value in your subscription closure, please have a look at this extension: [Combine+Pairwise.swift](https://gist.github.com/fxm90/be62335d987016c84d2f8b3731197c98).\n\n#### Update: Since version `2.2` an `Observable` instance conforms to the [`Publisher`](https://developer.apple.com/documentation/combine/publisher) protocol from Swift's `Combine` 🎉 \n\nThis makes transitioning from `LightweightObservable` to `Combine` a lot easier, as you can use features from `Combine` without having to change the underlying `Observable` to a `Publisher`. \n\nExample Code for using `Combine` functions on an instance of `PublishSubject`:\n\n```swift\nvar subscriptions = Set\u003cAnyCancellable\u003e()\n            \nlet publishSubject = PublishSubject\u003cInt\u003e()\npublishSubject\n    .map { $0 * 2 }\n    .sink { print($0) }\n    .store(in: \u0026subscriptions)\n\npublishSubject.update(1) // Prints \"2\"\npublishSubject.update(2) // Prints \"4\"\npublishSubject.update(3) // Prints \"6\"\n```\n\n**Cheatsheet**\n\n| `LightweightObservable` | `Combine`             |\n| ----------------------- | --------------------- |\n| `PublishSubject`        | `PassthroughSubject`  |\n| `Variable`              | `CurrentValueSubject` |\n\n\nFurthermore, using the property [`values`](https://developer.apple.com/documentation/combine/publisher/values-1dm9r) of a `Combine.Publisher`, you can use an `Observable` in an asynchronous sequence:\n\n```swift\nfor await value in observable.values {\n    // ...\n}\n```\n\n\n### Integration\n##### CocoaPods\n[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`:\n\n```ruby\npod 'LightweightObservable', '~\u003e 2.0'\n```\n\n##### Carthage\n[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`:\n\n```ogdl\ngithub \"fxm90/LightweightObservable\" ~\u003e 2.0\n```\nRun carthage update to build the framework and drag the built `LightweightObservable.framework` into your Xcode project.\n\n\n##### Swift Package Manager\nThe [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.\n\nOnce 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`.\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/fxm90/LightweightObservable\", from: \"2.0.0\")\n]\n```\n\n\n### How to use\nThe framework provides three classes `Observable`, `PublishSubject` and `Variable`:\n\n - `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.\n - `PublishSubject`: Subclass of `Observable` that starts empty and only emits new elements to subscribers (mutable).\n - `Variable`: Subclass of `Observable` that starts with an initial value and replays it or the latest element to new subscribers (mutable).\n\n#### – Create and update a `PublishSubject`\nA `PublishSubject` starts empty and only emits new elements to subscribers.\n\n```swift\nlet userLocationSubject = PublishSubject\u003cCLLocation\u003e()\n\n// ...\n\nuserLocationSubject.update(receivedUserLocation)\n```\n\n#### – Create and update a `Variable`\nA `Variable` starts with an initial value and replays it or the latest element to new subscribers.\n\n```swift\nlet formattedTimeSubject = Variable(\"4:20 PM\")\n\n// ...\n\nformattedTimeSubject.value = \"4:21 PM\"\n// or\nformattedTimeSubject.update(\"4:21 PM\")\n```\n\n#### – Create an `Observable`\nInitializing 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.\n\n```swift\nvar formattedTime: Observable\u003cString\u003e {\n    formattedTimeSubject\n}\n```\n```swift\nlazy var formattedTime: Observable\u003cString\u003e = formattedTimeSubject\n```\n\n#### – Subscribe to changes\nA subscriber will be informed at different times, depending on the subclass of the corresponding observable:\n\n - `PublishSubject`: Starts empty and only emits new elements to subscribers.\n - `Variable`: Starts with an initial value and replays it or the latest element to new subscribers.\n\n##### – Closure based subscription\n**Declaration**\n\n```swift\nfunc subscribe(_ observer: @escaping Observer) -\u003e Disposable\n```\n\nUse this method to subscribe to an observable via a closure:\n\n```swift\nformattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in\n    self?.timeLabel.text = newFormattedTime\n}\n```\n\nPlease 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.\n\n**Important:** To avoid retain cycles and/or crashes, **always** use `[weak self]` when an instance of `self` is needed by an observer.\n\n##### - KeyPath based subscription\n**Declaration**\n\n```swift\nfunc bind\u003cRoot: AnyObject\u003e(to keyPath: ReferenceWritableKeyPath\u003cRoot, Value\u003e, on object: Root) -\u003e Disposable\n```\n\nIt is also possible to use Swift's KeyPath feature to bind an observable directly to a property:\n\n```swift\nformattedTime.bind(to: \\.text, on: timeLabel)\n```\n\n#### – Memory Management (`Disposable` / `DisposeBag`)\n\nWhen you subscribe to an `Observable` the method returns a `Disposable`, which is basically a reference to the new subscription.\n\nWe need to maintain it, in order to properly control the life-cycle of that subscription.\n\nLet me explain you why in a little example:\n\n\u003e Imagine having a MVVM application using a service layer for network calls. A service is used as a singleton across the entire app.\n\u003e\n\u003e 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.\n\u003e\n\u003e 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.\n\u003e\n\u003e 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.\n\nIn case you only use a single subscriber you can store the returned `Disposable` to a variable:\n\n```swift\n// MARK: - Using `subscribe(_:)`\n\nlet disposable = formattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in\n    self?.timeLabel.text = newFormattedTime\n}\n\n// MARK: - Using a `bind(to:on:)`\n\nlet disposable = dateTimeViewModel\n    .formattedTime\n    .bind(to: \\.text, on: timeLabel)\n```\n\nIn 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`).\n\n```swift\nvar disposeBag = DisposeBag()\n\n// MARK: - Using `subscribe(_:)`\n\nformattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in\n    self?.timeLabel.text = newFormattedTime\n}.disposed(by: \u0026disposeBag)\n\nformattedDate.subscribe { [weak self] newFormattedDate, oldFormattedDate in\n    self?.dateLabel.text = newFormattedDate\n}.disposed(by: \u0026disposeBag)\n\n// MARK: - Using a `bind(to:on:)`\n\nformattedTime\n    .bind(to: \\.text, on: timeLabel)\n    .disposed(by: \u0026disposeBag)\n\nformattedDate\n    .bind(to: \\.text, on: dateLabel)\n    .disposed(by: \u0026disposeBag)\n```\n\nA `DisposeBag` is exactly what it says it is, a bag (or array) of disposables.\n\n#### – Observing `Equatable` values\nIf 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:\n\n```swift\ntypealias Filter = (NewValue, OldValue) -\u003e Bool\n\nfunc subscribe(filter: @escaping Filter, observer: @escaping Observer) -\u003e Disposable {}\n```\n\nUsing this method, the observer will only be notified on changes if the corresponding filter matches (returns `true`).\n\nThis 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.\n\nFeel free to add more filters, by extending the `Observable` like this:\n\n```swift\nextension Observable where T: Equatable {}\n```\n\n#### – Getting the current value synchronously\nYou 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**.\n\n```swift\nXCTAssertEqual(viewModel.formattedTime.value, \"4:20\")\n```\n\n### Sample code\nUsing the given approach, your view-model could look like this:\n\n```swift\nfinal class ViewModel {\n\n    // MARK: - Public properties\n\n    /// The current date and time as a formatted string (**immutable**).\n    var formattedDate: Observable\u003cString\u003e {\n        formattedDateSubject\n    }\n\n    // MARK: - Private properties\n\n    /// The current date and time as a formatted string (**mutable**).\n    private let formattedDateSubject: Variable\u003cString\u003e = Variable(\"\\(Date())\")\n\n    private var timer: Timer?\n\n    // MARK: - Instance Lifecycle\n\n    init() {\n        // Update variable with current date and time every second.\n        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in\n            self?.formattedDateSubject.value = \"\\(Date())\"\n        }\n    }\n```\n\nAnd your view controller like this:\n\n```swift\nfinal class ViewController: UIViewController {\n\n    // MARK: - Outlets\n\n    @IBOutlet private var dateLabel: UILabel!\n\n    // MARK: - Private properties\n\n    private let viewModel = ViewModel()\n\n    /// The dispose bag for this view controller. On it's deallocation, it removes the\n    /// subscription-closures from the corresponding observable-properties.\n    private var disposeBag = DisposeBag()\n\n    // MARK: - Public methods\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        viewModel\n            .formattedDate\n            .bind(to: \\.text, on: dateLabel)\n            .disposed(by: \u0026disposeBag)\n    }\n```\nFeel free to check out the example application as well for a better understanding of this approach 🙂\n\n\n### Author\nFelix Mau (me(@)felix.hamburg)\n\n\n### License\nLightweightObservable is available under the MIT license. See the LICENSE file for more info.\n\n\n[header]: Assets/header.png\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffxm90%2Flightweightobservable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffxm90%2Flightweightobservable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffxm90%2Flightweightobservable/lists"}