What's New



What's Changed

  • fix(identity): Fixed finding alias collection by @pjechris in #17
  • tech(log): Add Logger mechanism by @pjechris in #18
  • tech(thread): Make access to IdentityMap thread safe by @pjechris in #19

Full Changelog: 0.7.0...0.8.0


swift platforms tests twitter

Simple data synchronisation in plain Swift.


CohesionKit is a small library intended to remedy issues developers face when they try to display realtime data on multiple screens.

It is designed with latest Swift technologies:

  • 📇 Identifiable protocol
  • 🧰 Combine framework
  • 👀 KeyPath

When using it?

  • 🔁 You need to show realtime data (websockets for instance)
  • 🦕 You don't want to use a heavy frameworks like CoreData or Realm
  • 🪶 You look for a lightweight tool
  • 🗃️ You want to use structs


  • Thread safe
  • Lighweight (< 600 lines of code)
  • Simple API
  • Work with plain Swift struct
  • Work with Identifiable objects
  • Support for Combine
  • Use aliases to reference named objects
  • Use (time)stamps to mark you data
  • In-memory storage
  • Release objects you're not actively using (weak memory)


  • Swift Package Manager
dependencies: [
    .package(url: "https://github.com/pjechris/CohesionKit.git", .upToNextMajor(from: "0.7.0"))


This library come a very simple Example project so you can see a real case usage. It mostly show:

  • How to store data in the library
  • How to retrieve and update that data for realtime

Getting started

Store an object

First create an instance of IdentityMap:

let identityMap = IdentityMap()

IdentityMap let you store Identifiable objects:

struct Book: Identifiable {
  let id: String
  let title: String

let book = Book(id: "ABCD", name: "My Book")


Your can then retrieve the object anywhere in your code:

// somewhere else in the code
identityMap.find(Book.self, id: "ABCD") // return Book(id: "ABCD", name: "My Book")

Listening to updates

Every time data is updated in IdentityMap will trigger a notification to any registered observer. To register yourself as an observer just use result from store or find methods:

func findBooks() {
  // 1. load data using URLSession
  // 2. store data in `IdentityMap`
  // 3. return a `publisher` creating an observer
    .map { books in identityMap.store(books).asPublisher }
    .sink... }
    .store(in: &cancellables)
identityMap.find(Book.self, id: 1)?
  .sink { ... }
  .store(in: &cancellables)

CohesionKit has a weak memory policy you should understand.

Relational objects

To store objects containing other objects you need to make them conform to one protocol: Aggregate.

struct AuthorBooks: Aggregate
  var id: Author.ID { author.id }

  let author: Author
  let books: [Book]

  // `nestedEntitiesKeyPaths` must list all Identifiable/Aggregate this object contain
  var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath<Self>] {
    [.init(\.author), .init(\.books)]

CohesionKit will then handle synchronisation for the three entities:

  • AuthorBook
  • Author
  • Book

This allow you to retrieve them independently from each other:

let authorBooks = AuthorBooks(
    author: Author(id: 1, name: "George R.R Martin"),
    books: [
      Book(id: "ACK", title: "A Clash of Kings"),
      Book(id: "ADD", title: "A Dance with Dragons")


identityMap.find(Author.self, id: 1) // George R.R Martin
identityMap.find(Book.self, id: "ACK") // A Clash of Kings
identityMap.find(Book.self, id: "ADD") // A Dance with Dragons

You can also modify any of them however you want:

let newAuthor = Author(id: 1, name: "George R.R MartinI")


identityMap.find(Author.self, id: 1) // George R.R MartinI
identityMap.find(AuthorBooks.self, id: 1 // George R.R MartinI + [A Clash of Kings, A Dance with Dragons]

Advanced topics

Weak memory management

CohesionKit has a weak memory policy: objects are kept in IdentityMap as long as someone use them.

To that end you need to retain observers as long as you're interested in the data:

let book = Book(id: "ACK", title: "A Clash of Kings")
let cancellable = identityMap.store(book) // observer is not retained and no one else observe this book: data is released

identityMap.find(Book.self, id: "ACK") // return  "A Clash of Kings"

If you don't create/retain observers then once entities have no more observers they will be automatically discarded from the storage.

let book = Book(id: "ACK", title: "A Clash of Kings")
_ = identityMap.store(book) // observer is not retained and no one else observe this book: data is released

identityMap.find(Book.self, id: "ACK") // return nil
let book = let book = Book(id: "ACK", title: "A Clash of Kings")
var cancellable = identityMap.store(book).asPublisher.sink... }
let cancellable2 = identityMap.find(Book.self, id: "ACK") // return a publisher

cancellable = nil

identityMap.find(Book.self, id: "ADD") // return "A Clash of Kings" because cancellable2 still observe this book


Sometimes you need to retrieve data without knowing the id. Common scenario is current user.

CohesionKit provide a suitable mechanism: aliases. Aliases allow you to register and find entities using a key.

extension AliasKey where T == User {
  static let currentUser = AliasKey("user")

identityMap.store(currentUser, named: \.currentUser)

Then request it somewhere else:

identityMap.find(named: \.currentUser) // return the current user

Compared to regular entities aliased objects are long-live objects: they will be kept in the storage even if no one observe them. This allow registered observers to be notified when alias value change:

identityMap.removeAlias(named: \.currentUser) // observers will be notified currentUser is nil.

identityMap.store(newCurrentUser, named: \.currentUser) // observers will be notified that currentUser changed even if currentUser was nil before

Stale data

When storing data CohesionKit actually require you to set a modification stamp on it. Stamp is used as a marker to compare data freshness: the higher stamp is the more recent data is.

By default CohesionKit will use the current date as stamp.

identityMap.store(book) // use default stamp: current date
identityMap.store(book, modifiedAt: Date().stamp) // explicitly use Date time stamp
identityMap.store(book, modifiedAt: 9000) // any Double value is valid

If for some reason you try to store data with a stamp lower than the already stamped stored data then the update will be discarded.


This project is released under the MIT License. Please see the LICENSE file for details.


  • Swift Tools 5.3.0
View More Packages from this Author


Last updated: Tue Nov 08 2022 08:36:41 GMT-0500 (GMT-05:00)