Relations 0.0.4

Relations 0.0.4

TestsTested
LangLanguage SwiftSwift
License MIT
ReleasedLast Release Apr 2017
SwiftSwift Version 3.0
SPMSupports SPM

Maintained by Anton Bronnikov.



Relations 0.0.4

  • By
  • Anton Bronnikov

Relations

Swift helper-framework facilitating relations between objects.

The following kinds of the relations are supported so far:

Scroll down for installation guidelines.

Weak, 1:1

The facilitator of a weak one-to-one relation between two objects is the generic class Weak1to1Relation.

Consider the following example:

import Relations

class Tile : CustomStringConvertible {

    private var address: UnsafeMutableRawPointer { 
        return Unmanaged.passUnretained(self).toOpaque() 
    }

    var description: String { 
        return "tile.\(address)" 
    }

    private (set) var _tileForComponent: Weak1to1Relation<Tile, Component>! = nil

    var component: Component? {
        get { return _tileForComponent.counterpart?.side }
        set { _tileForComponent.relate(to: newValue?._componentForTile) }
    }

    init() {
        _tileForComponent = Weak1to1Relation(side: self)
    }

}
class Component : CustomStringConvertible {

    private var address: UnsafeMutableRawPointer { 
        return Unmanaged.passUnretained(self).toOpaque() 
    }

    var description: String { 
        return "component.\(address)" 
    }

    private (set) var _componentForTile: Weak1to1Relation<Component, Tile>! = nil

    var tile: Tile? {
        get { return _componentForTile.counterpart?.side }
        set { _componentForTile.relate(to: newValue?._tileForComponent) }
    }

    init() {
        _componentForTile = Weak1to1Relation(side: self, { [unowned self] in 
            self.tileDidChange()
        })
    }

    private func tileDidChange() {
        print("[\(self)].[tile] -> [\(tile?.description ?? "nil")]")
    }

}

Automatic update of relation sides

Objects of Component and Tile types can now relate to each other via the weak reference link that is synchnronously updated on both sides.

For example the following code does not fail the assertion:

let tile = Tile()
let component = Component()

tile.component = component

assert(component.tile === tile, 
    "The other side of the relation is automatically set")

Moreover, the following does not fail either:

let tile2 = Tile()

component.tile = tile2

assert(tile2.component === component, 
    "Yet again, the other side of relation is also set")
assert(tile.component == nil, 
    "And the object that was on the other side previously is updated")

The updates get also triggered when one of the relation sides is disposed of:

var component2: Component! = Component()

component2.tile = tile

assert(tile.component === component2, 
    "Both sides of the relation are set")

component2 = nil // Dispose component2

assert(tile.component == nil, 
    "Consequently, the tile's side of the relation is set to nil")

Notifications

It is possible to register the closures to get notified of the relation updates.

For example, note the difference between these two initializations from the example above:

_tileForComponent = Weak1to1Relation(side: self)

vs.

_componentForTile = Weak1to1Relation(side: self, { [unowned self] in
    self.tileDidChange()
})

The latter registers its own method as a notification handler to be triggered every time when the relation is changed. Such notification will be run after all the changes to relations are made, so you are never caught in the middle of the act.

Note: Make sure to use [unowned self] capture list to break otherwise inevitable strong reference cycle.

Weak, 1:N

The couple of generic classes Weak1toNRelation and WeakNto1Relation facilitate weak 1:N relationship management.

The use is quite similar to 1:1 case:

class File : CustomStringConvertible {

    let name: String
    var description: String { return "\"\(name)\"" }

    private (set) var _fileInFolder: WeakNto1Relation<File, Folder>! = nil

    var folder: Folder? {
        get { return _fileInFolder.counterpart?.side }
        set { _fileInFolder.relate(to: newValue?._folderForFiles) }
    }

    init(name: String) {
        self.name = name
        _fileInFolder = WeakNto1Relation(side: self)
    }

}
class Folder : CustomStringConvertible {

    let name: String
    var description: String { return "\"\(name)\"" }

    private (set) var _folderForFiles: Weak1toNRelation<Folder, File>! = nil

    var files: [File] {
        return _folderForFiles.counterparts
            .map({ $0.side })
            .sorted(by: { $0.name < $1.name })
    }

    private func filesDidChange() {
        print("Files in \(self)")
        files.forEach({
            print("- \($0)")
        })
    }

    init(name: String) {
        self.name = name
        _folderForFiles = Weak1toNRelation(side: self, { [unowned self] in
            self.filesDidChange()
        })
    }

    func insert(file: File) {
        _folderForFiles.relate(to: file._fileInFolder)
    }

    func insert(files: File...) {
        _folderForFiles.relate(to: files.map({ $0._fileInFolder }))
    }

    func remove(file: File) {
        _folderForFiles.unrelate(from: file._fileInFolder)
    }

    func remove(files: File...) {
        _folderForFiles.unrelate(from: files.map({ $0._fileInFolder }))
    }

}

… and then:

let etc = Folder(name: "etc")
let hosts = File(name: "hosts")
let services = File(name: "services")

etc.insert(files: hosts, services)

Indexed, Weak, 1:N

IndexedWeak1toNRelation and IndexedWeakNto1Relation pair of helper classes facilitate an indexed weak 1:N relation.

In a nutshell, it’s almost the same as 1:N relation desribed above. The difference is that the objects on N-side are registered with an index value (3-rd type in the generic list of parameter types). This value can be used on unary-side of the relation to look certain nary-object up. There is also a kick-out behaviour in case if a new nary-side object is inserted into relation that already has some object with the same index. In such case the previous object gets kicked out.

To illustrate quickly, the example with Folder/File can be re-written like this:

class File {

    let name: String

    private (set) var _fileInFolder: IndexedWeakNto1Relation<File, Folder, String>! = nil

    init(name: String) {
        self.name = name
        _fileInFolder = IndexedWeakNto1Relation(side: self, index: name)
    }

}

class Folder {

    private (set) var _folderForFiles: IndexedWeak1toNRelation<Folder, File, String>! = nil

    var fileNames: [String] {
        return _folderForFiles.indices.sorted()
    }

    init(name: String) {
        _folderForFiles = IndexedWeak1toNRelation(side: self)
    }

    func getFile(withName name: String) -> File? {
        return _folderForFiles.lookup(index: name)?.side
    }

}

Notifications

Important feature to notice about 1:N relations is that notifications, if registered, are always fired only once and only after all the changes are made. So if, for example, you tranfer bunch of N-side objects from one 1-side object to another, then each of those objects gets the notification fired only once.

Installation

Swift Package Manager

Add the following dependency to your Package.swift:

.Package(url: "https://github.com/courteouselk/Relations.git", majorVersion: 0)