TestsTested | ✓ |
LangLanguage | SwiftSwift |
License | MIT |
ReleasedLast Release | Apr 2017 |
SwiftSwift Version | 3.0 |
SPMSupports SPM | ✓ |
Maintained by Anton Bronnikov.
Swift helper-framework facilitating relations between objects.
The following kinds of the relations are supported so far:
Scroll down for installation guidelines.
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")]")
}
}
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")
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.
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)
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
}
}
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.
Add the following dependency to your Package.swift
:
.Package(url: "https://github.com/courteouselk/Relations.git", majorVersion: 0)