{"id":883,"url":"https://github.com/attaswift/GlueKit","last_synced_at":"2025-07-30T19:32:56.232Z","repository":{"id":55479936,"uuid":"47154474","full_name":"attaswift/GlueKit","owner":"attaswift","description":"Type-safe observable values and collections in Swift","archived":false,"fork":false,"pushed_at":"2022-02-23T10:18:49.000Z","size":1240,"stargazers_count":361,"open_issues_count":4,"forks_count":23,"subscribers_count":8,"default_branch":"master","last_synced_at":"2024-05-21T02:27:20.755Z","etag":null,"topics":[],"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/attaswift.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-12-01T00:19:35.000Z","updated_at":"2024-05-17T16:44:05.000Z","dependencies_parsed_at":"2022-08-15T01:20:20.551Z","dependency_job_id":null,"html_url":"https://github.com/attaswift/GlueKit","commit_stats":null,"previous_names":["lorentey/gluekit"],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/attaswift%2FGlueKit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/attaswift%2FGlueKit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/attaswift%2FGlueKit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/attaswift%2FGlueKit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/attaswift","download_url":"https://codeload.github.com/attaswift/GlueKit/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228178962,"owners_count":17881114,"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":[],"created_at":"2024-01-05T20:15:33.832Z","updated_at":"2024-12-04T19:32:13.519Z","avatar_url":"https://github.com/attaswift.png","language":"Swift","funding_links":[],"categories":["Code Quality"],"sub_categories":["Other free courses","Getting Started"],"readme":"# GlueKit\n\n[![Swift 3](https://img.shields.io/badge/Swift-3.0-blue.svg)](https://swift.org) \n[![License](https://img.shields.io/badge/licence-MIT-blue.svg)](https://github.com/attaswift/GlueKit/blob/master/LICENSE.md)\n[![Platform](https://img.shields.io/badge/platforms-macOS%20∙%20iOS%20∙%20watchOS%20∙%20tvOS-blue.svg)](https://developer.apple.com/platforms/)\n\n[![Build Status](https://travis-ci.org/attaswift/GlueKit.svg?branch=master)](https://travis-ci.org/attaswift/GlueKit)\n[![Code Coverage](https://codecov.io/github/attaswift/GlueKit/coverage.svg?branch=master)](https://codecov.io/github/attaswift/GlueKit?branch=master)\n\n[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg)](https://github.com/Carthage/Carthage)\n[![CocoaPod Version](https://img.shields.io/cocoapods/v/GlueKit.svg)](http://cocoapods.org/pods/GlueKit)\n\n\u003e :warning: **WARNING** :warning: This project is in a _prerelease_ state. There\n\u003e is active work going on that will result in API changes that can/will break\n\u003e code while things are finished. Use with caution.\n\nGlueKit is a Swift framework for creating observables and manipulating them in interesting and useful ways.\nIt is called GlueKit because it lets you stick stuff together. \n\nGlueKit contains type-safe analogues for Cocoa's [Key-Value Coding][KVC] and [Key-Value Observing][KVO] subsystems, \nwritten in pure Swift.\nBesides providing the basic observation mechanism, GlueKit also supports full-blown *key path*\nobserving, where a sequence of properties starting at a particular entity is observed at once. (E.g., you can observe\na person's best friend's favorite color, which might change whenever the person gets a new best friend, or when the friend\nchanges their mind about which color they like best.)\n\n[KVC]: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/KeyValueCoding/Articles/Overview.html\n[KVO]: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html\n\n(Note though that GlueKit's keys are functions so they aren't as easy to serialize as KVC's string-based keys and key paths.\nIt is definitely possible to implement serializable type-safe keys in Swift; but it involves some boilerplate code \nthat's better handled by code generation or core language enhancements such as property behaviors or improved \nreflection capabilities.)\n\nLike KVC/KVO, GlueKit supports observing not only individual values, but also collections like sets or arrays.\nThis includes full support for key path observing, too -- e.g., you can observe a person's children's children \nas a single set.\nThese observable collections report fine-grained incremental changes (e.g., \"'foo' was inserted at index 5\"), allowing\nyou to efficiently react to their changes.\n\nBeyond key path observing, GlueKit also provides a rich set of transformations and combinations for observables\nas a more flexible and extensible Swift version of KVC's \n[collection operators][KVC ops]. E.g., given an observable array of integers, you can (efficiently!) observe \nthe sum of its elements; you can filter it for elements that match a particular predicate; you can get an observable\nconcatenation of it with another observable array; and you can do much more.\n\n[KVC ops]: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/KeyValueCoding/Articles/CollectionOperators.html\n\nYou can use GlueKit's observable arrays to efficiently provide data to a `UITableView` or `UICollectionView`, including\nproviding them with incremental changes for animated updates. This functionality is roughly equivalent to what\n[`NSFetchedResultsController`][NSFRC] does in Core Data.\n\n[NSFRC]: https://developer.apple.com/reference/coredata/nsfetchedresultscontroller\n\nGlueKit is written in pure Swift; it does not require the Objective-C runtime for its functionality. \nHowever, it does provide easy-to-use adapters that turn KVO-compatible key paths on NSObjects into GlueKit observables.\n\nGlueKit hasn't been officially released yet. Its API is still in flux, and it has wildly outdated and woefully \nincomplete documentation. However, the project is getting close to a feature set that would make a coherent 1.0 version;\nI expect to have a useful first release before the end of 2016.\n\n##  Presentation\n\nKároly gave a talk on GlueKit during [Functional Swift Conference 2016][FunSwift16] in Budapest.\n[Watch the video][funvideo] or [read the slides][slides].\n\n[FunSwift16]: http://2016.funswiftconf.com\n[slides]: https://vellum.tech/assets/FunSwift2016%20-%20GlueKit.pdf\n[funvideo]: https://www.youtube.com/watch?v=98jsahDV4ts\n\n## Installation\n### CocoaPods\n\nIf you use CocoaPods, you can start using GlueKit by including it as a dependency in your  `Podfile`:\n\n```\npod 'GlueKit', :git =\u003e 'https://github.com/attaswift/GlueKit.git'\n```\n\n(There are no official releases of GlueKit yet; the API is incomplete and very unstable for now.)\n\n### Carthage\n\nFor Carthage, add the following line to your `Cartfile`:\n\n```\ngithub \"attaswift/GlueKit\" \"\u003ccommit-hash\u003e\"\n```\n\n(You have to use a specific commit hash, because there are no official releases of GlueKit yet; the API is incomplete and very unstable for now.)\n\n### Swift Package Manager\n\nFor Swift Package Manager, add the following entry to the dependencies list inside your `Package.swift` file:\n\n```\n.Package(url: \"https://github.com/attaswift/GlueKit.git\", branch: master)\n```\n\n### Standalone Development\n\nIf you don't use CocoaPods, Carthage or SPM, you need to clone GlueKit, [BTree][btree] and [SipHash][siphash], \nand add references to their `xcodeproj` files to your project's workspace. You may put the clones wherever you like,\nbut if you use Git for your app development, it is a good idea to set them up as submodules of your app's top-level \nGit repository.\n\n[btree]: https://github.com/attaswift/BTree\n[siphash]: https://github.com/attaswift/SipHash\n\nTo link your application binary with GlueKit, just add `GlueKit.framework`, `BTree.framework` and `SipHash.framework`\nfrom the BTree project to the Embedded Binaries section of your app target's General page in Xcode.\nAs long as the GlueKit and BTree project files are referenced in your workspace, these frameworks will be listed in \nthe \"Choose items to add\" sheet that opens when you click on the \"+\" button of your target's Embedded Binaries list.\n\nThere is no need to do any additional setup beyond adding the framework targets to Embedded Binaries.\n\n### Working on GlueKit Itself\n\nIf you want to do some work on GlueKit on its own, without embedding it in an application, \nsimply clone this repo with the `--recursive` option, open `GlueKit.xcworkspace`, and start hacking.\n\n```\ngit clone --recursive https://github.com/attaswift/GlueKit.git GlueKit\nopen GlueKit/GlueKit.xcworkspace\n```\n\n### Importing GlueKit\n\nOnce you've made GlueKit available in your project, you need to import it at the top of each  `.swift` file in \nwhich you want to use its features:\n\n```\nimport GlueKit\n```\n\n## Similar frameworks\n\nSome of GlueKit's constructs can be matched with those in discrete reactive frameworks, such as \n[ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa), \n[RxSwift](https://github.com/ReactiveX/RxSwift), \n[ReactKit](https://github.com/ReactKit/ReactKit),\n[Interstellar](https://github.com/JensRavens/Interstellar), and others. \nSometimes GlueKit even uses the same name for the same concept. But often it doesn't (sorry).\n\nGlueKit concentrates on creating a useful model for observables, rather than trying to unify \nobservable-like things with task-like things. \nGlueKit explicitly does not attempt to directly model networking operations \n(although a networking support library could certainly use GlueKit to implement some of its features).\nAs such, GlueKit's source/signal/stream concept transmits simple values; it doesn't wrap them in\n `Event`s. \n\n\nI have several reasons I chose to create GlueKit instead of just using a better established and\nbug-free library:\n\n- I wanted to have some experience with reactive stuff, and you can learn a lot about a paradigm by \n  trying to construct its foundations on your own. The idea is that I start simple and add things as \n  I find I need them. I want to see if I arrive at the same problems and solutions as the \n  Smart People who created the popular frameworks. Some common reactive patterns are not obviously \n  right at first glance.\n- I wanted to experiment with reentrant observables, where an observer is allowed to trigger updates \n  to the observable to which it's connected. I found no well-known implementation of Observable that \n  gets this *just right*.\n- Building a library is a really fun diversion!\n\n## Overview\n\n[The GlueKit Overview](https://github.com/attaswift/GlueKit/blob/master/Documentation/Overview.md)\ndescribes the basic concepts of GlueKit.\n\n## Appetizer\n\nLet's say you're writing a bug tracker application that has a list of projects, each with its own \nset of issues. With GlueKit, you'd use `Variable`s to define your model's attributes and relationships:\n\n```Swift\nclass Project {\n    let name: Variable\u003cString\u003e\n    let issues: ArrayVariable\u003cIssue\u003e\n}\n\nclass Account {\n    let name: Variable\u003cString\u003e\n    let email: Variable\u003cString\u003e\n}\n\nclass Issue {\n    let identifier: Variable\u003cString\u003e\n    let owner: Variable\u003cAccount\u003e\n    let isOpen: Variable\u003cBool\u003e\n    let created: Variable\u003cNSDate\u003e\n}\n\nclass Document {\n    let accounts: ArrayVariable\u003cAccount\u003e\n    let projects: ArrayVariable\u003cProject\u003e\n}\n```\n\nYou can use a `let observable: Variable\u003cFoo\u003e` like you would a `var raw: Foo` property, except \nyou need to write `observable.value` whenever you'd write `raw`:\n\n```Swift\n// Raw Swift       ===\u003e      // GlueKit                                    \nvar a = 42          ;        let b = Variable\u003cInt\u003e(42) \nprint(\"a = \\(a)\")   ;        print(\"b = \\(b.value\\)\")\na = 7               ;        b.value = 7\n```\n\nGiven the model above, in Cocoa you could specify key paths for accessing various parts of the model from a\n`Document` instance. For example, to get the email addresses of all issue owners in one big unsorted array, \nyou'd use the Cocoa key path `\"projects.issues.owner.email\"`. GlueKit is able to do this too, although\nit uses a specially constructed Swift closure to represent the key path:\n\n```Swift\nlet cocoaKeyPath: String = \"projects.issues.owner.email\"\n\nlet swiftKeyPath: Document -\u003e AnyObservableValue\u003c[String]\u003e = { document in \n    document.projects.flatMap{$0.issues}.flatMap{$0.owner}.map{$0.email} \n}\n```\n\n(The type declarations are included to make it clear that GlueKit is fully type-safe. Swift's type inference is able\nto find these out automatically, so typically you'd omit specifying types in declarations like this.)\nThe GlueKit syntax is certainly much more verbose, but in exchange it is typesafe, much more flexible, and also extensible. \nPlus, there is a visual difference between selecting a single value (`map`) or a collection of values (`flatMap`), \nwhich alerts you that using this key path might be more expensive than usual. (GlueKit's key paths are really just \ncombinations of observables. `map` is a combinator that is used to build one-to-one key paths; there are many other\ninteresting combinators available.)\n\nIn Cocoa, you would get the current list of emails using KVC's accessor method. In GlueKit, if you give the key path a\ndocument instance, it returns an `AnyObservableValue` that has a `value` property that you can get. \n\n```Swift\nlet document: Document = ...\nlet cocoaEmails: AnyObject? = document.valueForKeyPath(cocoaKeyPath)\nlet swiftEmails: [String] = swiftKeyPath(document).value\n```\n\nIn both cases, you get an array of strings. However, Cocoa returns it as an optional `AnyObject` that you'll need to\nunwrap and cast to the correct type yourself (you'll want to hold your nose while doing so). Boo! \nGlueKit knows what type the result is going to be, so it gives it to you straight. Yay!\n\nNeither Cocoa nor GlueKit allows you to update the value at the end of this key path; however, with Cocoa, you only find\nthis out at runtime, while with GlueKit, you get a nice compiler error:\n\n```Swift\n// Cocoa: Compiles fine, but oops, crash at runtime\ndocument.setValue(\"karoly@example.com\", forKeyPath: cocoaKeyPath)\n// GlueKit/Swift: error: cannot assign to property: 'value' is a get-only property\nswiftKeyPath(document).value = \"karoly@example.com\"\n```\n\nYou'll be happy to know that one-to-one key paths are assignable in both Cocoa and GlueKit:\n\n```Swift\nlet issue: Issue = ...\n/* Cocoa */   issue.setValue(\"karoly@example.com\", forKeyPath: \"owner.email\") // OK\n/* GlueKit */ issue.owner.map{$0.email}.value = \"karoly@example.com\"  // OK\n```\n\n(In GlueKit, you generally just use the observable combinators directly instead of creating key path entities.\nSo we're going to do that from now on. Serializable type-safe key paths require additional work, which is better\nprovided by a potentional future model object framework built on top of GlueKit.)\n\nMore interestingly, you can ask to be notified whenever a key path changes its value.\n\n```Swift\n// GlueKit\nlet c = document.projects.flatMap{$0.issues}.flatMap{$0.owner}.map{$0.name}.subscribe { emails in \n    print(\"Owners' email addresses are: \\(emails)\")\n}\n// Call c.disconnect() when you get bored of getting so many emails.\n\n// Cocoa\nclass Foo {\n    static let context: Int8 = 0\n    let document: Document\n    \n    init(document: Document) {\n        self.document = document\n        document.addObserver(self, forKeyPath: \"projects.issues.owner.email\", options: .New, context:\u0026context)\n    }\n    deinit {\n        document.removeObserver(self, forKeyPath: \"projects.issues.owner.email\", context: \u0026context)\n    }\n    func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, \n                                change change: [String : AnyObject]?, \n                                context context: UnsafeMutablePointer\u003cVoid\u003e) {\n        if context == \u0026self.context {\n\t    print(\"Owners' email addresses are: \\(change[NSKeyValueChangeNewKey]))\n        }\n        else {\n            super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)\n        }\n    }\n}\n```\n\nWell, Cocoa is a mouthful, but people tend to wrap this up in their own abstractions. In both cases, a new set of emails is\nprinted whenever the list of projects changes, or the list of issues belonging to any project changes, or the owner of any\nissue changes, or if the email address is changed on an individual account.\n\n\nTo present a more down-to-earth example, let's say you want to create a view model for a project summary screen that\ndisplays various useful data about the currently selected project. GlueKit's observable combinators make it simple to\nput together data derived from our model objects. The resulting fields in the view model are themselves observable,\nand react to changes to any of their dependencies on their own.\n\n```Swift\nclass ProjectSummaryViewModel {\n    let currentDocument: Variable\u003cDocument\u003e = ...\n    let currentAccount: Variable\u003cAccount?\u003e = ...\n    \n    let project: Variable\u003cProject\u003e = ...\n    \n    /// The name of the current project.\n\tvar projectName: Updatable\u003cString\u003e { \n\t    return project.map { $0.name } \n\t}\n\t\n    /// The number of issues (open and closed) in the current project.\n\tvar isssueCount: AnyObservableValue\u003cInt\u003e { \n\t    return project.selectCount { $0.issues }\n\t}\n\t\n    /// The number of open issues in the current project.\n\tvar openIssueCount: AnyObservableValue\u003cInt\u003e { \n\t    return project.selectCount({ $0.issues }, filteredBy: { $0.isOpen })\n\t}\n\t\n    /// The ratio of open issues to all issues, in percentage points.\n    var percentageOfOpenIssues: AnyObservableValue\u003cInt\u003e {\n        // You can use the standard arithmetic operators to combine observables.\n    \treturn AnyObservableValue.constant(100) * openIssueCount / issueCount\n    }\n    \n    /// The number of open issues assigned to the current account.\n    var yourOpenIssues: AnyObservableValue\u003cInt\u003e {\n        return project\n            .selectCount({ $0.issues }, \n                filteredBy: { $0.isOpen \u0026\u0026 $0.owner == self.currentAccount })\n    }\n    \n    /// The five most recently created issues assigned to the current account.\n    var yourFiveMostRecentIssues: AnyObservableValue\u003c[Issue]\u003e {\n        return project\n            .selectFirstN(5, { $0.issues }, \n                filteredBy: { $0.isOpen \u0026\u0026 $0.owner == currentAccount }),\n                orderBy: { $0.created \u003c $1.created })\n    }\n\n    /// An observable version of NSLocale.currentLocale().\n    var currentLocale: AnyObservableValue\u003cNSLocale\u003e {\n        let center = NSNotificationCenter.defaultCenter()\n\t\tlet localeSource = center\n\t\t    .source(forName: NSCurrentLocaleDidChangeNotification)\n\t\t    .map { _ in NSLocale.currentLocale() }\n        return AnyObservableValue(getter: { NSLocale.currentLocale() }, futureValues: localeSource)\n    }\n    \n    /// An observable localized string.\n    var localizedIssueCountFormat: AnyObservableValue\u003cString\u003e {\n        return currentLocale.map { _ in \n            return NSLocalizedString(\"%1$d of %2$d issues open (%3$d%%)\",\n                comment: \"Summary of open issues in a project\")\n        }\n    }\n    \n    /// An observable text for a label.\n    var localizedIssueCountString: AnyObservableValue\u003cString\u003e {\n        return AnyObservableValue\n            // Create an observable of tuples containing values of four observables\n            .combine(localizedIssueCountFormat, issueCount, openIssueCount, percentageOfOpenIssues)\n            // Then convert each tuple into a single localized string\n            .map { format, all, open, percent in \n                return String(format: format, open, all, percent)\n            }\n    }\n}\n```\n\n(Note that some of the operations above aren't implemented yet. Stay tuned!)\n\nWhenever the model is updated or another project or account is selected, the affected `Observable`s \nin the view model are recalculated accordingly, and their subscribers are notified with the updated\nvalues. \nGlueKit does this in a surprisingly efficient manner---for example, closing an issue in\na project will simply decrement a counter inside `openIssueCount`; it won't recalculate the issue\ncount from scratch. (Obviously, if the user switches to a new project, that change will trigger a recalculation of that project's issue counts from scratch.) Observables aren't actually calculating anything until and unless they have subscribers.\n\nOnce you have this view model, the view controller can simply subscribe its observables to various\nlabels displayed in the view hierarchy:\n\n```Swift\nclass ProjectSummaryViewController: UIViewController {\n    private let visibleConnections = Connector()\n    let viewModel: ProjectSummaryViewModel\n    \n    // ...\n    \n    override func viewWillAppear() {\n        super.viewWillAppear()\n        \n\t    viewModel.projectName.values\n\t        .subscribe { name in\n\t            self.titleLabel.text = name\n\t        }\n\t        .putInto(visibleConnections)\n\t     \n\t    viewModel.localizedIssueCountString.values\n\t        .subscribe { text in\n\t            self.subtitleLabel.text = text\n\t        }\n\t        .putInto(visibleConnections)\n\t        \n        // etc. for the rest of the observables in the view model\n    }\n    \n    override func viewDidDisappear() {\n        super.viewDidDisappear()\n        visibleConnections.disconnect()\n    }\n}\n```\n\nSetting up the connections in `viewWillAppear` ensures that the view model's complex observer\ncombinations are kept up to date only while the project summary is displayed on screen.\n\nThe `projectName` property in `ProjectSummaryViewModel` is declared an `Updatable`, so you can \nmodify its value. Doing that updates the name of the current project: \n\n```Swift\nviewModel.projectName.value = \"GlueKit\"   // Sets the current project's name via a key path\nprint(viewModel.project.name.value)       // Prints \"GlueKit\"\n```\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fattaswift%2FGlueKit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fattaswift%2FGlueKit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fattaswift%2FGlueKit/lists"}