{"id":1825,"url":"https://github.com/UrbanCompass/Snail","last_synced_at":"2025-08-02T04:32:40.807Z","repository":{"id":46818206,"uuid":"76066084","full_name":"UrbanCompass/Snail","owner":"UrbanCompass","description":"An observables framework for Swift","archived":true,"fork":false,"pushed_at":"2022-08-23T12:59:53.000Z","size":269,"stargazers_count":180,"open_issues_count":1,"forks_count":14,"subscribers_count":376,"default_branch":"main","last_synced_at":"2025-07-27T06:02:16.180Z","etag":null,"topics":["framework","ios","observables","swift"],"latest_commit_sha":null,"homepage":"","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/UrbanCompass.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2016-12-09T19:59:21.000Z","updated_at":"2025-06-25T20:29:49.000Z","dependencies_parsed_at":"2022-07-26T05:16:35.378Z","dependency_job_id":null,"html_url":"https://github.com/UrbanCompass/Snail","commit_stats":null,"previous_names":[],"tags_count":71,"template":false,"template_full_name":null,"purl":"pkg:github/UrbanCompass/Snail","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/UrbanCompass%2FSnail","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/UrbanCompass%2FSnail/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/UrbanCompass%2FSnail/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/UrbanCompass%2FSnail/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/UrbanCompass","download_url":"https://codeload.github.com/UrbanCompass/Snail/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/UrbanCompass%2FSnail/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268334618,"owners_count":24233793,"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","status":"online","status_checked_at":"2025-08-02T02:00:12.353Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["framework","ios","observables","swift"],"created_at":"2024-01-05T20:15:56.702Z","updated_at":"2025-08-02T04:32:40.413Z","avatar_url":"https://github.com/UrbanCompass.png","language":"Swift","readme":"# 🐌 snail [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ![Cocoapods](https://cocoapod-badges.herokuapp.com/v/Snail/badge.png) ![codecov.io](https://codecov.io/gh/UrbanCompass/snail/branch/master/graphs/badge.svg) ![SwiftPM Compatible](https://img.shields.io/badge/SwiftPM-Compatible-brightgreen.svg)\n\n[![SNAIL](https://img.youtube.com/vi/u4QAnCFd4iw/0.jpg)](https://www.youtube.com/watch?v=u4QAnCFd4iw)\n\nA lightweight observables framework, also available in [Kotlin](https://github.com/UrbanCompass/Snail-Kotlin)\n\n## Installation\n\n### Carthage\n\nYou can install [Carthage](https://github.com/Carthage/Carthage) with [Homebrew](http://brew.sh/) using the following command:\n\n```bash\nbrew update\nbrew install carthage\n```\nTo integrate Snail into your Xcode project using Carthage, specify it in your `Cartfile` where `\"x.x.x\"` is the current release:\n\n```ogdl\ngithub \"UrbanCompass/Snail\" \"x.x.x\"\n```\n\n### Swift Package Manager\n\nTo install using [Swift Package Manager](https://swift.org/package-manager/) have your Swift package set up, and add Snail as a dependency to your `Package.swift`.\n\n```swift\ndependencies: [\n    .Package(url: \"https://github.com/UrbanCompass/Snail.git\", majorVersion: 0)\n]\n```\n\n### Manually\nAdd all the files from `Snail/Snail` to your project\n\n### Developing Locally\n\n1. Run the setup script to install required dependencies `./scripts/setup.sh`\n\n## Creating Observables\n\n```swift\nlet observable = Observable\u003cthing\u003e()\n```\n\n## Disposer\n### What the Disposer IS\n\nThe disposer is used to maintain reference to many subscriptions in a single location. When a disposer is deinitialized, it removes all of its referenced subscriptions from memory. A disposer is usually located in a centralized place where most of the subscriptions happen (ie: UIViewController in an MVVM architecture). Since most of the subscriptions are to different observables, and those observables are tied to type, all the things that are going to be disposed need to comform to `Disposable`. \n\n### What the Disposer IS NOT\n\nThe disposer is not meant to prevent retain cycles. A common example is a `UIViewController` that has reference to a `Disposer` object. A subscription definition might look something like this:\n\n```swift\nextension MyViewController {\n  button.tap.subscribe(onNext: { [weak self] in\n    self?.navigationController.push(newVc)\n  }).add(to: disposer)\n}\n```\nWithout specifying a `[weak self]` capture list in a scenario like this, a retain cycle is created between the subscriber and the view controller. In this example, without the capture list, the view controller will not be deallocated as expected, causing its disposer object to stay in memory as well. Since the `Disposer` removes its referenced subscribers when it is deinitialized, these subscribers will stay in memory as well.\n\nSee [https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html](https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html) for more details on memory management in Swift.\n\n## Closure Wrapper\n\nThe main usage for the `Disposer` is to get rid of subscription closures that we create on `Observables`, but the other usage that we found handy, is the ability to dispose of regular closures. As part of the library, we created a small `Closure` wrapper class that complies with `Disposable`. This way you can wrap simple closures to be disposed. \n\n\n```swift\nlet closureCall = Closure {\n    print(\"We ❤️ Snail\")\n}.add(to: Disposer)\n```\n\nPlease note that this would not dispose of the `closureCall` reference to closure, it would only Dispose the content of the `Closure`.\n\n## Subscribing to Observables\n\n```swift\nobservable.subscribe(\n    onNext: { thing in ... }, // do something with thing\n    onError: { error in ... }, // do something with error\n    onDone: { ... } //do something when it's done\n).add(to: disposer)\n```\n\nClosures are optional too...\n\n```swift\nobservable.subscribe(\n    onNext: { thing in ... } // do something with thing\n).add(to: disposer)\n```\n\n```swift\nobservable.subscribe(\n    onError: { error in ... } // do something with error\n).add(to: disposer)\n```\n\n## Creating Observables Variables\n\n```swift\nlet variable = Variable\u003cwhatever\u003e(some initial value)\n```\n\n```swift\nlet optionalString = Variable\u003cString?\u003e(nil)\noptionalString.asObservable().subscribe(\n    onNext: { string in ... } // do something with value changes\n).add(to: disposer)\n\noptionalString.value = \"something\"\n```\n\n```swift\nlet int = Variable\u003cInt\u003e(12)\nint.asObservable().subscribe(\n    onNext: { int in ... } // do something with value changes\n).add(to: disposer)\n\nint.value = 42\n```\n\n## Combining Observable Variables\n\n\n```swift\nlet isLoaderAnimating = Variable\u003cBool\u003e(false)\nisLoaderAnimating.bind(to: viewModel.isLoading) // forward changes from one Variable to another\n\nviewModel.isLoading = true\nprint(isLoaderAnimating.value) // true\n```\n\n```swift\nObservable.merge([userCreated, userUpdated]).subscribe(\n  onNext: { user in ... } // do something with the latest value that got updated\n}).add(to: disposer)\n\nuserCreated.value = User(name: \"Russell\") // triggers \nuserUpdated.value = User(name: \"Lee\") // triggers \n```\n\n```swift\nObservable.combineLatest((isMapLoading, isListLoading)).subscribe(\n  onNext: { isMapLoading, isListLoading in ... } // do something when both values are set, every time one gets updated\n}).add(to: disposer)\n\nisMapLoading.value = true\nisListLoading.value = true // triggers\n```\n\n## Miscellaneous Observables\n\n```swift\nlet just = Just(1) // always returns the initial value (1 in this case)\n\nenum TestError: Error {\n  case test\n}\nlet failure = Fail(TestError.test) // always fail with error\n\nlet n = 5\nlet replay = Replay(n) // replays the last N events when a new observer subscribes\n```\n\n## Operators\n\nSnail provides some basic operators in order to transform and operate on observables. \n\n- `map`: This operator allows to map the value of an obsverable into another value. Similar to `map` on `Collection` types.\n\n  ```swift\n  let observable = Observable\u003cInt\u003e()\n  let subject = observable.map { \"Number: \\($0)\" }\n  // -\u003e subject emits `String` whenever `observable` emits.\n  ```\n- `filter`: This operator allows filtering out certain values from the observable chain. Similar to `filter` on `Collection` types. You simply return `true` if the value should be emitted and `false` to filter it out.\n\n  ```swift\n  let observable = Observable\u003cInt\u003e()\n  let subject = observable.filter { $0 % 2 == 0 }\n  // -\u003e subject will only emit even numbers.\n  ```\n- `flatMap`: This operator allows mapping values into other observables, for example you may want to create an observable for a network request when a user tap observable emits.\n\n  ```swift\n  let fetchTrigger = Observable\u003cVoid\u003e()\n  let subject = fetchTrigger.flatMap { Variable(100).asObservable() }\n  // -\u003e subject is an `Observable\u003cInt\u003e` that is created when `fetchTrigger` emits.\n  ```\n\n## Subscribing to Control Events\n\n```swift\nlet control = UIControl()\ncontrol.controlEvent(.touchUpInside).subscribe(\n  onNext: { ... }  // do something with thing\n).add(to: disposer)\n\nlet button = UIButton()\nbutton.tap.subscribe(\n  onNext: { ... }  // do something with thing\n).add(to: disposer)\n```\n\n## Queues\n\nYou can specify which queue an observables will be notified on by using `.subscribe(queue: \u003cdesired queue\u003e)`. If you don't specify, then the observable will be notified on the same queue that the observable published on.\n\nThere are 3 scenarios:\n\n1. You don't specify the queue. Your observer will be notified on the same thread as the observable published on.\n\n2. You specified `main` queue AND the observable published on the `main` queue. Your observer will be notified synchronously on the `main` queue.\n\n3. You specified a queue. Your observer will be notified async on the specified queue.\n\n### Examples\n\nSubscribing on `DispatchQueue.main`\n\n```swift\nobservable.subscribe(queue: .main,\n    onNext: { thing in ... }\n).add(to: disposer)\n```\n\n# In Practice\n\n## Subscribing to Notifications\n\n```swift\nNotificationCenter.default.observeEvent(Notification.Name.UIKeyboardWillShow)\n  .subscribe(queue: .main, onNext: { notification in\n    self.keyboardWillShow(notification)\n  }).add(to: disposer)\n```\n\n## Subscribing to Gestures\n\n```swift\nlet panGestureRecognizer = UIPanGestureRecognizer()\npanGestureRecognizer.asObservable()\n  .subscribe(queue: .main, onNext: { sender in\n    // Your code here\n  }).add(to: disposer)\nview.addGestureRecognizer(panGestureRecognizer)\n```\n\n## Subscribing to UIBarButton Taps\n\n```swift\nnavigationItem.leftBarButtonItem?.tap\n  .subscribe(onNext: {\n    self.dismiss(animated: true, completion: nil)\n  }).add(to: disposer)\n```","funding_links":[],"categories":["Libs","Reactive Programming","Events [🔝](#readme)"],"sub_categories":["Events","Other free courses","Prototyping","Other Parsing"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FUrbanCompass%2FSnail","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FUrbanCompass%2FSnail","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FUrbanCompass%2FSnail/lists"}