├── .codecov.yml ├── .gitignore ├── .gitmodules ├── .swift-version ├── .travis.yml ├── Cartfile ├── Cartfile.resolved ├── Demo.playground ├── Pages │ └── Untitled Page.xcplaygroundpage │ │ ├── Contents.swift │ │ └── timeline.xctimeline └── contents.xcplayground ├── Documentation ├── Language Enhancements.md └── Overview.md ├── GlueKit.podspec ├── GlueKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── GlueKit.xcscmblueprint └── xcshareddata │ ├── xcbaselines │ ├── BB351AFB1DB81E67005F083F.xcbaseline │ │ ├── EBF79DD4-EE66-4D33-B51C-EE37857B70A1.plist │ │ └── Info.plist │ └── BBB55BE11C8FD1C60050DDA9.xcbaseline │ │ ├── DD663408-0BA9-46F9-868F-F1570927CA52.plist │ │ └── Info.plist │ └── xcschemes │ ├── GlueKit-PerformanceTests.xcscheme │ ├── GlueKit-iOS.xcscheme │ ├── GlueKit-macOS.xcscheme │ ├── GlueKit-tvOS.xcscheme │ └── GlueKit-watchOS.xcscheme ├── GlueKit.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── GlueKit.xcscmblueprint ├── LICENSE.md ├── Package.swift ├── README.md ├── Sources ├── Abstract.swift ├── AccumulatedSource.swift ├── ArrayBasedTableViewDataSource.swift ├── ArrayChange.swift ├── ArrayChangeSeparation.swift ├── ArrayConcatenation.swift ├── ArrayFilteringIndexmap.swift ├── ArrayFilteringOnObservableBool.swift ├── ArrayFilteringOnPredicate.swift ├── ArrayFolding.swift ├── ArrayGatheringSource.swift ├── ArrayMappingForArrayField.swift ├── ArrayMappingForValue.swift ├── ArrayMappingForValueField.swift ├── ArrayReference.swift ├── ArrayVariable.swift ├── BracketingSource.swift ├── BufferedArray.swift ├── BufferedSet.swift ├── BufferedSource.swift ├── BufferedValue.swift ├── CADisplayLink Extensions.swift ├── Change.swift ├── ChangesSource.swift ├── CompositeObservable.swift ├── CompositeUpdatable.swift ├── ComputedUpdatable.swift ├── Connect.swift ├── Connector.swift ├── DependentValue.swift ├── DispatchSource.swift ├── DistinctUnion.swift ├── DistinctValue.swift ├── Info.plist ├── Locks.swift ├── MergedSource.swift ├── NSButton Glue.swift ├── NSControl Glue.swift ├── NSNotificationCenter Support.swift ├── NSObject Glue.swift ├── NSPopUpButton Glue.swift ├── NSTextField Glue.swift ├── ObservableArray.swift ├── ObservableContains.swift ├── ObservableSet.swift ├── ObservableType.swift ├── ObservableValue.swift ├── OwnedSink.swift ├── RefList.swift ├── Reference.swift ├── SetChange.swift ├── SetFilteringOnObservableBool.swift ├── SetFilteringOnPredicate.swift ├── SetFolding.swift ├── SetGatheringSource.swift ├── SetMappingBase.swift ├── SetMappingForArrayField.swift ├── SetMappingForSequence.swift ├── SetMappingForSetField.swift ├── SetMappingForValue.swift ├── SetMappingForValueField.swift ├── SetReference.swift ├── SetSortingByComparableField.swift ├── SetSortingByComparator.swift ├── SetSortingByMappingToComparable.swift ├── SetSortingByMappingToObservableComparable.swift ├── SetVariable.swift ├── Signal.swift ├── SimpleSources.swift ├── Sink.swift ├── Source.swift ├── TimerSource.swift ├── TransactionalThing.swift ├── TransformedSink.swift ├── TransformedSource.swift ├── TwoWayBinding.swift ├── Type Helpers.swift ├── UIBarButtonItem Extensions.swift ├── UIControl Glue.swift ├── UIDevice Glue.swift ├── UIGestureRecognizer Glue.swift ├── UILabel Glue.swift ├── UISearchBar Glue.swift ├── UISwitch Glue.swift ├── UpdatableArray.swift ├── UpdatableSet.swift ├── UpdatableValue.swift ├── Update.swift ├── ValueChange.swift ├── ValueMappingForArrayField.swift ├── ValueMappingForSetField.swift ├── ValueMappingForSourceField.swift ├── ValueMappingForValue.swift ├── ValueMappingForValueField.swift ├── ValueReference.swift └── Variable.swift ├── Tests ├── GlueKitTests │ ├── AnySinkTests.swift │ ├── AnySourceTests.swift │ ├── ArrayBufferingTests.swift │ ├── ArrayChangeSeparationTests.swift │ ├── ArrayChangeTests.swift │ ├── ArrayConcatenationTests.swift │ ├── ArrayFilteringTests.swift │ ├── ArrayFoldingTests.swift │ ├── ArrayMappingTests.swift │ ├── ArrayModificationTests.swift │ ├── ArrayReferenceTests.swift │ ├── ArrayVariableTests.swift │ ├── Bookshelf.swift │ ├── BracketingSourceTests.swift │ ├── BufferedSourceTests.swift │ ├── ChangeTests.swift │ ├── ChangesSourceTests.swift │ ├── CombinedObservableTests.swift │ ├── CombinedUpdatableTests.swift │ ├── ConnectorTests.swift │ ├── DispatchSourceTests.swift │ ├── DistinctTests.swift │ ├── DistinctUnionTests.swift │ ├── Info.plist │ ├── KVOSupportTests.swift │ ├── MergedSourceTests.swift │ ├── MockArrayObserver.swift │ ├── MockSetObserver.swift │ ├── MockSink.swift │ ├── MockUpdateSink.swift │ ├── NSUserDefaultsSupportTests.swift │ ├── NotificationCenterSupportTests.swift │ ├── ObservableArrayTests.swift │ ├── ObservableSetTests.swift │ ├── ObservableTypeTests.swift │ ├── ObservableValueTests.swift │ ├── RefListTests.swift │ ├── SetBufferingTests.swift │ ├── SetFilteringTests.swift │ ├── SetFoldingTests.swift │ ├── SetMappingTests.swift │ ├── SetReferenceTests.swift │ ├── SetSortingTests.swift │ ├── SetVariableTests.swift │ ├── SignalTests.swift │ ├── SimpleSourcesTests.swift │ ├── SourceOperatorTests.swift │ ├── TestChange.swift │ ├── TestObservable.swift │ ├── TestUpdatable.swift │ ├── TestUtilities.swift │ ├── TimerSourceTests.swift │ ├── TransactionStateTests.swift │ ├── TwoWayBindingTests.swift │ ├── TypeHelperTests.swift │ ├── UpdatableValueTests.swift │ ├── UpdateTests.swift │ ├── ValueBufferingTests.swift │ ├── ValueChangeTests.swift │ ├── ValueMappingTests.swift │ ├── ValueReferenceTests.swift │ └── VariableTests.swift └── PerformanceTests │ ├── GlueKitPerformanceTests.swift │ └── Info.plist ├── jazzy.sh └── version.xcconfig /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "/Tests/*" 3 | comment: 4 | layout: "header, diff" 5 | behavior: default 6 | require_changes: no 7 | coverage: 8 | status: 9 | project: 10 | default: 11 | target: auto 12 | threshold: null 13 | base: auto 14 | paths: "Sources/*" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | /Carthage/Build 3 | /Packages 4 | xcuserdata 5 | /Package.resolved 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Carthage/Checkouts/BTree"] 2 | path = Carthage/Checkouts/BTree 3 | url = https://github.com/attaswift/BTree.git 4 | [submodule "Carthage/Checkouts/SipHash"] 5 | path = Carthage/Checkouts/SipHash 6 | url = https://github.com/attaswift/SipHash.git 7 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode9 3 | script: 4 | - xcrun xcodebuild -workspace GlueKit.xcworkspace -scheme GlueKit-macOS test 5 | - xcrun xcodebuild -quiet -workspace GlueKit.xcworkspace -scheme GlueKit-iOS 6 | - xcrun xcodebuild -quiet -workspace GlueKit.xcworkspace -scheme GlueKit-watchOS 7 | - xcrun xcodebuild -quiet -workspace GlueKit.xcworkspace -scheme GlueKit-tvOS 8 | - swift test 9 | after_success: bash <(curl -s https://codecov.io/bash) 10 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "attaswift/BTree" ~> 4.1 2 | github "attaswift/SipHash" ~> 1.2 3 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "attaswift/BTree" "v4.1.0" 2 | github "attaswift/SipHash" "v1.2.0" 3 | -------------------------------------------------------------------------------- /Demo.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GlueKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'GlueKit' 3 | spec.version = '0.2.0' 4 | spec.ios.deployment_target = "9.3" 5 | spec.osx.deployment_target = "10.11" 6 | spec.tvos.deployment_target = "10.0" 7 | spec.watchos.deployment_target = "3.0" 8 | spec.summary = 'Type-safe observable values and collections in Swift' 9 | spec.author = 'Károly Lőrentey' 10 | spec.homepage = 'https://github.com/attaswift/GlueKit' 11 | spec.license = { :type => 'MIT', :file => 'LICENSE.md' } 12 | spec.source = { :git => 'https://github.com/attaswift/GlueKit.git', :tag => 'v' + String(spec.version) } 13 | spec.source_files = 'Sources/*.swift' 14 | spec.social_media_url = 'https://twitter.com/lorentey' 15 | #spec.documentation_url = 'http://lorentey.github.io/GlueKit/' 16 | spec.dependency 'BTree', '~> 4.1' 17 | spec.dependency 'SipHash', '~> 1.2' 18 | end 19 | -------------------------------------------------------------------------------- /GlueKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /GlueKit.xcodeproj/project.xcworkspace/xcshareddata/GlueKit.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "1C1227F33F1818028D5EE61FD15F57BEC36D44F4", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "05D6826B0162543BD57EFC4EE6354EB49FD59438" : 9223372036854775807, 8 | "1C1227F33F1818028D5EE61FD15F57BEC36D44F4" : 9223372036854775807 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "7F20ECAA-BD48-4077-9606-C54D512EB5F5", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "05D6826B0162543BD57EFC4EE6354EB49FD59438" : "BTree\/", 13 | "1C1227F33F1818028D5EE61FD15F57BEC36D44F4" : "GlueKit\/" 14 | }, 15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "GlueKit", 16 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "GlueKit.xcodeproj", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 19 | { 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/lorentey\/BTree.git", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "05D6826B0162543BD57EFC4EE6354EB49FD59438" 23 | }, 24 | { 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/lorentey\/GlueKit.git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "1C1227F33F1818028D5EE61FD15F57BEC36D44F4" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /GlueKit.xcodeproj/xcshareddata/xcbaselines/BB351AFB1DB81E67005F083F.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | EBF79DD4-EE66-4D33-B51C-EE37857B70A1 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i7 17 | cpuSpeedInMHz 18 | 2600 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro10,1 23 | physicalCPUCoresPerPackage 24 | 4 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /GlueKit.xcodeproj/xcshareddata/xcbaselines/BBB55BE11C8FD1C60050DDA9.xcbaseline/DD663408-0BA9-46F9-868F-F1570927CA52.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | SignalPerformanceTests 8 | 9 | testChainedSendPerformance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.53204 15 | baselineIntegrationDisplayName 16 | 2016-10-19 23:23:14 17 | 18 | 19 | testConcurrentSendPerformance() 20 | 21 | com.apple.XCTPerformanceMetric_WallClockTime 22 | 23 | baselineAverage 24 | 1.6244 25 | baselineIntegrationDisplayName 26 | 2016-10-19 23:23:14 27 | 28 | 29 | testSendPerformance() 30 | 31 | com.apple.XCTPerformanceMetric_WallClockTime 32 | 33 | baselineAverage 34 | 0.6672 35 | baselineIntegrationDisplayName 36 | 2016-10-19 23:23:14 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /GlueKit.xcodeproj/xcshareddata/xcbaselines/BBB55BE11C8FD1C60050DDA9.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | DD663408-0BA9-46F9-868F-F1570927CA52 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i7 17 | cpuSpeedInMHz 18 | 2600 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookPro10,1 23 | physicalCPUCoresPerPackage 24 | 4 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /GlueKit.xcodeproj/xcshareddata/xcschemes/GlueKit-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 72 | 73 | 74 | 75 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /GlueKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /GlueKit.xcworkspace/xcshareddata/GlueKit.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "1C1227F33F1818028D5EE61FD15F57BEC36D44F4", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "05D6826B0162543BD57EFC4EE6354EB49FD59438" : 9223372036854775807, 8 | "FB48966E443D25259D3CA0BE6E56D2D7343B7F97" : 9223372036854775807, 9 | "1C1227F33F1818028D5EE61FD15F57BEC36D44F4" : 9223372036854775807 10 | }, 11 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "46A01E16-332E-44AB-A9D5-63D415AD87CA", 12 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 13 | "05D6826B0162543BD57EFC4EE6354EB49FD59438" : "GlueKit\/Carthage\/Checkouts\/BTree\/", 14 | "FB48966E443D25259D3CA0BE6E56D2D7343B7F97" : "GlueKit\/Carthage\/Checkouts\/SipHash\/", 15 | "1C1227F33F1818028D5EE61FD15F57BEC36D44F4" : "GlueKit\/" 16 | }, 17 | "DVTSourceControlWorkspaceBlueprintNameKey" : "GlueKit", 18 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 19 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "GlueKit.xcworkspace", 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 21 | { 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/lorentey\/BTree.git", 23 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 24 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "05D6826B0162543BD57EFC4EE6354EB49FD59438" 25 | }, 26 | { 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/lorentey\/GlueKit.git", 28 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 29 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "1C1227F33F1818028D5EE61FD15F57BEC36D44F4" 30 | }, 31 | { 32 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/lorentey\/SipHash.git", 33 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 34 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "FB48966E443D25259D3CA0BE6E56D2D7343B7F97" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015–2017 Károly Lőrentey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "GlueKit", 6 | products: [ 7 | .library(name: "GlueKit", type: .dynamic, targets: ["GlueKit"]) 8 | ], 9 | dependencies: [ 10 | .package(url: "https://github.com/attaswift/SipHash", from: "1.2.0"), 11 | .package(url: "https://github.com/attaswift/BTree", from: "4.1.0") 12 | ], 13 | targets: [ 14 | .target(name: "GlueKit", dependencies: ["BTree", "SipHash"], path: "Sources"), 15 | .testTarget(name: "GlueKitTests", dependencies: ["GlueKit"], path: "Tests/GlueKitTests"), 16 | .testTarget(name: "PerformanceTests", dependencies: ["GlueKit"], path: "Tests/PerformanceTests") 17 | ], 18 | swiftLanguageVersions: [4] 19 | ) 20 | -------------------------------------------------------------------------------- /Sources/Abstract.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Abstract.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-08-22. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | internal func abstract() -> Never { 10 | fatalError("Unimplemented abstract method") 11 | } 12 | -------------------------------------------------------------------------------- /Sources/AccumulatedSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccumulatedSource.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2017-04-23. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension SourceType { 10 | public func accumulated(_ initial: R, _ next: @escaping (R, Value) -> R) -> AnyObservableValue { 11 | return AccumulatedSource(self, initial, next).anyObservableValue 12 | } 13 | 14 | public func counted() -> AnyObservableValue { 15 | return accumulated(0) { value, _ in value + 1 } 16 | } 17 | } 18 | 19 | private class AccumulatedSource: _BaseObservableValue where S: SourceType { 20 | let source: S 21 | let next: (Value, S.Value) -> Value 22 | var _value: Value 23 | 24 | struct Sink: UniqueOwnedSink { 25 | typealias Owner = AccumulatedSource 26 | unowned(unsafe) let owner: Owner 27 | func receive(_ value: S.Value) { 28 | owner.beginTransaction() 29 | let old = owner._value 30 | let new = owner.next(owner._value, value) 31 | owner._value = new 32 | owner.sendChange(ValueChange(from: old, to: new)) 33 | owner.endTransaction() 34 | } 35 | } 36 | 37 | init(_ source: S, _ initial: Value, _ next: @escaping (Value, S.Value) -> Value) { 38 | self.source = source 39 | self.next = next 40 | self._value = initial 41 | super.init() 42 | source.add(Sink(owner: self)) 43 | } 44 | 45 | deinit { 46 | source.remove(Sink(owner: self)) 47 | } 48 | 49 | override var value: Value { return _value } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ArrayFilteringIndexmap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayFilteringIndexmap.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-07. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import BTree 10 | 11 | internal struct ArrayFilteringIndexmap { 12 | let isIncluded: (Element) -> Bool 13 | var matchingIndices = SortedSet() 14 | 15 | init(initialValues values: [Element], isIncluded: @escaping (Element) -> Bool) { 16 | self.isIncluded = isIncluded 17 | for index in values.indices { 18 | if isIncluded(values[index]) { 19 | matchingIndices.insert(index) 20 | } 21 | } 22 | } 23 | 24 | mutating func apply(_ change: ArrayChange) -> ArrayChange { 25 | var filteredChange = ArrayChange(initialCount: matchingIndices.count) 26 | for mod in change.modifications { 27 | switch mod { 28 | case .insert(let element, at: let index): 29 | matchingIndices.shift(startingAt: index, by: 1) 30 | if isIncluded(element) { 31 | matchingIndices.insert(index) 32 | filteredChange.add(.insert(element, at: matchingIndices.offset(of: index)!)) 33 | } 34 | case .remove(let element, at: let index): 35 | if let filteredIndex = matchingIndices.offset(of: index) { 36 | filteredChange.add(.remove(element, at: filteredIndex)) 37 | } 38 | matchingIndices.shift(startingAt: index + 1, by: -1) 39 | case .replace(let old, at: let index, with: let new): 40 | switch (matchingIndices.offset(of: index), isIncluded(new)) { 41 | case (.some(let offset), true): 42 | filteredChange.add(.replace(old, at: offset, with: new)) 43 | case (.none, true): 44 | matchingIndices.insert(index) 45 | filteredChange.add(.insert(new, at: matchingIndices.offset(of: index)!)) 46 | case (.some(let offset), false): 47 | matchingIndices.remove(index) 48 | filteredChange.add(.remove(old, at: offset)) 49 | case (.none, false): 50 | // Do nothing 51 | break 52 | } 53 | case .replaceSlice(let old, at: let index, with: let new): 54 | let filteredIndex = matchingIndices.prefix(upTo: index).count 55 | let filteredOld = matchingIndices.intersection(elementsIn: index ..< index + old.count).map { old[$0 - index] } 56 | var filteredNew: [Element] = [] 57 | 58 | matchingIndices.subtract(elementsIn: index ..< index + old.count) 59 | matchingIndices.shift(startingAt: index + old.count, by: new.count - old.count) 60 | for i in 0 ..< new.count { 61 | if isIncluded(new[i]) { 62 | matchingIndices.insert(index + i) 63 | filteredNew.append(new[i]) 64 | } 65 | } 66 | if let mod = ArrayModification(replacing: filteredOld, at: filteredIndex, with: filteredNew) { 67 | filteredChange.add(mod) 68 | } 69 | } 70 | } 71 | return filteredChange 72 | } 73 | 74 | mutating func insert(_ index: Int) -> Int? { 75 | guard !matchingIndices.contains(index) else { return nil } 76 | matchingIndices.insert(index) 77 | return matchingIndices.offset(of: index)! 78 | } 79 | 80 | mutating func remove(_ index: Int) -> Int? { 81 | guard let filteredIndex = matchingIndices.offset(of: index) else { return nil } 82 | matchingIndices.remove(index) 83 | return filteredIndex 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/ArrayFilteringOnPredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayFilteringOnPredicate.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-09-26. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableArrayType { 10 | public func filter(_ isIncluded: @escaping (Element) -> Bool) -> AnyObservableArray { 11 | return ArrayFilteringOnPredicate(parent: self, isIncluded: isIncluded).anyObservableArray 12 | } 13 | } 14 | 15 | private final class ArrayFilteringOnPredicate: _BaseObservableArray { 16 | public typealias Element = Parent.Element 17 | public typealias Change = ArrayChange 18 | 19 | private struct ParentSink: UniqueOwnedSink { 20 | typealias Owner = ArrayFilteringOnPredicate 21 | 22 | unowned(unsafe) let owner: Owner 23 | 24 | func receive(_ update: ArrayUpdate) { 25 | owner.applyParentUpdate(update) 26 | } 27 | } 28 | 29 | private let parent: Parent 30 | private let isIncluded: (Element) -> Bool 31 | 32 | private var indexMapping: ArrayFilteringIndexmap 33 | 34 | init(parent: Parent, isIncluded: @escaping (Element) -> Bool) { 35 | self.parent = parent 36 | self.isIncluded = isIncluded 37 | self.indexMapping = ArrayFilteringIndexmap(initialValues: parent.value, isIncluded: isIncluded) 38 | super.init() 39 | parent.add(ParentSink(owner: self)) 40 | } 41 | 42 | deinit { 43 | parent.remove(ParentSink(owner: self)) 44 | } 45 | 46 | func applyParentUpdate(_ update: ArrayUpdate) { 47 | switch update { 48 | case .beginTransaction: 49 | beginTransaction() 50 | case .change(let change): 51 | let filteredChange = self.indexMapping.apply(change) 52 | if !filteredChange.isEmpty { 53 | sendChange(filteredChange) 54 | } 55 | case .endTransaction: 56 | endTransaction() 57 | } 58 | } 59 | 60 | override var isBuffered: Bool { 61 | return false 62 | } 63 | 64 | override subscript(index: Int) -> Element { 65 | return parent[indexMapping.matchingIndices[index]] 66 | } 67 | 68 | override subscript(bounds: Range) -> ArraySlice { 69 | precondition(0 <= bounds.lowerBound && bounds.lowerBound <= bounds.upperBound && bounds.upperBound <= count) 70 | var result: [Element] = [] 71 | result.reserveCapacity(bounds.count) 72 | for index in indexMapping.matchingIndices[bounds] { 73 | result.append(parent[index]) 74 | } 75 | return ArraySlice(result) 76 | } 77 | 78 | override var value: Array { 79 | return indexMapping.matchingIndices.map { parent[$0] } 80 | } 81 | 82 | override var count: Int { 83 | return indexMapping.matchingIndices.count 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/ArrayFolding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayFolding.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-09. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableArrayType { 10 | /// Returns an observable whose value is always equal to `self.value.reduce(initial, add)`. 11 | /// 12 | /// - Parameter initial: The accumulation starts with this initial value. 13 | /// - Parameter add: A closure that adds an element of the array into an accumulated value. 14 | /// - Parameter remove: A closure that cancels the effect of an earlier `add`. 15 | /// - Returns: An observable value for the reduction of this array. 16 | /// 17 | /// - Note: Elements are added and removed in no particular order. 18 | /// (I.e., the underlying binary operation over `Result` must form an abelian group.) 19 | /// 20 | /// - SeeAlso: `sum()` which returns a reduction using addition. 21 | public func reduce(_ initial: Result, add: @escaping (Result, Element) -> Result, remove: @escaping (Result, Element) -> Result) -> AnyObservableValue { 22 | return ArrayFoldingByTwoWayFunction(base: self, initial: initial, add: add, remove: remove).anyObservableValue 23 | } 24 | } 25 | 26 | extension ObservableArrayType where Element: BinaryInteger { 27 | /// Return the (observable) sum of the elements contained in this array. 28 | public func sum() -> AnyObservableValue { 29 | return reduce(0, add: +, remove: -) 30 | } 31 | } 32 | 33 | private class ArrayFoldingByTwoWayFunction: _BaseObservableValue { 34 | private var _value: Value 35 | 36 | let add: (Value, Base.Element) -> Value 37 | let remove: (Value, Base.Element) -> Value 38 | var connection: Connection? = nil 39 | 40 | init(base: Base, initial: Value, add: @escaping (Value, Base.Element) -> Value, remove: @escaping (Value, Base.Element) -> Value) { 41 | self._value = base.value.reduce(initial, add) 42 | self.add = add 43 | self.remove = remove 44 | super.init() 45 | 46 | connection = base.updates.subscribe { [unowned self] in self.apply($0) } 47 | } 48 | 49 | deinit { 50 | connection!.disconnect() 51 | } 52 | 53 | private func apply(_ update: ArrayUpdate) { 54 | switch update { 55 | case .beginTransaction: 56 | beginTransaction() 57 | case .change(let change): 58 | let old = _value 59 | change.forEachOld { _value = remove(_value, $0) } 60 | change.forEachNew { _value = add(_value, $0) } 61 | sendChange(ValueChange(from: old, to: _value)) 62 | case .endTransaction: 63 | endTransaction() 64 | } 65 | } 66 | 67 | override var value: Value { return _value } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/ArrayGatheringSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayGatheringSource.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2017-09-05. 6 | // Copyright © 2017 Károly Lőrentey. All rights reserved. 7 | // 8 | 9 | extension ObservableArrayType where Element: SourceType { 10 | public func gather() -> AnySource { 11 | return ArrayGatheringSource(self).anySource 12 | } 13 | } 14 | 15 | private class ArrayGatheringSource: _AbstractSource 16 | where Origin.Element: SourceType, Origin.Element.Value == Value { 17 | let origin: Origin 18 | var sinks: Set> = [] 19 | 20 | private struct GatherSink: UniqueOwnedSink { 21 | typealias Owner = ArrayGatheringSource 22 | unowned let owner: Owner 23 | 24 | func receive(_ value: ArrayUpdate) { 25 | guard case let .change(change) = value else { return } 26 | change.forEachOld { source in 27 | for sink in owner.sinks { 28 | source.remove(sink) 29 | } 30 | } 31 | change.forEachNew { source in 32 | for sink in owner.sinks { 33 | source.add(sink) 34 | } 35 | } 36 | } 37 | } 38 | 39 | init(_ origin: Origin) { 40 | self.origin = origin 41 | } 42 | 43 | override func add(_ sink: Sink) where Sink.Value == Value { 44 | if sinks.isEmpty { 45 | origin.add(GatherSink(owner: self)) 46 | } 47 | let new = sinks.insert(sink.anySink).inserted 48 | precondition(new) 49 | for source in origin.value { 50 | source.add(sink) 51 | } 52 | } 53 | 54 | @discardableResult 55 | override func remove(_ sink: Sink) -> Sink where Sink.Value == Value { 56 | let result = sinks.remove(sink.anySink)! 57 | for source in origin.value { 58 | source.remove(result) 59 | } 60 | if sinks.isEmpty { 61 | origin.remove(GatherSink(owner: self)) 62 | } 63 | return result.opened()! 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /Sources/ArrayReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayReference.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-08-17. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableValueType where Value: ObservableArrayType{ 10 | public func unpacked() -> AnyObservableArray { 11 | return UnpackedObservableArrayReference(self).anyObservableArray 12 | } 13 | } 14 | 15 | /// A mutable reference to an `AnyObservableArray` that's also an observable array. 16 | /// You can switch to another target array without having to re-register subscribers. 17 | private final class UnpackedObservableArrayReference: _BaseObservableArray 18 | where Reference.Value: ObservableArrayType { 19 | typealias Target = Reference.Value 20 | typealias Element = Target.Element 21 | typealias Change = ArrayChange 22 | 23 | private struct ReferenceSink: UniqueOwnedSink { 24 | typealias Owner = UnpackedObservableArrayReference 25 | 26 | unowned(unsafe) let owner: Owner 27 | 28 | func receive(_ update: ValueUpdate) { 29 | owner.applyReferenceUpdate(update) 30 | } 31 | } 32 | 33 | private struct TargetSink: UniqueOwnedSink { 34 | typealias Owner = UnpackedObservableArrayReference 35 | 36 | unowned(unsafe) let owner: Owner 37 | 38 | func receive(_ update: ArrayUpdate) { 39 | owner.applyTargetUpdate(update) 40 | } 41 | } 42 | 43 | private var _reference: Reference 44 | private var _target: Reference.Value? = nil // Retained to make sure we keep it alive 45 | 46 | init(_ reference: Reference) { 47 | _reference = reference 48 | super.init() 49 | } 50 | 51 | override func activate() { 52 | _reference.updates.add(ReferenceSink(owner: self)) 53 | let target = _reference.value 54 | _target = target 55 | target.updates.add(TargetSink(owner: self)) 56 | } 57 | 58 | override func deactivate() { 59 | _target!.updates.remove(TargetSink(owner: self)) 60 | _reference.updates.remove(ReferenceSink(owner: self)) 61 | } 62 | 63 | func applyReferenceUpdate(_ update: ValueUpdate) { 64 | switch update { 65 | case .beginTransaction: 66 | beginTransaction() 67 | case .change(let change): 68 | if isConnected { 69 | _target!.remove(TargetSink(owner: self)) 70 | _target = change.new 71 | _target!.add(TargetSink(owner: self)) 72 | sendChange(ArrayChange(from: change.old.value, to: change.new.value)) 73 | } 74 | case .endTransaction: 75 | endTransaction() 76 | } 77 | } 78 | 79 | func applyTargetUpdate(_ update: ArrayUpdate) { 80 | switch update { 81 | case .beginTransaction: 82 | beginTransaction() 83 | case .change(let change): 84 | sendChange(change) 85 | case .endTransaction: 86 | endTransaction() 87 | } 88 | } 89 | 90 | override var isBuffered: Bool { return false } 91 | override subscript(_ index: Int) -> Element { return _reference.value[index] } 92 | override subscript(_ range: Range) -> ArraySlice { return _reference.value[range] } 93 | override var value: [Element] { return _reference.value.value } 94 | override var count: Int { return _reference.value.count } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/ArrayVariable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayVariable.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-12-08. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | open class ArrayVariable: _BaseUpdatableArray, ExpressibleByArrayLiteral { 10 | public typealias Value = Array 11 | public typealias Change = ArrayChange 12 | 13 | fileprivate var _value: [Element] 14 | 15 | public override init() { 16 | _value = [] 17 | } 18 | public init(_ elements: [Element]) { 19 | _value = elements 20 | } 21 | public init(_ elements: S) where S.Iterator.Element == Element { 22 | _value = Array(elements) 23 | } 24 | public init(elements: Element...) { 25 | _value = elements 26 | } 27 | public required convenience init(arrayLiteral elements: Element...) { 28 | self.init(elements) 29 | } 30 | 31 | override func rawApply(_ change: ArrayChange) { 32 | _value.apply(change) 33 | } 34 | 35 | final public override var value: [Element] { 36 | get { 37 | return _value 38 | } 39 | set { 40 | if isConnected { 41 | let old = _value 42 | beginTransaction() 43 | _value = newValue 44 | sendChange(ArrayChange(from: old, to: newValue)) 45 | endTransaction() 46 | } 47 | else { 48 | _value = newValue 49 | } 50 | } 51 | } 52 | 53 | final public override var count: Int { 54 | return _value.count 55 | } 56 | 57 | final public override var isBuffered: Bool { 58 | return true 59 | } 60 | 61 | final public override subscript(index: Int) -> Element { 62 | get { 63 | return _value[index] 64 | } 65 | set { 66 | if isConnected { 67 | let old = _value[index] 68 | beginTransaction() 69 | _value[index] = newValue 70 | sendChange(ArrayChange(initialCount: _value.count, modification: .replace(old, at: index, with: newValue))) 71 | endTransaction() 72 | } 73 | else { 74 | _value[index] = newValue 75 | } 76 | } 77 | } 78 | 79 | final public override subscript(bounds: Range) -> ArraySlice { 80 | get { 81 | return value[bounds] 82 | } 83 | set { 84 | if isConnected { 85 | let oldCount = _value.count 86 | let old = Array(_value[bounds]) 87 | beginTransaction() 88 | _value[bounds] = newValue 89 | sendChange(ArrayChange(initialCount: oldCount, 90 | modification: .replaceSlice(old, at: bounds.lowerBound, with: Array(newValue)))) 91 | endTransaction() 92 | } 93 | else { 94 | _value[bounds] = newValue 95 | } 96 | } 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /Sources/BracketingSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BracketingSource.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-25. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension SourceType { 10 | /// Returns a version of this source that optionally prefixes or suffixes all observers' received values 11 | /// with computed start/end values. 12 | /// 13 | /// For each new subscriber, the returned source evaluates `hello`; if it returns a non-nil value, 14 | /// the value is sent to the sink; then the sink is added to `self`. 15 | /// 16 | /// For each subscriber that is to be removed, the returned source first removes it from `self`, then 17 | /// evaluates `goodbye`; if it returns a non-nil value, the bracketing source sends it to the sink. 18 | public func bracketed(hello: @escaping () -> Value?, goodbye: @escaping () -> Value?) -> AnySource { 19 | return BracketingSource(self, hello: hello, goodbye: goodbye).anySource 20 | } 21 | } 22 | 23 | private class BracketingSink: SinkType { 24 | typealias Value = Sink.Value 25 | 26 | let sink: Sink 27 | var pendingValues: [Value]? 28 | var removed = false 29 | 30 | init(_ sink: Sink) { 31 | self.sink = sink 32 | self.pendingValues = nil 33 | } 34 | 35 | init(_ sink: Sink, _ initial: Value) { 36 | self.sink = sink 37 | self.pendingValues = [initial] 38 | } 39 | 40 | func receive(_ value: Value) { 41 | if pendingValues == nil { 42 | sink.receive(value) 43 | } 44 | else { 45 | pendingValues!.append(value) 46 | } 47 | } 48 | 49 | var hashValue: Int { return sink.hashValue } 50 | static func ==(left: BracketingSink, right: BracketingSink) -> Bool { 51 | return left.sink == right.sink 52 | } 53 | } 54 | 55 | private final class BracketingSource: _AbstractSource { 56 | typealias Value = Input.Value 57 | let input: Input 58 | let hello: () -> Value? 59 | let goodbye: () -> Value? 60 | 61 | init(_ input: Input, hello: @escaping () -> Value?, goodbye: @escaping () -> Value?) { 62 | self.input = input 63 | self.hello = hello 64 | self.goodbye = goodbye 65 | } 66 | 67 | final override func add(_ sink: Sink) where Sink.Value == Value { 68 | if let greeting = hello() { 69 | let bracketing = BracketingSink(sink, greeting) 70 | input.add(bracketing) 71 | while !bracketing.pendingValues!.isEmpty && !bracketing.removed { 72 | let value = bracketing.pendingValues!.removeFirst() 73 | bracketing.sink.receive(value) 74 | } 75 | bracketing.pendingValues = nil 76 | } 77 | else { 78 | input.add(BracketingSink(sink)) 79 | } 80 | } 81 | 82 | @discardableResult 83 | final override func remove(_ sink: Sink) -> Sink where Sink.Value == Value { 84 | let old = input.remove(BracketingSink(sink)) 85 | old.removed = true 86 | if let farewell = goodbye() { 87 | old.sink.receive(farewell) 88 | } 89 | return old.sink 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/BufferedArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BufferedArray.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-08-22. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableArrayType { 10 | public func buffered() -> AnyObservableArray { 11 | if isBuffered { 12 | return anyObservableArray 13 | } 14 | return BufferedObservableArray(self).anyObservableArray 15 | } 16 | } 17 | 18 | internal class BufferedObservableArray: _BaseObservableArray { 19 | typealias Element = Content.Element 20 | typealias Change = ArrayChange 21 | 22 | private struct BufferedSink: UniqueOwnedSink { 23 | typealias Owner = BufferedObservableArray 24 | 25 | unowned(unsafe) let owner: Owner 26 | 27 | func receive(_ update: ArrayUpdate) { 28 | owner.applyUpdate(update) 29 | } 30 | } 31 | 32 | private let _content: Content 33 | private var _value: [Element] 34 | private var _pendingChange: Change? = nil 35 | 36 | init(_ content: Content) { 37 | _content = content 38 | _value = content.value 39 | super.init() 40 | _content.add(BufferedSink(owner: self)) 41 | } 42 | 43 | deinit { 44 | _content.remove(BufferedSink(owner: self)) 45 | } 46 | 47 | func applyUpdate(_ update: ArrayUpdate) { 48 | switch update { 49 | case .beginTransaction: 50 | beginTransaction() 51 | case .change(let change): 52 | if _pendingChange != nil { 53 | _pendingChange!.merge(with: change) 54 | } 55 | else { 56 | _pendingChange = change 57 | } 58 | case .endTransaction: 59 | if let change = _pendingChange { 60 | _value.apply(change) 61 | _pendingChange = nil 62 | sendChange(change) 63 | } 64 | endTransaction() 65 | } 66 | } 67 | 68 | override var isBuffered: Bool { 69 | return true 70 | } 71 | 72 | override subscript(_ index: Int) -> Content.Element { 73 | return _value[index] 74 | } 75 | 76 | override subscript(_ range: Range) -> ArraySlice { 77 | return _value[range] 78 | } 79 | 80 | override var value: [Element] { 81 | return _value 82 | } 83 | 84 | override var count: Int { 85 | return _value.count 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/BufferedSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BufferedSet.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-11-02. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableSetType { 10 | public func buffered() -> AnyObservableSet { 11 | if isBuffered { 12 | return anyObservableSet 13 | } 14 | return BufferedObservableSet(self).anyObservableSet 15 | } 16 | } 17 | 18 | internal class BufferedObservableSet: _BaseObservableSet { 19 | typealias Element = Content.Element 20 | typealias Change = SetChange 21 | 22 | private struct BufferedSink: UniqueOwnedSink { 23 | typealias Owner = BufferedObservableSet 24 | 25 | unowned(unsafe) let owner: Owner 26 | 27 | func receive(_ update: SetUpdate) { 28 | owner.applyUpdate(update) 29 | } 30 | } 31 | 32 | private let _content: Content 33 | private var _value: Set 34 | private var _pendingChange: Change? = nil 35 | 36 | init(_ content: Content) { 37 | _content = content 38 | _value = content.value 39 | super.init() 40 | _content.add(BufferedSink(owner: self)) 41 | } 42 | 43 | deinit { 44 | _content.remove(BufferedSink(owner: self)) 45 | } 46 | 47 | func applyUpdate(_ update: SetUpdate) { 48 | switch update { 49 | case .beginTransaction: 50 | beginTransaction() 51 | case .change(let change): 52 | if _pendingChange != nil { 53 | _pendingChange!.merge(with: change) 54 | } 55 | else { 56 | _pendingChange = change 57 | } 58 | case .endTransaction: 59 | if let change = _pendingChange { 60 | _value.apply(change) 61 | _pendingChange = nil 62 | sendChange(change) 63 | } 64 | endTransaction() 65 | } 66 | } 67 | 68 | override var isBuffered: Bool { 69 | return true 70 | } 71 | 72 | override var count: Int { 73 | return _value.count 74 | } 75 | 76 | override var value: Set { 77 | return _value 78 | } 79 | 80 | override func contains(_ member: Element) -> Bool { 81 | return _value.contains(member) 82 | } 83 | 84 | override func isSubset(of other: Set) -> Bool { 85 | return _value.isSubset(of: other) 86 | } 87 | 88 | override func isSuperset(of other: Set) -> Bool { 89 | return _value.isSuperset(of: other) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/BufferedSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BufferedSource.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-25. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension SourceType { 10 | public func buffered() -> AnySource { 11 | return BufferedSource(self).anySource 12 | } 13 | } 14 | 15 | private final class BufferedSource: SignalerSource, SinkType { 16 | typealias Value = Input.Value 17 | 18 | private let source: Input 19 | 20 | init(_ source: Input) { 21 | self.source = source 22 | super.init() 23 | } 24 | 25 | override func activate() { 26 | source.add(self.unowned()) 27 | } 28 | 29 | override func deactivate() { 30 | source.remove(self.unowned()) 31 | } 32 | 33 | func receive(_ value: Input.Value) { 34 | signal.send(value) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/BufferedValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Observable Transformations.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-12-07. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableValueType { 10 | public func buffered() -> AnyObservableValue { 11 | return BufferedObservableValue(self).anyObservableValue 12 | } 13 | } 14 | 15 | private struct BufferedObservableSink: UniqueOwnedSink { 16 | typealias Owner = BufferedObservableValue 17 | 18 | unowned(unsafe) let owner: Owner 19 | 20 | func receive(_ update: ValueUpdate) { 21 | owner.apply(update) 22 | } 23 | } 24 | 25 | private class BufferedObservableValue: _BaseObservableValue { 26 | typealias Value = Base.Value 27 | 28 | private var _base: Base 29 | 30 | private var _value: Value 31 | private var _pending: Value? = nil 32 | 33 | init(_ base: Base) { 34 | self._base = base 35 | self._value = base.value 36 | super.init() 37 | 38 | _base.updates.add(BufferedObservableSink(owner: self)) 39 | } 40 | 41 | deinit { 42 | _base.updates.remove(BufferedObservableSink(owner: self)) 43 | } 44 | 45 | func apply(_ update: ValueUpdate) { 46 | switch update { 47 | case .beginTransaction: 48 | beginTransaction() 49 | case .change(let change): 50 | _pending = change.new 51 | case .endTransaction: 52 | if let pending = _pending { 53 | let old = _value 54 | _value = pending 55 | _pending = nil 56 | sendChange(.init(from: old, to: _value)) 57 | } 58 | endTransaction() 59 | } 60 | } 61 | 62 | override var value: Value { return _value } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/CADisplayLink Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CADisplayLink Extensions.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-03-17. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | #if os(iOS) 10 | import QuartzCore 11 | 12 | private var associatedTargetKey: UInt8 = 0 13 | 14 | public class CADisplayLinkSource: SignalerSource { 15 | public typealias Value = CADisplayLink 16 | 17 | private var runLoop: RunLoop? = nil 18 | public var displayLink: CADisplayLink? = nil 19 | 20 | public override init() { 21 | super.init() 22 | } 23 | 24 | override func activate() { 25 | displayLink = CADisplayLink(target: self, selector: #selector(CADisplayLinkSource.tick(_:))) 26 | precondition(self.runLoop == nil) 27 | let runLoop = RunLoop.current 28 | self.runLoop = runLoop 29 | displayLink!.add(to: runLoop, forMode: RunLoopMode.commonModes) 30 | } 31 | 32 | override func deactivate() { 33 | precondition(runLoop != nil) 34 | displayLink!.remove(from: runLoop!, forMode: RunLoopMode.commonModes) 35 | displayLink = nil 36 | runLoop = nil 37 | } 38 | 39 | @objc private func tick(_ displayLink: CADisplayLink) { 40 | signal.send(displayLink) 41 | } 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /Sources/Change.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Change.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-08-12. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | /// Describes a change to an observable value. 10 | /// An instance of a type implementing this protocol contains just enough information to describe the difference 11 | /// between the old value and the new value of the observable. 12 | public protocol ChangeType { 13 | associatedtype Value 14 | 15 | /// Creates a new change description for a change that goes from `oldValue` to `newValue`. 16 | init(from oldValue: Value, to newValue: Value) 17 | 18 | /// Returns true if this change did not actually change the value of the observable. 19 | /// Noop changes aren't usually sent by observables, but it is possible to get them by merging a sequence of 20 | /// changes to a collection. 21 | var isEmpty: Bool { get } 22 | 23 | /// Applies this change on `value` in place. 24 | /// Note that not all changes may be applicable on all values. 25 | func apply(on value: inout Value) 26 | 27 | /// Applies this change on `value` and returns the result. 28 | /// Note that not all changes may be applicable on all values. 29 | func applied(on value: Value) -> Value 30 | 31 | /// Merge this change with the `next` change. The result is a single change description that describes the 32 | /// change of performing `self` followed by `next`. 33 | /// 34 | /// The resulting instance may take a shortcut when producing the result value if some information in `self` 35 | /// is overwritten by `next`. 36 | func merged(with next: Self) -> Self 37 | 38 | mutating func merge(with next: Self) 39 | 40 | /// Reverse the direction of this change, i.e., return a change that undoes the effect of this change. 41 | func reversed() -> Self 42 | } 43 | 44 | 45 | extension ChangeType { 46 | /// Applies this change on `value` and returns the result. 47 | /// Note that not all changes may be applicable on all values. 48 | public func applied(on value: Value) -> Value { 49 | var result = value 50 | self.apply(on: &result) 51 | return result 52 | } 53 | 54 | public func merged(with next: Self) -> Self { 55 | var temp = self 56 | temp.merge(with: next) 57 | return temp 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /Sources/ChangesSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChangesSource.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-22. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableType { 10 | /// A source that reports changes to the value of this observable. 11 | /// Changes reported correspond to complete transactions in `self.updates`. 12 | public var changes: AnySource { 13 | return ChangesSource(self).anySource 14 | } 15 | } 16 | 17 | private class ChangesSinkState { 18 | typealias Value = Update 19 | 20 | var pending: Change? = nil 21 | 22 | func apply(_ update: Update) -> Change? { 23 | switch update { 24 | case .beginTransaction: 25 | precondition(pending == nil) 26 | case .change(let change): 27 | if pending == nil { 28 | pending = change 29 | } 30 | else { 31 | pending!.merge(with: change) 32 | } 33 | case .endTransaction: 34 | if let change = pending { 35 | pending = nil 36 | if !change.isEmpty { 37 | return change 38 | } 39 | } 40 | } 41 | return nil 42 | } 43 | } 44 | 45 | private struct ChangesSink: SinkType where Wrapped.Value: ChangeType { 46 | typealias Change = Wrapped.Value 47 | typealias Value = Update 48 | 49 | let wrapped: Wrapped 50 | let state: ChangesSinkState? 51 | 52 | init(_ wrapped: Wrapped, withState needState: Bool) { 53 | self.wrapped = wrapped 54 | self.state = needState ? ChangesSinkState() : nil 55 | } 56 | 57 | func receive(_ update: Update) { 58 | if let change = state?.apply(update) { 59 | wrapped.receive(change) 60 | } 61 | } 62 | 63 | var hashValue: Int { 64 | return wrapped.hashValue 65 | } 66 | 67 | static func ==(left: ChangesSink, right: ChangesSink) -> Bool { 68 | return left.wrapped == right.wrapped 69 | } 70 | } 71 | 72 | internal class ChangesSource: _AbstractSource { 73 | typealias Change = Observable.Change 74 | 75 | let observable: Observable 76 | 77 | init(_ observable: Observable) { 78 | self.observable = observable 79 | } 80 | 81 | override func add(_ sink: Sink) where Sink.Value == Change { 82 | observable.add(ChangesSink(sink, withState: true)) 83 | } 84 | 85 | @discardableResult 86 | override func remove(_ sink: Sink) -> Sink where Sink.Value == Change { 87 | let old = observable.remove(ChangesSink(sink, withState: false)) 88 | return old.wrapped 89 | } 90 | } 91 | 92 | extension Connector { 93 | @discardableResult 94 | public func subscribe(_ observable: Observable, to sink: @escaping (Observable.Change) -> Void) -> Connection { 95 | return observable.changes.subscribe(sink).putInto(self) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/ComputedUpdatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComputedUpdatable.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-11-11. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | public final class ComputedUpdatable: _BaseUpdatableValue { 10 | public let getter: () -> Value 11 | public let setter: (Value) -> () 12 | public let refreshSource: AnySource? 13 | 14 | private var _value: Value 15 | 16 | private struct Sink: UniqueOwnedSink { 17 | typealias Owner = ComputedUpdatable 18 | unowned(unsafe) let owner: Owner 19 | func receive(_ value: Void) { 20 | owner.refresh() 21 | } 22 | } 23 | 24 | public init(getter: @escaping () -> Value, 25 | setter: @escaping (Value) -> (), 26 | refreshSource: AnySource? = nil) { 27 | self.getter = getter 28 | self.setter = setter 29 | self.refreshSource = refreshSource 30 | self._value = getter() 31 | super.init() 32 | refreshSource?.add(Sink(owner: self)) 33 | } 34 | 35 | deinit { 36 | refreshSource?.remove(Sink(owner: self)) 37 | } 38 | 39 | override func rawGetValue() -> Value { 40 | return _value 41 | } 42 | 43 | override func rawSetValue(_ value: Value) { 44 | setter(value) 45 | _value = getter() 46 | } 47 | 48 | public func refresh() { 49 | self.value = getter() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Connector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Connector.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-11-30. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension Connection { 10 | /// Put this connection into `connector`. The connector will disconnect the connection when it is deallocated. 11 | @discardableResult 12 | public func putInto(_ connector: Connector) -> Connection { 13 | connector.add(self) 14 | return self 15 | } 16 | } 17 | 18 | /// A class for controlling the lifecycle of connections. 19 | /// The connector owns a set of connections and forces them to disconnect when it is deallocated. 20 | public class Connector { 21 | private var connections: [Connection] = [] 22 | 23 | public init() {} 24 | 25 | deinit { 26 | disconnect() 27 | } 28 | 29 | fileprivate func add(_ connection: Connection) { 30 | connections.append(connection) 31 | } 32 | 33 | public func disconnect() { 34 | let cs = connections 35 | connections.removeAll() 36 | for c in cs { 37 | c.disconnect() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/DependentValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DependentValue.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2017-04-23. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | infix operator <-- 10 | 11 | public func <-- (target: DependentValue, source: Source) where Source.Value == Value { 12 | target.origin = source.anyObservableValue 13 | } 14 | 15 | public class DependentValue { 16 | private let setter: (Value) -> () 17 | private var transactions: Int = 0 18 | private var pending: Value? 19 | 20 | internal var origin: AnyObservableValue? { 21 | didSet { 22 | receive(.beginTransaction) 23 | oldValue?.remove(Sink(owner: self)) 24 | if let origin = origin { 25 | pending = origin.value 26 | origin.add(Sink(owner: self)) 27 | } 28 | receive(.endTransaction) 29 | } 30 | } 31 | 32 | private struct Sink: UniqueOwnedSink { 33 | typealias Owner = DependentValue 34 | unowned(unsafe) let owner: Owner 35 | func receive(_ update: ValueUpdate) { 36 | owner.receive(update) 37 | } 38 | } 39 | 40 | public init(setter: @escaping (Value) -> ()) { 41 | self.setter = setter 42 | self.origin = nil 43 | } 44 | 45 | public init(origin: Origin, setter: @escaping (Value) -> ()) where Origin.Value == Value { 46 | self.setter = setter 47 | self.origin = origin.anyObservableValue 48 | origin.add(Sink(owner: self)) 49 | } 50 | 51 | deinit { 52 | origin?.remove(Sink(owner: self)) 53 | } 54 | 55 | func receive(_ update: ValueUpdate) { 56 | switch update { 57 | case .beginTransaction: 58 | transactions += 1 59 | case .change(let change): 60 | pending = change.new 61 | case .endTransaction: 62 | transactions -= 1 63 | if transactions == 0, let pending = pending { 64 | self.pending = nil 65 | setter(pending) 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/DispatchSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchSource.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-24. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import Foundation 10 | 11 | extension SourceType { 12 | public func dispatch(on queue: DispatchQueue) -> AnySource { 13 | return TransformedSource(input: self, transform: SinkTransformForDispatchQueue(queue)).anySource 14 | } 15 | 16 | public func dispatch(on queue: OperationQueue) -> AnySource { 17 | return TransformedSource(input: self, transform: SinkTransformForOperationQueue(queue)).anySource 18 | } 19 | } 20 | 21 | final class SinkTransformForDispatchQueue: SinkTransform { 22 | typealias Input = Value 23 | typealias Output = Value 24 | 25 | let queue: DispatchQueue 26 | 27 | init(_ queue: DispatchQueue) { 28 | self.queue = queue 29 | } 30 | 31 | func apply(_ input: Value, _ sink: Sink) where Sink.Value == Value { 32 | queue.async { 33 | sink.receive(input) 34 | } 35 | } 36 | } 37 | 38 | final class SinkTransformForOperationQueue: SinkTransform { 39 | typealias Input = Value 40 | typealias Output = Value 41 | 42 | let queue: OperationQueue 43 | 44 | init(_ queue: OperationQueue) { 45 | self.queue = queue 46 | } 47 | 48 | func apply(_ input: Value, _ sink: Sink) where Sink.Value == Value { 49 | if OperationQueue.current == queue { 50 | sink.receive(input) 51 | } 52 | else { 53 | queue.addOperation { 54 | sink.receive(input) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/DistinctUnion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DistinctUnion.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-04. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableArrayType where Element: Hashable { 10 | /// Returns an observable set that contains the same elements as this array. 11 | public func distinctUnion() -> AnyObservableSet { 12 | return DistinctUnion(self).anyObservableSet 13 | } 14 | } 15 | 16 | private class DistinctUnion: _BaseObservableSet 17 | where Input.Element: Hashable { 18 | typealias Element = Input.Element 19 | typealias Change = SetChange 20 | 21 | private struct DistinctSink: UniqueOwnedSink { 22 | typealias Owner = DistinctUnion 23 | 24 | unowned(unsafe) let owner: Owner 25 | 26 | func receive(_ update: ArrayUpdate) { 27 | owner.apply(update) 28 | } 29 | } 30 | 31 | private let input: Input 32 | private var members = Dictionary() 33 | 34 | init(_ input: Input) { 35 | self.input = input 36 | super.init() 37 | for element in input.value { 38 | _ = self.add(element) 39 | } 40 | input.updates.add(DistinctSink(owner: self)) 41 | } 42 | 43 | deinit { 44 | input.updates.remove(DistinctSink(owner: self)) 45 | } 46 | 47 | func apply(_ update: ArrayUpdate) { 48 | switch update { 49 | case .beginTransaction: 50 | beginTransaction() 51 | case .change(let change): 52 | var setChange = SetChange() 53 | for mod in change.modifications { 54 | mod.forEachOldElement { 55 | if remove($0) { 56 | setChange.remove($0) 57 | } 58 | } 59 | mod.forEachNewElement { 60 | if add($0) { 61 | setChange.insert($0) 62 | } 63 | } 64 | } 65 | if !setChange.isEmpty { 66 | sendChange(setChange) 67 | } 68 | case .endTransaction: 69 | endTransaction() 70 | } 71 | } 72 | 73 | private func add(_ element: Element) -> Bool { 74 | if let old = self.members[element] { 75 | self.members[element] = old + 1 76 | return false 77 | } 78 | self.members[element] = 1 79 | return true 80 | } 81 | 82 | private func remove(_ element: Element) -> Bool { 83 | let old = self.members[element]! 84 | if old == 1 { 85 | self.members[element] = nil 86 | return true 87 | } 88 | self.members[element] = old - 1 89 | return false 90 | } 91 | 92 | override var isBuffered: Bool { return true } 93 | override var count: Int { return value.count } 94 | override var value: Set { return Set(members.keys) } 95 | override func contains(_ element: Element) -> Bool { return members[element] != nil } 96 | override func isSubset(of other: Set) -> Bool { 97 | guard count <= other.count else { return false } 98 | for (key, _) in members { 99 | guard other.contains(key) else { return false } 100 | } 101 | return true 102 | } 103 | override func isSuperset(of other: Set) -> Bool { 104 | guard count >= other.count else { return false } 105 | for element in other { 106 | guard members[element] != nil else { return false } 107 | } 108 | return true 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(VERSION_STRING) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/Locks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Locks.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-12-01. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import Foundation 10 | 11 | internal protocol Lockable { 12 | func lock() 13 | func unlock() 14 | func withLock(_ block: () throws -> Result) rethrows -> Result 15 | } 16 | 17 | extension Lockable { 18 | func withLock(_ block: () throws -> Result) rethrows -> Result { 19 | lock() 20 | defer { unlock() } 21 | return try block() 22 | } 23 | } 24 | 25 | struct Lock: Lockable { 26 | private let _lock: LockImplementation 27 | 28 | init() { 29 | if #available(macOS 10.12, iOS 10, watchOS 3.0, tvOS 10.0, *) { 30 | self._lock = UnfairLock() 31 | } 32 | else { 33 | self._lock = PosixMutex() 34 | } 35 | } 36 | func lock() { _lock.lock() } 37 | func unlock() { _lock.unlock() } 38 | } 39 | 40 | private class LockImplementation: Lockable { 41 | init() {} 42 | 43 | func lock() {} 44 | func unlock() {} 45 | } 46 | 47 | @available(macOS 10.12, iOS 10, watchOS 3.0, tvOS 10.0, *) 48 | private final class UnfairLock: LockImplementation { 49 | private var _lock = os_unfair_lock() 50 | 51 | override func lock() { 52 | os_unfair_lock_lock(&_lock) 53 | } 54 | 55 | override func unlock() { 56 | os_unfair_lock_unlock(&_lock) 57 | } 58 | } 59 | 60 | private final class PosixMutex: LockImplementation { 61 | private var mutex = pthread_mutex_t() 62 | 63 | override init() { 64 | let result = pthread_mutex_init(&mutex, nil) 65 | precondition(result == 0) 66 | } 67 | 68 | deinit { 69 | let result = pthread_mutex_destroy(&mutex) 70 | precondition(result == 0) 71 | } 72 | 73 | override func lock() { 74 | let result = pthread_mutex_lock(&mutex) 75 | precondition(result == 0) 76 | } 77 | 78 | override func unlock() { 79 | let result = pthread_mutex_unlock(&mutex) 80 | precondition(result == 0) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/MergedSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MergedSource.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-12-04. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import SipHash 10 | 11 | extension Sequence where Element: SourceType { 12 | public func gather() -> MergedSource { 13 | return MergedSource(sources: self) 14 | } 15 | } 16 | 17 | extension SourceType { 18 | /// Returns a source that merges self with `source`. The returned source will forward all values sent by either 19 | /// of its two input sources to its own connected sinks. 20 | /// 21 | /// It is fine to chain multiple merges together: `MergedSource` has its own, specialized `merge` method to 22 | /// collapse multiple merges into a single source. 23 | public func merged(with source: S) -> MergedSource where S.Value == Value { 24 | return MergedSource(sources: [self.anySource, source.anySource]) 25 | } 26 | 27 | public static func merge(_ sources: Self...) -> MergedSource { 28 | return MergedSource(sources: sources.map { s in s.anySource }) 29 | } 30 | 31 | public static func merge(_ sources: S) -> MergedSource where S.Iterator.Element == Self { 32 | return MergedSource(sources: sources.map { s in s.anySource }) 33 | } 34 | } 35 | 36 | /// A Source that receives all values from a set of input sources and forwards all to its own connected sinks. 37 | /// 38 | /// Note that MergedSource only connects to its input sources while it has at least one connection of its own. 39 | public final class MergedSource: SignalerSource { 40 | public typealias SourceValue = Value 41 | 42 | private let inputs: [AnySource] 43 | 44 | /// Initializes a new merged source with `sources` as its input sources. 45 | public init(sources: S) where S.Iterator.Element: SourceType, S.Iterator.Element.Value == Value { 46 | self.inputs = sources.map { $0.anySource } 47 | } 48 | 49 | override func activate() { 50 | for i in 0 ..< inputs.count { 51 | inputs[i].add(MergedSink(source: self, index: i)) 52 | } 53 | } 54 | 55 | override func deactivate() { 56 | for i in 0 ..< inputs.count { 57 | inputs[i].remove(MergedSink(source: self, index: i)) 58 | } 59 | } 60 | 61 | fileprivate func receive(_ value: Value, from index: Int) { 62 | signal.send(value) 63 | } 64 | 65 | /// Returns a new MergedSource that merges the same sources as self but also listens to `source`. 66 | /// The returned source will forward all values sent by either of its input sources to its own connected sinks. 67 | public func merged(with source: Source) -> MergedSource where Source.Value == Value { 68 | return MergedSource(sources: self.inputs + [source.anySource]) 69 | } 70 | } 71 | 72 | private struct MergedSink: SinkType, SipHashable { 73 | let source: MergedSource 74 | let index: Int 75 | 76 | func receive(_ value: Value) { 77 | source.receive(value, from: index) 78 | } 79 | 80 | func appendHashes(to hasher: inout SipHasher) { 81 | hasher.append(ObjectIdentifier(source)) 82 | hasher.append(index) 83 | } 84 | 85 | static func ==(left: MergedSink, right: MergedSink) -> Bool { 86 | return left.source === right.source && left.index == right.index 87 | } 88 | } 89 | 90 | 91 | -------------------------------------------------------------------------------- /Sources/NSButton Glue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSButton Glue.swift 3 | // macOS 4 | // 5 | // Created by Károly Lőrentey on 2017-09-05. 6 | // Copyright © 2017 Károly Lőrentey. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | 12 | extension NSButton { 13 | @objc open dynamic override var glue: GlueForNSButton { return _glue() } 14 | } 15 | 16 | public func <-- (target: GlueForNSButton.StateReceiver, model: V) where V.Value == NSControl.StateValue { 17 | target.glue.model = model.anyUpdatableValue 18 | } 19 | 20 | public func <-- (target: GlueForNSButton.StateReceiver, model: B) where B.Value == Bool { 21 | target.glue.model = model.map({ $0 ? .on : .off }, inverse: { $0 == .off ? false : true }) 22 | } 23 | 24 | open class GlueForNSButton: GlueForNSControl { 25 | private var object: NSButton { return owner as! NSButton } 26 | 27 | public struct StateReceiver { 28 | let glue: GlueForNSButton 29 | } 30 | 31 | public var state: StateReceiver { return StateReceiver(glue: self) } 32 | 33 | private let modelConnector = Connector() 34 | fileprivate var model: AnyUpdatableValue? { 35 | didSet { 36 | modelConnector.disconnect() 37 | if object.target === self { 38 | object.target = nil 39 | object.action = nil 40 | } 41 | if let model = model { 42 | object.target = self 43 | object.action = #selector(GlueForNSButton.buttonAction(_:)) 44 | modelConnector.connect(model.values) { [unowned self] value in 45 | self.object.state = value 46 | } 47 | } 48 | } 49 | } 50 | 51 | @IBAction func buttonAction(_ sender: NSButton) { 52 | self.model?.value = sender.state 53 | } 54 | } 55 | #endif 56 | -------------------------------------------------------------------------------- /Sources/NSNotificationCenter Support.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSNotificationCenter Support.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-11-30. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NotificationCenter { 12 | open override var glue: GlueForNotificationCenter { 13 | return _glue() 14 | } 15 | } 16 | 17 | open class GlueForNotificationCenter: GlueForNSObject { 18 | private var object: NotificationCenter { return owner as! NotificationCenter } 19 | 20 | /// Creates a Source that observes the specified notifications and forwards it to its connected sinks. 21 | /// 22 | /// The returned source holds strong references to the notification center and the sender (if any). 23 | /// The source will only observe the notification while a sink is actually connected. 24 | /// 25 | /// - Parameter name: The name of the notification to observe. 26 | /// - Parameter sender: The sender of the notifications to observe, or nil for any object. This parameter is nil by default. 27 | /// - Parameter queue: The operation queue on which the source will trigger. If you pass nil, the sinks are run synchronously on the thread that posted the notification. This parameter is nil by default. 28 | /// - Returns: A Source that triggers when the specified notification is posted. 29 | public func source(forName name: NSNotification.Name, sender: AnyObject? = nil, queue: OperationQueue? = nil) -> AnySource { 30 | return NotificationSource(center: object, name: name, sender: sender, queue: queue).anySource 31 | } 32 | } 33 | 34 | private class NotificationSource: SignalerSource { 35 | let center: NotificationCenter 36 | let name: NSNotification.Name 37 | let sender: AnyObject? 38 | let queue: OperationQueue? 39 | 40 | init(center: NotificationCenter, name: NSNotification.Name, sender: AnyObject?, queue: OperationQueue?) { 41 | self.center = center 42 | self.name = name 43 | self.sender = sender 44 | self.queue = queue 45 | super.init() 46 | } 47 | 48 | override func activate() { 49 | center.addObserver(self, selector: #selector(didReceive(_:)), name: name, object: sender) 50 | } 51 | 52 | override func deactivate() { 53 | center.removeObserver(self, name: name, object: sender) 54 | } 55 | 56 | @objc private func didReceive(_ notification: Notification) { 57 | if let queue = queue { 58 | queue.addOperation { 59 | self.signal.send(notification) 60 | } 61 | } 62 | else { 63 | self.signal.send(notification) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/NSPopUpButton Glue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSPopUpButton Glue.swift 3 | // macOS 4 | // 5 | // Created by Károly Lőrentey on 2017-09-05. 6 | // Copyright © 2017 Károly Lőrentey. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | 12 | extension NSPopUpButton { 13 | @objc open dynamic override var glue: GlueForNSPopUpButton { return _glue() } 14 | } 15 | 16 | public func <-- (target: GlueForNSPopUpButton, choices: NSPopUpButton.Choices) { 17 | target.setChoices(choices) 18 | } 19 | 20 | extension NSPopUpButton { 21 | public struct Choices { 22 | let model: AnyUpdatableValue 23 | let values: AnyObservableArray<(label: String, value: Value)> 24 | 25 | public init(model: U, values: C) where U.Value == Value, C.Element == (label: String, value: Value) { 26 | self.model = model.anyUpdatableValue 27 | self.values = values.anyObservableArray 28 | } 29 | 30 | public init(model: U, values: S) where U.Value == Value, S.Element == (label: String, value: Value) { 31 | self.model = model.anyUpdatableValue 32 | self.values = AnyObservableArray.constant(Array(values)) 33 | } 34 | 35 | public init(model: U, values: DictionaryLiteral) where U.Value == Value { 36 | self.model = model.anyUpdatableValue 37 | self.values = AnyObservableArray.constant(Array(values.map { ($0.key, $0.value) })) 38 | } 39 | } 40 | } 41 | 42 | open class GlueForNSPopUpButton: GlueForNSButton { 43 | private var object: NSPopUpButton { return owner as! NSPopUpButton } 44 | 45 | private var valueConnection: Connection? = nil 46 | private var choicesConnection: Connection? = nil 47 | private var update: (Any?) -> Void = { _ in } 48 | 49 | fileprivate func setChoices(_ choices: NSPopUpButton.Choices) { 50 | 51 | valueConnection?.disconnect() 52 | choicesConnection?.disconnect() 53 | 54 | update = { value in if let value = value as? Value { choices.model.value = value } } 55 | 56 | choicesConnection = choices.values.anyObservableValue.values.subscribe { [unowned self] choices in 57 | let menu = NSMenu() 58 | choices.forEach { choice in 59 | let item = NSMenuItem(title: choice.label, action: #selector(GlueForNSPopUpButton.choiceAction(_:)), keyEquivalent: "") 60 | item.target = self 61 | item.representedObject = choice.value 62 | menu.addItem(item) 63 | } 64 | self.object.menu = menu 65 | } 66 | 67 | valueConnection = choices.model.values.subscribe { [unowned self] newValue in 68 | if let item = self.object.menu?.items.first(where: { $0.representedObject as? Value == newValue }) { 69 | if self.object.selectedItem != item { 70 | self.object.select(item) 71 | } 72 | } 73 | else { 74 | self.object.select(nil) 75 | } 76 | } 77 | } 78 | 79 | @IBAction func choiceAction(_ sender: NSMenuItem) { 80 | update(sender.representedObject) 81 | } 82 | } 83 | #endif 84 | -------------------------------------------------------------------------------- /Sources/NSTextField Glue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextField Glue.swift 3 | // macOS 4 | // 5 | // Created by Károly Lőrentey on 2017-09-05. 6 | // Copyright © 2017 Károly Lőrentey. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | import AppKit 11 | 12 | extension NSTextField { 13 | @objc open dynamic override var glue: GlueForNSTextField { return _glue() } 14 | } 15 | 16 | public func <-- (target: GlueForNSTextField.ValidatingValueReceiver, model: V) where V.Value: LosslessStringConvertible { 17 | target.glue.setModel(model) 18 | } 19 | 20 | open class GlueForNSTextField: GlueForNSControl { 21 | private var object: NSTextField { return owner as! NSTextField } 22 | private var delegate: Any? = nil 23 | 24 | public struct ValidatingValueReceiver { let glue: GlueForNSTextField } 25 | public var value: ValidatingValueReceiver { return ValidatingValueReceiver(glue: self) } 26 | 27 | fileprivate func setModel(_ model: V) where V.Value: LosslessStringConvertible { 28 | if let delegate = self.delegate as? GlueKitTextFieldDelegate { 29 | delegate.model = model.anyUpdatableValue 30 | } 31 | else { 32 | let delegate = GlueKitTextFieldDelegate(object, model) 33 | self.delegate = delegate 34 | } 35 | } 36 | } 37 | 38 | class GlueKitTextFieldDelegate: NSObject, NSTextFieldDelegate { 39 | unowned let view: NSTextField 40 | var model: AnyUpdatableValue { 41 | didSet { reconnect() } 42 | } 43 | 44 | init(_ view: NSTextField, _ model: V) where V.Value == Value { 45 | self.view = view 46 | self.model = model.anyUpdatableValue 47 | super.init() 48 | reconnect() 49 | } 50 | 51 | private var modelConnection: Connection? = nil 52 | private func reconnect() { 53 | view.delegate = self 54 | modelConnection?.disconnect() 55 | modelConnection = model.values.subscribe { [unowned self] value in 56 | self.view.stringValue = "\(value)" 57 | } 58 | } 59 | 60 | func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool { 61 | return Value(view.stringValue) != nil 62 | } 63 | 64 | override func controlTextDidEndEditing(_ obj: Notification) { 65 | if let value = Value(view.stringValue) { 66 | model.value = value 67 | } 68 | else { 69 | view.stringValue = "\(model.value)" 70 | } 71 | } 72 | 73 | func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { 74 | guard commandSelector == #selector(NSResponder.cancelOperation(_:)) else { return false } 75 | view.stringValue = "\(model.value)" 76 | //textView.window?.makeFirstResponder(nil) 77 | return true 78 | } 79 | } 80 | #endif 81 | -------------------------------------------------------------------------------- /Sources/ObservableContains.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableContains.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-02. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableSetType { 10 | public func observableContains(_ member: Element) -> AnyObservableValue { 11 | return ObservableContains(input: self, member: member).anyObservableValue 12 | } 13 | } 14 | 15 | private final class ObservableContains: _AbstractObservableValue { 16 | let input: Input 17 | let member: Input.Element 18 | let _updates: AnySource> 19 | 20 | init(input: Input, member: Input.Element) { 21 | self.input = input 22 | self.member = member 23 | self._updates = input.updates.flatMap { update in 24 | update.flatMap { 25 | let old = $0.removed.contains(member) 26 | let new = $0.inserted.contains(member) 27 | if old == new { 28 | return nil 29 | } 30 | else { 31 | return ValueChange(from: old, to: new) 32 | } 33 | } 34 | } 35 | } 36 | 37 | override var value: Bool { 38 | return input.contains(member) 39 | } 40 | 41 | override func add(_ sink: Sink) where Sink.Value == Update { 42 | _updates.add(sink) 43 | } 44 | 45 | @discardableResult 46 | override func remove(_ sink: Sink) -> Sink where Sink.Value == Update { 47 | return _updates.remove(sink) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/ObservableType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableValueType.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-04. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | public protocol ObservableType { 10 | associatedtype Value 11 | associatedtype Change: ChangeType where Change.Value == Value 12 | 13 | /// The current value of this observable. 14 | var value: Value { get } 15 | 16 | func add(_ sink: Sink) where Sink.Value == Update 17 | 18 | @discardableResult 19 | func remove(_ sink: Sink) -> Sink where Sink.Value == Update 20 | } 21 | 22 | extension ObservableType { 23 | /// A source that reports update transaction events for this observable. 24 | public var updates: UpdateSource { 25 | return UpdateSource(owner: self) 26 | } 27 | } 28 | 29 | extension ObservableType { 30 | /// A source that sends an empty value whenever the observable completes a transaction. 31 | public var tick: AnySource { 32 | return self.updates.flatMap { if case .endTransaction = $0 { return () }; return nil } 33 | } 34 | } 35 | 36 | public struct UpdateSource: SourceType { 37 | public typealias Value = Update 38 | 39 | private let owner: Observable 40 | 41 | init(owner: Observable) { 42 | self.owner = owner 43 | } 44 | 45 | public func add(_ sink: Sink) where Sink.Value == Value { 46 | owner.add(sink) 47 | } 48 | 49 | @discardableResult 50 | public func remove(_ sink: Sink) -> Sink where Sink.Value == Value { 51 | return owner.remove(sink) 52 | } 53 | } 54 | 55 | public protocol UpdatableType: ObservableType { 56 | /// The current value of this observable. 57 | /// 58 | /// The setter is nonmutating because the value ultimately needs to be stored in a reference type anyway. 59 | var value: Value { get nonmutating set } 60 | 61 | func apply(_ update: Update) 62 | } 63 | 64 | extension UpdatableType { 65 | public func withTransaction(_ body: () -> Result) -> Result { 66 | apply(.beginTransaction) 67 | defer { apply(.endTransaction) } 68 | return body() 69 | } 70 | 71 | public func apply(_ change: Change) { 72 | if !change.isEmpty { 73 | apply(.beginTransaction) 74 | apply(.change(change)) 75 | apply(.endTransaction) 76 | } 77 | } 78 | } 79 | 80 | extension ObservableType { 81 | public func subscribe(to updatable: Updatable) -> Connection 82 | where Updatable.Change == Change { 83 | updatable.apply(.beginTransaction) 84 | updatable.apply(.change(Change(from: updatable.value, to: self.value))) 85 | let connection = updates.subscribe { update in updatable.apply(update) } 86 | updatable.apply(.endTransaction) 87 | return connection 88 | } 89 | } 90 | 91 | extension Connector { 92 | @discardableResult 93 | public func subscribe(_ observable: Observable, to sink: @escaping (Update) -> Void) -> Connection { 94 | return observable.updates.subscribe(sink).putInto(self) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/OwnedSink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StrongMethodSink.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-24. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import SipHash 10 | 11 | protocol UniqueOwnedSink: SinkType { 12 | associatedtype Owner: AnyObject 13 | 14 | var owner: Owner { get } 15 | } 16 | 17 | extension UniqueOwnedSink { 18 | var hashValue: Int { 19 | return ObjectIdentifier(owner).hashValue 20 | } 21 | 22 | static func ==(left: Self, right: Self) -> Bool { 23 | return left.owner === right.owner 24 | } 25 | } 26 | 27 | protocol OwnedSink: SinkType, SipHashable { 28 | associatedtype Owner: AnyObject 29 | associatedtype Identifier: Hashable 30 | 31 | var owner: Owner { get } 32 | var identifier: Identifier { get } 33 | } 34 | 35 | extension OwnedSink { 36 | func appendHashes(to hasher: inout SipHasher) { 37 | hasher.append(ObjectIdentifier(owner)) 38 | hasher.append(identifier) 39 | } 40 | 41 | static func ==(left: Self, right: Self) -> Bool { 42 | return left.owner === right.owner && left.identifier == right.identifier 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Reference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reference.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-12-13. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | internal struct UnownedReference { 10 | unowned var value: Target 11 | 12 | init(_ value: Target) { 13 | self.value = value 14 | } 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /Sources/SetChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetChange.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-08-12. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | public struct SetChange: ChangeType { 10 | public typealias Value = Set 11 | 12 | public private(set) var removed: Set 13 | public private(set) var inserted: Set 14 | 15 | public init(removed: Set = [], inserted: Set = []) { 16 | self.inserted = inserted 17 | self.removed = removed 18 | } 19 | 20 | public init(from oldValue: Value, to newValue: Value) { 21 | self.removed = oldValue.subtracting(newValue) 22 | self.inserted = newValue.subtracting(oldValue) 23 | } 24 | 25 | public var isEmpty: Bool { 26 | return inserted.isEmpty && removed.isEmpty 27 | } 28 | 29 | public func apply(on value: inout Set) { 30 | value.subtract(removed) 31 | value = inserted.union(value) 32 | } 33 | 34 | public func apply(on value: Value) -> Value { 35 | return inserted.union(value.subtracting(removed)) 36 | } 37 | 38 | public mutating func remove(_ element: Element) { 39 | self.inserted.remove(element) 40 | self.removed.update(with: element) 41 | } 42 | 43 | public mutating func insert(_ element: Element) { 44 | self.inserted.update(with: element) 45 | } 46 | 47 | public mutating func merge(with next: SetChange) { 48 | removed = next.removed.union(removed) 49 | inserted = next.inserted.union(inserted.subtracting(next.removed)) 50 | } 51 | 52 | public func merged(with next: SetChange) -> SetChange { 53 | return SetChange(removed: next.removed.union(removed), 54 | inserted: next.inserted.union(inserted.subtracting(next.removed))) 55 | } 56 | 57 | public func reversed() -> SetChange { 58 | return SetChange(removed: inserted, inserted: removed) 59 | } 60 | 61 | public func removingEqualChanges() -> SetChange { 62 | let intersection = removed.intersection(inserted) 63 | if intersection.isEmpty { 64 | return self 65 | } 66 | return SetChange(removed: removed.subtracting(intersection), inserted: inserted.subtracting(intersection)) 67 | } 68 | } 69 | 70 | extension Set { 71 | public mutating func apply(_ change: SetChange) { 72 | self.subtract(change.removed) 73 | for e in change.inserted { 74 | self.update(with: e) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/SetFolding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetFolding.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-09. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableSetType { 10 | /// Returns an observable whose value is always equal to `self.value.reduce(initial, add)`. 11 | /// 12 | /// - Parameter initial: The accumulation starts with this initial value. 13 | /// - Parameter add: A closure that adds an element of the set into an accumulated value. 14 | /// - Parameter remove: A closure that cancels the effect of an earlier `add`. 15 | /// - Returns: An observable value for the reduction of this set. 16 | /// 17 | /// - Note: Elements are added and removed in no particular order. 18 | /// (I.e., the underlying binary operation over `Result` must form an abelian group.) 19 | /// 20 | /// - SeeAlso: `sum()` which returns a reduction using addition. 21 | public func reduce(_ initial: Result, add: @escaping (Result, Element) -> Result, remove: @escaping (Result, Element) -> Result) -> AnyObservableValue { 22 | return SetFoldingByTwoWayFunction(parent: self, initial: initial, add: add, remove: remove).anyObservableValue 23 | } 24 | } 25 | 26 | extension ObservableSetType where Element: BinaryInteger { 27 | /// Return the (observable) sum of the elements contained in this set. 28 | public func sum() -> AnyObservableValue { 29 | return reduce(0, add: +, remove: -) 30 | } 31 | } 32 | 33 | private class SetFoldingByTwoWayFunction: _BaseObservableValue { 34 | private struct FoldingSink: UniqueOwnedSink { 35 | typealias Owner = SetFoldingByTwoWayFunction 36 | 37 | unowned(unsafe) let owner: Owner 38 | 39 | func receive(_ update: SetUpdate) { 40 | owner.applyUpdate(update) 41 | } 42 | } 43 | 44 | let parent: Parent 45 | let add: (Value, Parent.Element) -> Value 46 | let remove: (Value, Parent.Element) -> Value 47 | 48 | private var _value: Value 49 | 50 | init(parent: Parent, initial: Value, add: @escaping (Value, Parent.Element) -> Value, remove: @escaping (Value, Parent.Element) -> Value) { 51 | self.parent = parent 52 | self.add = add 53 | self.remove = remove 54 | 55 | self._value = parent.value.reduce(initial, add) 56 | 57 | super.init() 58 | 59 | parent.add(FoldingSink(owner: self)) 60 | } 61 | 62 | deinit { 63 | parent.remove(FoldingSink(owner: self)) 64 | } 65 | 66 | override var value: Value { 67 | return _value 68 | } 69 | 70 | func applyUpdate(_ update: SetUpdate) { 71 | switch update { 72 | case .beginTransaction: 73 | beginTransaction() 74 | case .change(let change): 75 | let old = _value 76 | for old in change.removed { _value = remove(_value, old) } 77 | for new in change.inserted { _value = add(_value, new) } 78 | sendChange(ValueChange(from: old, to: _value)) 79 | case .endTransaction: 80 | endTransaction() 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/SetGatheringSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetGatheringSource.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2017-09-05. 6 | // Copyright © 2017 Károly Lőrentey. All rights reserved. 7 | // 8 | 9 | extension ObservableSetType where Element: SourceType { 10 | public func gather() -> AnySource { 11 | return SetGatheringSource(self).anySource 12 | } 13 | } 14 | 15 | private class SetGatheringSource: _AbstractSource 16 | where Origin.Element: SourceType, Origin.Element.Value == Value { 17 | let origin: Origin 18 | var sinks: Set> = [] 19 | 20 | private struct GatherSink: UniqueOwnedSink { 21 | typealias Owner = SetGatheringSource 22 | unowned let owner: Owner 23 | 24 | func receive(_ value: SetUpdate) { 25 | guard case let .change(change) = value else { return } 26 | change.removed.forEach { source in 27 | for sink in owner.sinks { 28 | source.remove(sink) 29 | } 30 | } 31 | change.inserted.forEach { source in 32 | for sink in owner.sinks { 33 | source.add(sink) 34 | } 35 | } 36 | } 37 | } 38 | 39 | init(_ origin: Origin) { 40 | self.origin = origin 41 | } 42 | 43 | override func add(_ sink: Sink) where Sink.Value == Value { 44 | if sinks.isEmpty { 45 | origin.add(GatherSink(owner: self)) 46 | } 47 | let new = sinks.insert(sink.anySink).inserted 48 | precondition(new) 49 | for source in origin.value { 50 | source.add(sink) 51 | } 52 | } 53 | 54 | @discardableResult 55 | override func remove(_ sink: Sink) -> Sink where Sink.Value == Value { 56 | let result = sinks.remove(sink.anySink)! 57 | for source in origin.value { 58 | source.remove(result) 59 | } 60 | if sinks.isEmpty { 61 | origin.remove(GatherSink(owner: self)) 62 | } 63 | return result.opened()! 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /Sources/SetMappingBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetMappingBase.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-05. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | /// An observable set where the value is internally represented as a dictionary of element multiplicities. 10 | /// This class implements the full `ObservableSetType` protocol, and serves as the base class for several transformations 11 | /// on observable sets. 12 | class SetMappingBase: _BaseObservableSet { 13 | typealias Change = SetChange 14 | 15 | private(set) var contents: [Element: Int] = [:] 16 | 17 | /// Insert `newMember` into `state`, and return true iff it did not previously contain it. 18 | final func insert(_ newMember: Element) -> Bool { 19 | if let count = contents[newMember] { 20 | contents[newMember] = count + 1 21 | return false 22 | } 23 | contents[newMember] = 1 24 | return true 25 | } 26 | 27 | /// Remove a single instance of `newMember` from `state`, and return true iff this was the last instance. 28 | /// - Requires: `self.contains(member)`. 29 | final func remove(_ member: Element) -> Bool { 30 | guard let count = contents[member] else { 31 | fatalError("Inconsistent change: \(member) to be removed is not in result set") 32 | } 33 | if count > 1 { 34 | contents[member] = count - 1 35 | return false 36 | } 37 | contents.removeValue(forKey: member) 38 | return true 39 | } 40 | 41 | final override var isBuffered: Bool { return false } 42 | final override var count: Int { return contents.count } 43 | final override var value: Set { return Set(contents.keys) } 44 | final override func contains(_ member: Element) -> Bool { return contents[member] != nil } 45 | 46 | final override func isSubset(of other: Set) -> Bool { 47 | guard other.count >= contents.count else { return false } 48 | for (key, _) in contents { 49 | guard other.contains(key) else { return false } 50 | } 51 | return true 52 | } 53 | 54 | final override func isSuperset(of other: Set) -> Bool { 55 | guard other.count <= contents.count else { return false } 56 | for element in other { 57 | guard contents[element] != nil else { return false } 58 | } 59 | return true 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/SetMappingForSequence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetMappingForSequence.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-07. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableSetType { 10 | public func flatMap(_ key: @escaping (Element) -> Result) -> AnyObservableSet where Result.Iterator.Element: Hashable { 11 | return SetMappingForSequence(parent: self, key: key).anyObservableSet 12 | } 13 | } 14 | 15 | class SetMappingForSequence: SetMappingBase 16 | where Result.Iterator.Element: Hashable { 17 | typealias Element = Result.Iterator.Element 18 | 19 | private struct ParentSink: UniqueOwnedSink { 20 | typealias Owner = SetMappingForSequence 21 | 22 | unowned(unsafe) let owner: Owner 23 | 24 | func receive(_ update: SetUpdate) { 25 | owner.apply(update) 26 | } 27 | } 28 | 29 | let parent: Parent 30 | let key: (Parent.Element) -> Result 31 | 32 | init(parent: Parent, key: @escaping (Parent.Element) -> Result) { 33 | self.parent = parent 34 | self.key = key 35 | super.init() 36 | for e in parent.value { 37 | for new in key(e) { 38 | _ = self.insert(new) 39 | } 40 | } 41 | parent.add(ParentSink(owner: self)) 42 | } 43 | 44 | deinit { 45 | parent.remove(ParentSink(owner: self)) 46 | } 47 | 48 | func apply(_ update: SetUpdate) { 49 | switch update { 50 | case .beginTransaction: 51 | beginTransaction() 52 | case .change(let change): 53 | var transformedChange = SetChange() 54 | for e in change.removed { 55 | for old in key(e) { 56 | if self.remove(old) { 57 | transformedChange.remove(old) 58 | } 59 | } 60 | } 61 | for e in change.inserted { 62 | for new in key(e) { 63 | if self.insert(new) { 64 | transformedChange.insert(new) 65 | } 66 | } 67 | } 68 | if !transformedChange.isEmpty { 69 | sendChange(transformedChange) 70 | } 71 | case .endTransaction: 72 | endTransaction() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/SetMappingForSetField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetMappingForSetField.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-07. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableSetType { 10 | public func flatMap(_ key: @escaping (Element) -> Field) -> AnyObservableSet { 11 | return SetMappingForSetField(parent: self, key: key).anyObservableSet 12 | } 13 | } 14 | 15 | class SetMappingForSetField: SetMappingBase { 16 | private struct ParentSink: UniqueOwnedSink { 17 | typealias Owner = SetMappingForSetField 18 | 19 | unowned(unsafe) let owner: Owner 20 | 21 | func receive(_ update: SetUpdate) { 22 | owner.applyParentUpdate(update) 23 | } 24 | } 25 | 26 | private struct FieldSink: UniqueOwnedSink { 27 | typealias Owner = SetMappingForSetField 28 | 29 | unowned(unsafe) let owner: Owner 30 | 31 | func receive(_ update: SetUpdate) { 32 | owner.applyFieldUpdate(update) 33 | } 34 | } 35 | 36 | let parent: Parent 37 | let key: (Parent.Element) -> Field 38 | 39 | init(parent: Parent, key: @escaping (Parent.Element) -> Field) { 40 | self.parent = parent 41 | self.key = key 42 | super.init() 43 | parent.add(ParentSink(owner: self)) 44 | 45 | for e in parent.value { 46 | let field = key(e) 47 | field.add(FieldSink(owner: self)) 48 | for new in field.value { 49 | _ = self.insert(new) 50 | } 51 | } 52 | } 53 | 54 | deinit { 55 | parent.remove(ParentSink(owner: self)) 56 | parent.value.forEach { e in 57 | let field = key(e) 58 | field.remove(FieldSink(owner: self)) 59 | } 60 | } 61 | 62 | func applyParentUpdate(_ update: SetUpdate) { 63 | switch update { 64 | case .beginTransaction: 65 | beginTransaction() 66 | case .change(let change): 67 | var transformedChange = SetChange() 68 | for e in change.removed { 69 | let field = key(e) 70 | field.remove(FieldSink(owner: self)) 71 | for r in field.value { 72 | if self.remove(r) { 73 | transformedChange.remove(r) 74 | } 75 | } 76 | } 77 | for e in change.inserted { 78 | let field = key(e) 79 | field.add(FieldSink(owner: self)) 80 | for i in field.value { 81 | if self.insert(i) { 82 | transformedChange.insert(i) 83 | } 84 | } 85 | } 86 | if !transformedChange.isEmpty { 87 | sendChange(transformedChange) 88 | } 89 | case .endTransaction: 90 | endTransaction() 91 | } 92 | } 93 | 94 | func applyFieldUpdate(_ update: SetUpdate) { 95 | switch update { 96 | case .beginTransaction: 97 | beginTransaction() 98 | case .change(let change): 99 | var transformedChange = SetChange() 100 | for old in change.removed { 101 | if self.remove(old) { 102 | transformedChange.remove(old) 103 | } 104 | } 105 | for new in change.inserted { 106 | if self.insert(new) { 107 | transformedChange.insert(new) 108 | } 109 | } 110 | if !transformedChange.isEmpty { 111 | sendChange(transformedChange) 112 | } 113 | case .endTransaction: 114 | endTransaction() 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/SetReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetReference.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-08-17. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableValueType where Value: ObservableSetType { 10 | public func unpacked() -> AnyObservableSet { 11 | return UnpackedObservableSetReference(self).anyObservableSet 12 | } 13 | } 14 | 15 | /// A mutable reference to an `AnyObservableSet` that's also an observable set. 16 | /// You can switch to another target set without having to re-register subscribers. 17 | private final class UnpackedObservableSetReference: _BaseObservableSet 18 | where Reference.Value: ObservableSetType { 19 | typealias Target = Reference.Value 20 | typealias Element = Target.Element 21 | typealias Change = SetChange 22 | 23 | private struct ReferenceSink: UniqueOwnedSink { 24 | typealias Owner = UnpackedObservableSetReference 25 | 26 | unowned(unsafe) let owner: Owner 27 | 28 | func receive(_ update: ValueUpdate) { 29 | owner.applyReferenceUpdate(update) 30 | } 31 | } 32 | 33 | private struct TargetSink: UniqueOwnedSink { 34 | typealias Owner = UnpackedObservableSetReference 35 | 36 | unowned(unsafe) let owner: Owner 37 | 38 | func receive(_ update: SetUpdate) { 39 | owner.applyTargetUpdate(update) 40 | } 41 | } 42 | 43 | private var _reference: Reference 44 | private var _target: Reference.Value? = nil // Retained to make sure we keep it alive 45 | 46 | init(_ reference: Reference) { 47 | _reference = reference 48 | super.init() 49 | } 50 | 51 | override func activate() { 52 | _reference.add(ReferenceSink(owner: self)) 53 | let target = _reference.value 54 | _target = target 55 | target.add(TargetSink(owner: self)) 56 | } 57 | 58 | override func deactivate() { 59 | _target!.remove(TargetSink(owner: self)) 60 | _reference.remove(ReferenceSink(owner: self)) 61 | } 62 | 63 | func applyReferenceUpdate(_ update: ValueUpdate) { 64 | switch update { 65 | case .beginTransaction: 66 | beginTransaction() 67 | case .change(let change): 68 | if isConnected { 69 | _target!.remove(TargetSink(owner: self)) 70 | _target = change.new 71 | _target!.add(TargetSink(owner: self)) 72 | sendChange(SetChange(from: change.old.value, to: change.new.value)) 73 | } 74 | case .endTransaction: 75 | endTransaction() 76 | } 77 | } 78 | 79 | func applyTargetUpdate(_ update: SetUpdate) { 80 | switch update { 81 | case .beginTransaction: 82 | beginTransaction() 83 | case .change(let change): 84 | sendChange(change) 85 | case .endTransaction: 86 | endTransaction() 87 | } 88 | } 89 | 90 | override var isBuffered: Bool { return false } 91 | override var count: Int { return _reference.value.count } 92 | override var value: Set { return _reference.value.value } 93 | override func contains(_ member: Element) -> Bool { return _reference.value.contains(member) } 94 | override func isSubset(of other: Set) -> Bool { return _reference.value.isSubset(of: other) } 95 | override func isSuperset(of other: Set) -> Bool { return _reference.value.isSuperset(of: other) } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/SetSortingByComparator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetSortingByComparator.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-07. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableSetType { 10 | public func sorted(by areInIncreasingOrder: @escaping (Element, Element) -> Bool) -> AnyObservableArray { 11 | let comparator = Comparator(areInIncreasingOrder) 12 | return self 13 | .sortedMap(by: { [unowned comparator] in ComparableWrapper($0, comparator) }) 14 | .map { [comparator] in _ = comparator; return $0.element } 15 | } 16 | 17 | public func sorted(by key: @escaping (Element) -> Key) -> AnyObservableArray { 18 | return self.sorted { a, b in key(a) < key(b) } 19 | } 20 | 21 | public func sorted(by comparator: Comparator) -> AnyObservableArray 22 | where Comparator.Value == (Element, Element) -> Bool { 23 | let reference: AnyObservableValue> = comparator.map { comparator in 24 | self.sorted(by: comparator).anyObservableArray 25 | } 26 | return reference.unpacked() 27 | } 28 | 29 | public func sorted(by key: ObservableKey) -> AnyObservableArray 30 | where ObservableKey.Value == (Element) -> Key { 31 | let reference: AnyObservableValue> = key.map { key in 32 | self.sorted(by: key).anyObservableArray 33 | } 34 | return reference.unpacked() 35 | } 36 | 37 | } 38 | 39 | private final class Comparator { 40 | let comparator: (Element, Element) -> Bool 41 | 42 | init(_ comparator: @escaping (Element, Element) -> Bool) { 43 | self.comparator = comparator 44 | } 45 | func compare(_ a: Element, _ b: Element) -> Bool { 46 | return comparator(a, b) 47 | } 48 | } 49 | 50 | private struct ComparableWrapper: Comparable { 51 | unowned(unsafe) let comparator: Comparator 52 | let element: Element 53 | 54 | init(_ element: Element, _ comparator: Comparator) { 55 | self.comparator = comparator 56 | self.element = element 57 | } 58 | static func ==(a: ComparableWrapper, b: ComparableWrapper) -> Bool { 59 | return !(a < b) && !(b < a) 60 | } 61 | static func <(a: ComparableWrapper, b: ComparableWrapper) -> Bool { 62 | return a.comparator.compare(a.element, b.element) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/SimpleSources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleSources.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-11-30. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension SourceType { 10 | /// Returns a source that never fires. 11 | public static func empty() -> AnySource { 12 | return NeverSource().anySource 13 | } 14 | 15 | /// Returns a source that never fires. 16 | public static func never() -> AnySource { 17 | return NeverSource().anySource 18 | } 19 | } 20 | 21 | class NeverSource: _AbstractSource { 22 | override func add(_ sink: Sink) where Sink.Value == Value { 23 | // Do nothing. 24 | } 25 | 26 | @discardableResult 27 | override func remove(_ sink: Sink) -> Sink where Sink.Value == Value { 28 | // Do nothing. 29 | return sink 30 | } 31 | } 32 | 33 | extension SourceType { 34 | /// Returns a source that fires exactly once with the given value, then never again. 35 | public static func just(_ value: Value) -> AnySource { 36 | return JustSource(value).anySource 37 | } 38 | } 39 | 40 | class JustSource: _AbstractSource { 41 | private var value: Value 42 | 43 | init(_ value: Value) { 44 | self.value = value 45 | super.init() 46 | } 47 | 48 | override func add(_ sink: Sink) where Sink.Value == Value { 49 | sink.receive(value) 50 | } 51 | 52 | @discardableResult 53 | override func remove(_ sink: Sink) -> Sink where Sink.Value == Value { 54 | return sink 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/TransactionalThing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransactionState.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-22. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | 10 | internal class TransactionalSignal: Signal> { 11 | typealias Value = Update 12 | 13 | var isInTransaction: Bool = false 14 | 15 | public override func add(_ sink: Sink) where Sink.Value == Value { 16 | if self.isInTransaction { 17 | // Make sure the new subscriber knows we're in the middle of a transaction. 18 | sink.receive(.beginTransaction) 19 | } 20 | super.add(sink) 21 | } 22 | 23 | @discardableResult 24 | public override func remove(_ sink: Sink) -> Sink where Sink.Value == Value { 25 | let old = super.remove(sink) 26 | if self.isInTransaction { 27 | // Wave goodbye by sending a virtual endTransaction that makes state management easier. 28 | old.receive(.endTransaction) 29 | } 30 | return old 31 | } 32 | } 33 | 34 | protocol TransactionalThing: class, SignalDelegate { 35 | associatedtype Change: ChangeType 36 | 37 | var _signal: TransactionalSignal? { get set } 38 | var _transactionCount: Int { get set } 39 | } 40 | 41 | extension TransactionalThing { 42 | var signal: TransactionalSignal { 43 | if let signal = _signal { return signal } 44 | let signal = TransactionalSignal() 45 | signal.isInTransaction = _transactionCount > 0 46 | signal.delegate = self 47 | _signal = signal 48 | return signal 49 | } 50 | 51 | func beginTransaction() { 52 | _transactionCount += 1 53 | if _transactionCount == 1, let signal = _signal { 54 | signal.isInTransaction = true 55 | signal.send(.beginTransaction) 56 | } 57 | } 58 | 59 | func sendChange(_ change: Change) { 60 | precondition(_transactionCount > 0) 61 | _signal?.send(.change(change)) 62 | } 63 | 64 | func sendIfConnected(_ change: @autoclosure () -> Change) { 65 | if isConnected { 66 | sendChange(change()) 67 | } 68 | } 69 | 70 | func endTransaction() { 71 | precondition(_transactionCount > 0) 72 | _transactionCount -= 1 73 | if _transactionCount == 0, let signal = _signal { 74 | signal.isInTransaction = false 75 | signal.send(.endTransaction) 76 | } 77 | } 78 | 79 | public func send(_ update: Update) { 80 | switch update { 81 | case .beginTransaction: beginTransaction() 82 | case .change(let change): sendChange(change) 83 | case .endTransaction: endTransaction() 84 | } 85 | } 86 | 87 | var isInTransaction: Bool { return _transactionCount > 0 } 88 | var isConnected: Bool { return _signal?.isConnected ?? false } 89 | var isActive: Bool { return isInTransaction || isConnected } 90 | var isInOuterMostTransaction: Bool { return _transactionCount == 1 } // Used by KVO 91 | } 92 | 93 | public class TransactionalSource: _AbstractSource>, TransactionalThing { 94 | internal var _signal: TransactionalSignal? = nil 95 | internal var _transactionCount = 0 96 | 97 | func activate() { 98 | } 99 | 100 | func deactivate() { 101 | } 102 | 103 | public override func add(_ sink: Sink) where Sink.Value == Value { 104 | signal.add(sink) 105 | } 106 | 107 | @discardableResult 108 | public override func remove(_ sink: Sink) -> Sink where Sink.Value == Value { 109 | return signal.remove(sink) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/TransformedSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransformedSource.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-11-30. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | final class TransformedSource: _AbstractSource where Transform.Input == Input.Value { 10 | let input: Input 11 | let transform: Transform 12 | 13 | init(input: Input, transform: Transform) { 14 | self.input = input 15 | self.transform = transform 16 | } 17 | 18 | final override func add(_ sink: Sink) where Sink.Value == Transform.Output { 19 | self.input.add(sink.transform(transform)) 20 | } 21 | 22 | @discardableResult 23 | final override func remove(_ sink: Sink) -> Sink where Sink.Value == Transform.Output { 24 | return self.input.remove(sink.transform(transform)).sink 25 | } 26 | } 27 | 28 | extension SourceType { 29 | /// Returns a source that, applies `transform` on each value produced by `self` and each subscriber sink. 30 | /// 31 | /// `transform` should be a pure function, i.e., one with no side effects or hidden parameters. 32 | /// For each value that is received from `self`, it is called as many times as there are subscribers. 33 | public func transform(_ type: Result.Type = Result.self, _ transform: @escaping (Value, (Result) -> Void) -> Void) -> AnySource { 34 | return TransformedSource(input: self, transform: SinkTransformFromClosure(transform)).anySource 35 | } 36 | 37 | public func map(_ transform: @escaping (Value) -> Output) -> AnySource { 38 | return TransformedSource(input: self, transform: SinkTransformFromMapping(transform)).anySource 39 | } 40 | 41 | public func flatMap(_ transform: @escaping (Value) -> Output?) -> AnySource { 42 | return TransformedSource(input: self, transform: SinkTransformFromOptionalMapping(transform)).anySource 43 | } 44 | 45 | public func filter(_ predicate: @escaping (Value) -> Bool) -> AnySource { 46 | return TransformedSource(input: self, transform: SinkTransformFromFilter(predicate)).anySource 47 | } 48 | 49 | public func flatMap(_ transform: @escaping (Value) -> S) -> AnySource { 50 | return TransformedSource(input: self, transform: SinkTransformFromSequence(transform)).anySource 51 | } 52 | 53 | public func mapToVoid() -> AnySource { 54 | return TransformedSource(input: self, transform: SinkTransformToConstant(())).anySource 55 | } 56 | 57 | public func mapToConstant(_ value: C) -> AnySource { 58 | return TransformedSource(input: self, transform: SinkTransformToConstant(value)).anySource 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/UIBarButtonItem Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIBarButtonItem Extensions.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-04-09. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | #if os(iOS) 10 | import UIKit 11 | 12 | private var associatedObjectKey: UInt8 = 0 13 | private let listenerAction = #selector(TargetActionListener.actionDidFire) 14 | 15 | extension UIBarButtonItem { 16 | 17 | public var actionSource: AnySource { 18 | if let target = objc_getAssociatedObject(self, &associatedObjectKey) as? TargetActionListener { 19 | return target.signal.anySource 20 | } 21 | let target = TargetActionListener() 22 | self.target = target 23 | self.action = listenerAction 24 | objc_setAssociatedObject(self, &associatedObjectKey, target, .OBJC_ASSOCIATION_RETAIN) 25 | return target.signal.anySource 26 | } 27 | 28 | public convenience init(barButtonSystemItem systemItem: UIBarButtonSystemItem, actionBlock: (() -> ())? = nil) { 29 | let target = TargetActionListener() 30 | self.init(barButtonSystemItem: systemItem, target: target, action: listenerAction) 31 | objc_setAssociatedObject(self, &associatedObjectKey, target, .OBJC_ASSOCIATION_RETAIN) 32 | 33 | if let actionBlock = actionBlock { 34 | self.glue.connector.connect(target.signal, to: actionBlock) 35 | } 36 | } 37 | 38 | public convenience init(image: UIImage?, style: UIBarButtonItemStyle, actionBlock: (() -> ())? = nil) { 39 | let target = TargetActionListener() 40 | self.init(image: image, style: style, target: target, action: listenerAction) 41 | objc_setAssociatedObject(self, &associatedObjectKey, target, .OBJC_ASSOCIATION_RETAIN) 42 | 43 | if let actionBlock = actionBlock { 44 | self.glue.connector.connect(target.signal, to: actionBlock) 45 | } 46 | } 47 | 48 | public convenience init(image: UIImage?, landscapeImagePhone: UIImage?, style: UIBarButtonItemStyle, actionBlock: (() -> ())? = nil) { 49 | let target = TargetActionListener() 50 | self.init(image: image, landscapeImagePhone: landscapeImagePhone, style: style, target: target, action: listenerAction) 51 | objc_setAssociatedObject(self, &associatedObjectKey, target, .OBJC_ASSOCIATION_RETAIN) 52 | 53 | if let actionBlock = actionBlock { 54 | self.glue.connector.connect(target.signal, to: actionBlock) 55 | } 56 | } 57 | 58 | public convenience init(title: String?, style: UIBarButtonItemStyle, actionBlock: (() -> ())? = nil) { 59 | let target = TargetActionListener() 60 | self.init(title: title, style: style, target: target, action: listenerAction) 61 | objc_setAssociatedObject(self, &associatedObjectKey, target, .OBJC_ASSOCIATION_RETAIN) 62 | 63 | if let actionBlock = actionBlock { 64 | self.glue.connector.connect(target.signal, to: actionBlock) 65 | } 66 | } 67 | } 68 | 69 | private class TargetActionListener: NSObject { 70 | let signal = Signal() 71 | 72 | @objc func actionDidFire() { 73 | signal.send() 74 | } 75 | } 76 | #endif 77 | -------------------------------------------------------------------------------- /Sources/UIControl Glue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIControl Glue.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-03-11. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | #if os(iOS) 10 | import UIKit 11 | import SipHash 12 | 13 | private var associatedObjectKey: Int8 = 0 14 | 15 | extension UIControl { 16 | open override var glue: GlueForUIControl { 17 | return _glue() 18 | } 19 | } 20 | 21 | open class GlueForUIControl: GlueForNSObject { 22 | private struct ControlEventsTargetKey: SipHashable { 23 | let sink: AnySink 24 | let events: UIControlEvents 25 | 26 | func appendHashes(to hasher: inout SipHasher) { 27 | hasher.append(sink) 28 | hasher.append(events.rawValue) 29 | } 30 | 31 | static func ==(left: ControlEventsTargetKey, right: ControlEventsTargetKey) -> Bool { 32 | return left.sink == right.sink && left.events == right.events 33 | } 34 | } 35 | 36 | private final class ControlEventsTarget: NSObject { 37 | let sink: AnySink 38 | 39 | init(sink: AnySink) { 40 | self.sink = sink 41 | } 42 | 43 | @objc func eventDidTrigger(_ sender: AnyObject, forEvent event: UIEvent) { 44 | sink.receive(event) 45 | } 46 | } 47 | 48 | public struct ControlEventsSource: SourceType { 49 | public typealias Value = UIEvent 50 | 51 | public let control: UIControl 52 | public let events: UIControlEvents 53 | 54 | public func add(_ sink: Sink) where Sink.Value == Value { 55 | let target = control.glue.add(sink.anySink, for: events) 56 | control.addTarget(target, action: #selector(ControlEventsTarget.eventDidTrigger(_:forEvent:)), for: events) 57 | } 58 | 59 | public func remove(_ sink: Sink) -> Sink where Sink.Value == Value { 60 | let target = control.glue.remove(sink.anySink, for: events) 61 | control.removeTarget(target, action: #selector(ControlEventsTarget.eventDidTrigger(_:forEvent:)), for: events) 62 | return target.sink.opened()! 63 | } 64 | } 65 | 66 | private var object: UIControl { return owner as! UIControl } 67 | 68 | private var targets: [ControlEventsTargetKey: ControlEventsTarget] = [:] 69 | 70 | public func source(for events: UIControlEvents = .primaryActionTriggered) -> ControlEventsSource { 71 | return ControlEventsSource(control: object, events: events) 72 | } 73 | 74 | private func add(_ sink: AnySink, for events: UIControlEvents) -> ControlEventsTarget { 75 | let target = ControlEventsTarget(sink: sink) 76 | let key = ControlEventsTargetKey(sink: sink, events: events) 77 | precondition(targets[key] == nil) 78 | targets[key] = target 79 | return target 80 | } 81 | 82 | private func remove(_ sink: AnySink, for events: UIControlEvents) -> ControlEventsTarget { 83 | let key = ControlEventsTargetKey(sink: sink, events: events) 84 | let target = targets[key]! 85 | targets[key] = nil 86 | return target 87 | } 88 | } 89 | #endif 90 | -------------------------------------------------------------------------------- /Sources/UIGestureRecognizer Glue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIGestureRecognizer Glue.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-03-16. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | #if os(iOS) 10 | import UIKit 11 | 12 | extension UIGestureRecognizer { 13 | open override var glue: GlueForUIGestureRecognizer { 14 | return _glue() 15 | } 16 | } 17 | 18 | open class GlueForUIGestureRecognizer: GlueForNSObject { 19 | private var object: UIGestureRecognizer { return owner as! UIGestureRecognizer } 20 | 21 | public lazy var state: AnyObservableValue 22 | = ObservableGestureRecognizerState(self.object).anyObservableValue 23 | } 24 | 25 | private class ObservableGestureRecognizerState: _BaseObservableValue { 26 | private unowned let _gestureRecognizer: UIGestureRecognizer 27 | private var _value: UIGestureRecognizerState? = nil 28 | 29 | init(_ gestureRecognizer: UIGestureRecognizer) { 30 | _gestureRecognizer = gestureRecognizer 31 | } 32 | 33 | override var value: UIGestureRecognizerState { 34 | return _gestureRecognizer.state 35 | } 36 | 37 | override func activate() { 38 | _value = _gestureRecognizer.state 39 | _gestureRecognizer.addTarget(self, action: #selector(ObservableGestureRecognizerState.gestureRecognizerDidFire)) 40 | } 41 | 42 | override func deactivate() { 43 | _gestureRecognizer.removeTarget(self, action: #selector(ObservableGestureRecognizerState.gestureRecognizerDidFire)) 44 | _value = nil 45 | } 46 | 47 | @objc func gestureRecognizerDidFire() { 48 | beginTransaction() 49 | let old = _value! 50 | _value = _gestureRecognizer.state 51 | sendChange(ValueChange(from: old, to: _value!)) 52 | endTransaction() 53 | } 54 | } 55 | #endif 56 | -------------------------------------------------------------------------------- /Sources/UILabel Glue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabel Glue.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2017-04-23. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | #if os(iOS) 10 | import UIKit 11 | 12 | extension UILabel { 13 | open override var glue: GlueForUILabel { 14 | return _glue() 15 | } 16 | } 17 | 18 | open class GlueForUILabel: GlueForNSObject { 19 | private var object: UILabel { return owner as! UILabel } 20 | 21 | public lazy var text: DependentValue = DependentValue { [unowned self] in self.object.text = $0 } 22 | public lazy var textColor: DependentValue = DependentValue { [unowned self] in self.object.textColor = $0 } 23 | public lazy var font: DependentValue = DependentValue { [unowned self] in self.object.font = $0 } 24 | public lazy var textAlignment: DependentValue = DependentValue { [unowned self] in self.object.textAlignment = $0 } 25 | public lazy var lineBreakMode: DependentValue = DependentValue { [unowned self] in self.object.lineBreakMode = $0 } 26 | } 27 | #endif 28 | -------------------------------------------------------------------------------- /Sources/UISearchBar Glue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UISearchBar Glue.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2017-05-08. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | #if os(iOS) 10 | import UIKit 11 | 12 | extension UISearchBar { 13 | open override var glue: GlueForUISearchBar { 14 | return _glue() 15 | } 16 | } 17 | 18 | open class GlueForUISearchBar: GlueForNSObject, UISearchBarDelegate { 19 | private var object: UISearchBar { return owner as! UISearchBar } 20 | 21 | public lazy var text: ComputedUpdatable 22 | = ComputedUpdatable( 23 | getter: { [unowned self] in self.object.text }, 24 | setter: { [unowned self] in self.object.text = $0 }) 25 | 26 | private lazy var _isEditing = Variable(false) 27 | public var isEditing: AnyObservableValue { return _isEditing.anyObservableValue } 28 | 29 | public required init(owner: NSObject) { 30 | super.init(owner: owner) 31 | object.delegate = self 32 | } 33 | 34 | public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { 35 | text.refresh() 36 | } 37 | 38 | public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { 39 | _isEditing.value = true 40 | } 41 | 42 | public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { 43 | _isEditing.value = false 44 | } 45 | 46 | public func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool { 47 | return true 48 | } 49 | public func searchBarShouldEndEditing(_ searchBar: UISearchBar) -> Bool { 50 | return true 51 | } 52 | } 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /Sources/UISwitch Glue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UISwitch Glue.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-11-12. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | #if os(iOS) 10 | import UIKit 11 | 12 | extension UISwitch { 13 | open override var glue: GlueForUISwitch { 14 | return _glue() 15 | } 16 | } 17 | 18 | open class GlueForUISwitch: GlueForUIControl { 19 | private var object: UISwitch { return owner as! UISwitch } 20 | 21 | public lazy var isOn: ComputedUpdatable 22 | = ComputedUpdatable(getter: { [unowned self] in self.object.isOn }, 23 | setter: { [unowned self] in self.object.isOn = $0 }, 24 | refreshSource: self.source(for: .valueChanged).mapToVoid()) 25 | } 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /Sources/Update.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Update.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-26. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | /// Updates are events that describe a change that is happening to an observable. 10 | /// Observables only change inside transactions. A transaction consists three phases, represented 11 | /// by the three cases of this enum type: 12 | /// 13 | /// - `beginTransaction` signals the start of a new transaction. 14 | /// - `change` describes a (partial) change to the value of the observable. 15 | /// Each transaction may include any number of such changes. 16 | /// - `endTransaction` closes the transaction. 17 | /// 18 | /// While a transaction is in progress, the value of an observable includes all changes that have already been 19 | /// reported in updates. 20 | /// 21 | /// Note that is perfectly legal for a transaction to include no actual changes. 22 | public enum Update { 23 | public typealias Value = Change.Value 24 | 25 | /// Hang on, I feel a change coming up. 26 | case beginTransaction 27 | /// Here is one change, but I think there might be more coming. 28 | case change(Change) 29 | /// OK, I'm done changing. 30 | case endTransaction 31 | } 32 | 33 | extension Update { 34 | public var change: Change? { 35 | if case let .change(change) = self { return change } 36 | return nil 37 | } 38 | 39 | public func filter(_ test: (Change) -> Bool) -> Update? { 40 | switch self { 41 | case .beginTransaction, .endTransaction: 42 | return self 43 | case .change(let change): 44 | if test(change) { 45 | return self 46 | } 47 | return nil 48 | } 49 | } 50 | 51 | public func map(_ transform: (Change) -> Result) -> Update { 52 | switch self { 53 | case .beginTransaction: 54 | return .beginTransaction 55 | case .change(let change): 56 | return .change(transform(change)) 57 | case .endTransaction: 58 | return .endTransaction 59 | } 60 | } 61 | 62 | public func flatMap(_ transform: (Change) -> Result?) -> Update? { 63 | switch self { 64 | case .beginTransaction: 65 | return .beginTransaction 66 | case .change(let change): 67 | guard let new = transform(change) else { return nil } 68 | return .change(new) 69 | case .endTransaction: 70 | return .endTransaction 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Sources/ValueChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueChange.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-04. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | /// A simple change description that includes a snapshot of the value before and after the change. 10 | public struct ValueChange: ChangeType { 11 | public var old: Value 12 | public var new: Value 13 | 14 | public init(from old: Value, to new: Value) { 15 | self.old = old 16 | self.new = new 17 | } 18 | 19 | public var isEmpty: Bool { 20 | // There is no way to compare old and new at this level. 21 | return false 22 | } 23 | 24 | public func apply(on value: inout Value) { 25 | value = new 26 | } 27 | 28 | public func applied(on value: Value) -> Value { 29 | return new 30 | } 31 | 32 | public mutating func merge(with next: ValueChange) { 33 | self.new = next.new 34 | } 35 | 36 | public func merged(with next: ValueChange) -> ValueChange { 37 | return .init(from: old, to: next.new) 38 | } 39 | 40 | public func reversed() -> ValueChange { 41 | return .init(from: new, to: old) 42 | } 43 | 44 | public func map(_ transform: (Value) -> R) -> ValueChange { 45 | return .init(from: transform(old), to: transform(new)) 46 | } 47 | } 48 | 49 | extension ValueChange: CustomStringConvertible { 50 | public var description: String { 51 | return "\(old) -> \(new)" 52 | } 53 | } 54 | 55 | public func ==(a: ValueChange, b: ValueChange) -> Bool { 56 | return a.old == b.old && a.new == b.new 57 | } 58 | 59 | public func !=(a: ValueChange, b: ValueChange) -> Bool { 60 | return !(a == b) 61 | } 62 | -------------------------------------------------------------------------------- /Sources/ValueMappingForSourceField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueMappingForSourceField.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-09. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableValueType { 10 | /// Map is an operator that implements key path coding and observing. 11 | /// Given an observable parent and a key that selects a child component (a.k.a "field") of its value that is a source, 12 | /// `map` returns a new source that can be used subscribe to the field indirectly through the parent. 13 | /// 14 | /// - Parameter key: An accessor function that returns a component of self (a field) that is a SourceType. 15 | /// 16 | /// - Returns: A new source that sends the same values as the current source returned by key in the parent. 17 | public func map(_ key: @escaping (Value) -> Source) -> AnySource { 18 | return ValueMappingForSourceField(parent: self, key: key).anySource 19 | } 20 | } 21 | 22 | /// A source of values for a Source field. 23 | private final class ValueMappingForSourceField: SignalerSource { 24 | typealias Value = Field.Value 25 | 26 | private struct SourceFieldSink: UniqueOwnedSink { 27 | typealias Owner = ValueMappingForSourceField 28 | 29 | unowned let owner: Owner 30 | 31 | func receive(_ update: ValueUpdate) { 32 | owner.applyParentUpdate(update) 33 | } 34 | } 35 | 36 | let parent: Parent 37 | let key: (Parent.Value) -> Field 38 | 39 | private var _field: Field? = nil 40 | 41 | init(parent: Parent, key: @escaping (Parent.Value) -> Field) { 42 | self.parent = parent 43 | self.key = key 44 | } 45 | 46 | override func activate() { 47 | precondition(_field == nil) 48 | let field = key(parent.value) 49 | _field = field 50 | parent.add(SourceFieldSink(owner: self)) 51 | field.add(signal.asSink) 52 | } 53 | 54 | override func deactivate() { 55 | _field!.remove(signal.asSink) 56 | _field = nil 57 | parent.remove(SourceFieldSink(owner: self)) 58 | } 59 | 60 | func applyParentUpdate(_ update: ValueUpdate) { 61 | switch update { 62 | case .beginTransaction: 63 | break 64 | case .change(let change): 65 | let field = key(change.new) 66 | _field!.remove(signal.asSink) 67 | _field = field 68 | field.add(signal.asSink) 69 | case .endTransaction: 70 | break 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/ValueMappingForValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueMappingForValue.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-09. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | public extension ObservableValueType { 10 | /// Returns an observable that calculates `transform` on all current and future values of this observable. 11 | public func map(_ transform: @escaping (Value) -> Output) -> AnyObservableValue { 12 | return ValueMappingForValue(parent: self, transform: transform).anyObservableValue 13 | } 14 | } 15 | 16 | private final class ValueMappingForValue: _AbstractObservableValue { 17 | let parent: Parent 18 | let transform: (Parent.Value) -> Value 19 | let sinkTransform: SinkTransformFromMapping, ValueUpdate> 20 | 21 | init(parent: Parent, transform: @escaping (Parent.Value) -> Value) { 22 | self.parent = parent 23 | self.transform = transform 24 | self.sinkTransform = SinkTransformFromMapping { u in u.map { c in c.map(transform) } } 25 | } 26 | 27 | override var value: Value { 28 | return transform(parent.value) 29 | } 30 | 31 | override func add(_ sink: Sink) where Sink.Value == Update { 32 | parent.add(TransformedSink(sink: sink, transform: sinkTransform)) 33 | } 34 | 35 | @discardableResult 36 | override func remove(_ sink: Sink) -> Sink where Sink.Value == Update { 37 | return parent.remove(TransformedSink(sink: sink, transform: sinkTransform)).sink 38 | } 39 | } 40 | 41 | extension UpdatableValueType { 42 | public func map(_ transform: @escaping (Value) -> Output, inverse: @escaping (Output) -> Value) -> AnyUpdatableValue { 43 | return ValueMappingForUpdatableValue(parent: self, transform: transform, inverse: inverse).anyUpdatableValue 44 | } 45 | } 46 | 47 | private final class ValueMappingForUpdatableValue: _AbstractUpdatableValue { 48 | let parent: Parent 49 | let transform: (Parent.Value) -> Value 50 | let inverse: (Value) -> Parent.Value 51 | let sinkTransform: SinkTransformFromMapping, ValueUpdate> 52 | 53 | init(parent: Parent, transform: @escaping (Parent.Value) -> Value, inverse: @escaping (Value) -> Parent.Value) { 54 | self.parent = parent 55 | self.transform = transform 56 | self.inverse = inverse 57 | self.sinkTransform = SinkTransformFromMapping { u in u.map { c in c.map(transform) } } 58 | } 59 | 60 | override var value: Value { 61 | get { 62 | return transform(parent.value) 63 | } 64 | set { 65 | parent.value = inverse(newValue) 66 | } 67 | } 68 | 69 | override func apply(_ update: Update>) { 70 | parent.apply(update.map { change in change.map(inverse) }) 71 | } 72 | 73 | override func add(_ sink: Sink) where Sink.Value == Update { 74 | parent.add(TransformedSink(sink: sink, transform: sinkTransform)) 75 | } 76 | 77 | @discardableResult 78 | override func remove(_ sink: Sink) -> Sink where Sink.Value == Update { 79 | return parent.remove(TransformedSink(sink: sink, transform: sinkTransform)).sink 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/ValueReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueReference.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-11-02. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | extension ObservableValueType where Value: ObservableValueType { 10 | public func unpacked() -> AnyObservableValue { 11 | return UnpackedObservableValueReference(self).anyObservableValue 12 | } 13 | } 14 | 15 | private final class UnpackedObservableValueReference: _BaseObservableValue 16 | where Reference.Value: ObservableValueType { 17 | typealias Target = Reference.Value 18 | typealias Value = Target.Value 19 | typealias Change = ValueChange 20 | 21 | private struct ReferenceSink: UniqueOwnedSink { 22 | typealias Owner = UnpackedObservableValueReference 23 | 24 | unowned(unsafe) let owner: Owner 25 | 26 | func receive(_ update: ValueUpdate) { 27 | owner.applyReferenceUpdate(update) 28 | } 29 | } 30 | 31 | private struct TargetSink: UniqueOwnedSink { 32 | typealias Owner = UnpackedObservableValueReference 33 | 34 | unowned(unsafe) let owner: Owner 35 | 36 | func receive(_ update: ValueUpdate) { 37 | owner.applyTargetUpdate(update) 38 | } 39 | } 40 | 41 | private var _reference: Reference 42 | private var _target: Reference.Value? = nil // Retained to make sure we keep it alive 43 | 44 | init(_ reference: Reference) { 45 | _reference = reference 46 | super.init() 47 | } 48 | 49 | override func activate() { 50 | _reference.updates.add(ReferenceSink(owner: self)) 51 | let target = _reference.value 52 | _target = target 53 | target.updates.add(TargetSink(owner: self)) 54 | } 55 | 56 | override func deactivate() { 57 | _target!.updates.remove(TargetSink(owner: self)) 58 | _reference.updates.remove(ReferenceSink(owner: self)) 59 | } 60 | 61 | func applyReferenceUpdate(_ update: ValueUpdate) { 62 | switch update { 63 | case .beginTransaction: 64 | beginTransaction() 65 | case .change(let change): 66 | if isConnected { 67 | _target!.remove(TargetSink(owner: self)) 68 | _target = change.new 69 | _target!.add(TargetSink(owner: self)) 70 | sendChange(ValueChange(from: change.old.value, to: change.new.value)) 71 | } 72 | case .endTransaction: 73 | endTransaction() 74 | } 75 | } 76 | 77 | func applyTargetUpdate(_ update: ValueUpdate) { 78 | switch update { 79 | case .beginTransaction: 80 | beginTransaction() 81 | case .change(let change): 82 | sendChange(change) 83 | case .endTransaction: 84 | endTransaction() 85 | } 86 | } 87 | 88 | override var value: Value { return _reference.value.value } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/AnySinkTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnySinkTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-25. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | private class TestSink: SinkType { 13 | typealias Value = Int 14 | 15 | func receive(_ value: Value) { 16 | // Noop 17 | } 18 | } 19 | 20 | class AnySinkTests: XCTestCase { 21 | func test_equality() { 22 | let sink1 = MockSink() 23 | let sink2 = MockSink() 24 | let sink3 = TestSink() 25 | let sink4 = TestSink() 26 | 27 | XCTAssertEqual(sink1, sink1) 28 | XCTAssertNotEqual(sink1, sink2) 29 | XCTAssertNotEqual(sink3, sink4) 30 | 31 | XCTAssertEqual(sink1.anySink, sink1.anySink) 32 | XCTAssertNotEqual(sink1.anySink, sink2.anySink) 33 | XCTAssertNotEqual(sink1.anySink, sink3.anySink) 34 | 35 | XCTAssertEqual(sink1.anySink, sink1.anySink.anySink) 36 | } 37 | 38 | func test_hashValue() { 39 | let sink = MockSink() 40 | 41 | XCTAssertEqual(sink.hashValue, sink.anySink.hashValue) 42 | } 43 | 44 | func test_receive() { 45 | let sink = MockSink() 46 | 47 | sink.expecting(1) { 48 | sink.receive(1) 49 | } 50 | 51 | sink.expecting(2) { 52 | sink.anySink.receive(2) 53 | } 54 | 55 | sink.expecting(3) { 56 | sink.anySink.anySink.receive(3) 57 | } 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/AnySourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnySourceTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-25. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | private class ForwardingSource: SourceType { 13 | typealias Value = Source.Value 14 | 15 | let target: Source 16 | 17 | init(_ target: Source) { 18 | self.target = target 19 | } 20 | 21 | func add(_ sink: Sink) where Sink.Value == Value { 22 | target.add(sink) 23 | } 24 | 25 | func remove(_ sink: Sink) -> Sink where Sink.Value == Value { 26 | return target.remove(sink) 27 | } 28 | } 29 | 30 | class AnySourceTests: XCTestCase { 31 | func test_Works() { 32 | let signal = Signal() 33 | let source = signal.anySource 34 | 35 | let sink = MockSink() 36 | 37 | source.add(sink) 38 | 39 | sink.expecting(1) { 40 | signal.send(1) 41 | } 42 | 43 | source.remove(sink) 44 | 45 | sink.expectingNothing { 46 | signal.send(2) 47 | } 48 | } 49 | 50 | func test_Idempotent() { 51 | let signal = Signal() 52 | let source = signal.anySource.anySource 53 | 54 | let sink = MockSink() 55 | 56 | source.add(sink) 57 | 58 | sink.expecting(1) { 59 | signal.send(1) 60 | } 61 | 62 | source.remove(sink) 63 | 64 | sink.expectingNothing { 65 | signal.send(2) 66 | } 67 | } 68 | 69 | func test_Custom() { 70 | let signal = Signal() 71 | let source = ForwardingSource(signal).anySource 72 | 73 | let sink = MockSink() 74 | 75 | source.add(sink) 76 | 77 | sink.expecting(1) { 78 | signal.send(1) 79 | } 80 | 81 | source.remove(sink) 82 | 83 | sink.expectingNothing { 84 | signal.send(2) 85 | } 86 | } 87 | 88 | 89 | func test_RetainsOriginal() { 90 | weak var signal: Signal? = nil 91 | var source: AnySource? = nil 92 | 93 | do { 94 | let s = Signal() 95 | signal = s 96 | source = s.anySource 97 | withExtendedLifetime(s) {} 98 | } 99 | 100 | XCTAssertNotNil(signal) 101 | XCTAssertNotNil(source) 102 | source = nil 103 | XCTAssertNil(signal) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/ArrayBufferingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayBufferingTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-11-02. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | @testable import GlueKit 11 | 12 | class ArrayBufferingTests: XCTestCase { 13 | func test_connectsImmediately() { 14 | let observable = TestObservableArray([1, 2, 3]) 15 | 16 | do { 17 | let buffered = observable.buffered() 18 | XCTAssertTrue(observable.isConnected) 19 | withExtendedLifetime(buffered) {} 20 | } 21 | XCTAssertFalse(observable.isConnected) 22 | } 23 | 24 | func test_properties() { 25 | let observable = TestObservableArray([1, 2, 3]) 26 | let buffered = observable.buffered() 27 | 28 | for b in [buffered, buffered.buffered()] { 29 | XCTAssertEqual(b.isBuffered, true) 30 | XCTAssertEqual(b[0], 1) 31 | XCTAssertEqual(b[1], 2) 32 | XCTAssertEqual(b[2], 3) 33 | XCTAssertEqual(b[0 ..< 2], [1, 2]) 34 | XCTAssertEqual(b.value, [1, 2, 3]) 35 | XCTAssertEqual(b.count, 3) 36 | } 37 | 38 | observable.apply(ArrayChange(initialCount: 3, modification: .replace(2, at: 1, with: 4))) 39 | XCTAssertEqual(buffered.value, [1, 4, 3]) 40 | } 41 | 42 | func test_updates() { 43 | let observable = TestObservableArray([1, 2, 3]) 44 | let buffered = observable.buffered() 45 | 46 | let sink = MockArrayObserver(buffered) 47 | 48 | sink.expecting(["begin", "3.remove(3, at: 2)", "end"]) { 49 | observable.apply(ArrayChange(initialCount: 3, modification: .remove(3, at: 2))) 50 | } 51 | XCTAssertEqual(buffered.value, [1, 2]) 52 | 53 | sink.expecting("begin") { 54 | observable.beginTransaction() 55 | } 56 | 57 | sink.expectingNothing { 58 | observable.apply(ArrayChange(initialCount: 2, modification: .replace(1, at: 0, with: 2))) 59 | } 60 | XCTAssertEqual(buffered.value, [1, 2]) 61 | 62 | sink.expectingNothing { 63 | observable.apply(ArrayChange(initialCount: 2, modification: .insert(6, at: 2))) 64 | } 65 | XCTAssertEqual(buffered.value, [1, 2]) 66 | 67 | sink.expecting(["2.replace(1, at: 0, with: 2).insert(6, at: 2)", "end"]) { 68 | observable.endTransaction() 69 | } 70 | XCTAssertEqual(buffered.value, [2, 2, 6]) 71 | 72 | sink.disconnect() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/ArrayConcatenationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayConcatenationTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-10. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class ArrayConcatenationTests: XCTestCase { 13 | func testConcatenation() { 14 | func check(a: [Int], b: [Int], c: O, file: StaticString = #file, line: UInt = #line) where O.Element == Int { 15 | XCTAssertEqual(c.count, a.count + b.count, file: file, line: line) 16 | let v = a + b 17 | XCTAssertEqual(c.value, v, file: file, line: line) 18 | for i in 0 ..< v.count { 19 | XCTAssertEqual(c[i], v[i], file: file, line: line) 20 | for j in i ..< v.count { 21 | XCTAssertEqual(c[i ..< j], v[i ..< j], file: file, line: line) 22 | } 23 | } 24 | } 25 | 26 | let a: ArrayVariable = [0, 1, 2] 27 | let b: ArrayVariable = [10, 20] 28 | 29 | let c = a + b 30 | 31 | XCTAssertFalse(c.isBuffered) 32 | XCTAssertEqual(c.count, 5) 33 | XCTAssertEqual(c.value, [0, 1, 2, 10, 20]) 34 | XCTAssertEqual(c[0], 0) 35 | XCTAssertEqual(c[1], 1) 36 | XCTAssertEqual(c[2], 2) 37 | XCTAssertEqual(c[3], 10) 38 | XCTAssertEqual(c[4], 20) 39 | check(a: a.value, b: b.value, c: c) 40 | 41 | let mock = MockArrayObserver(c) 42 | 43 | mock.expecting(["begin", "5.insert(30, at: 5)", "end"]) { 44 | b.append(30) 45 | } 46 | check(a: a.value, b: b.value, c: c) 47 | 48 | mock.expecting(["begin", "6.insert(3, at: 3)", "end"]) { 49 | a.append(3) 50 | } 51 | check(a: a.value, b: b.value, c: c) 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/ArrayFoldingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayFoldingTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-09. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class ArrayFoldingTests: XCTestCase { 13 | 14 | func testSum() { 15 | let array = ArrayVariable([1, 2, 3]) 16 | let sum = array.sum() 17 | 18 | XCTAssertEqual(sum.value, 6) 19 | 20 | array.append(4) 21 | XCTAssertEqual(sum.value, 10) 22 | 23 | array.insert(5, at: 1) 24 | XCTAssertEqual(sum.value, 15) 25 | 26 | array.remove(at: 0) 27 | XCTAssertEqual(sum.value, 14) 28 | 29 | array.removeAll() 30 | XCTAssertEqual(sum.value, 0) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/ArrayReferenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayReferenceTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-11-02. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class ArrayReferenceTests: XCTestCase { 13 | func testReference() { 14 | let a: ArrayVariable = [1, 2, 3] 15 | let b: ArrayVariable = [10, 20] 16 | let c: ArrayVariable = [] 17 | let ref = Variable>(a) 18 | 19 | XCTAssertEqual(ref.value.value, [1, 2, 3]) 20 | a[1] = 4 21 | XCTAssertEqual(ref.value.value, [1, 4, 3]) 22 | 23 | let unpacked = ref.unpacked() 24 | 25 | XCTAssertEqual(unpacked.isBuffered, false) 26 | XCTAssertEqual(unpacked.count, 3) 27 | XCTAssertEqual(unpacked[0], 1) 28 | XCTAssertEqual(Array(unpacked[0 ..< 3]), [1, 4, 3]) 29 | XCTAssertEqual(unpacked.value, [1, 4, 3]) 30 | a[0] = 2 31 | XCTAssertEqual(unpacked.value, [2, 4, 3]) 32 | 33 | let sink = MockArrayObserver(unpacked) 34 | 35 | sink.expecting(["begin", "3.replace(3, at: 2, with: 6)", "end"]) { 36 | a[2] = 6 37 | } 38 | 39 | sink.expecting(["begin", "3.replaceSlice([2, 4, 6], at: 0, with: [10, 20])", "end"]) { 40 | ref.value = b 41 | } 42 | 43 | sink.expecting("begin") { 44 | b.apply(.beginTransaction) 45 | } 46 | 47 | sink.expectingNothing { 48 | ref.apply(.beginTransaction) 49 | } 50 | 51 | sink.expecting("2.insert(15, at: 1)") { 52 | b.insert(15, at: 1) 53 | } 54 | 55 | sink.expecting("3.replaceSlice([10, 15, 20], at: 0, with: [])") { 56 | ref.value = c 57 | } 58 | 59 | sink.expecting("0.insert(100, at: 0)") { 60 | c.append(100) 61 | } 62 | 63 | sink.expectingNothing { 64 | b.apply(.endTransaction) 65 | } 66 | 67 | sink.expecting("end") { 68 | ref.apply(.endTransaction) 69 | } 70 | 71 | sink.disconnect() 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/BracketingSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BracketingSourceTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-26. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class BracketingSourceTests: XCTestCase { 13 | func testHello() { 14 | let source = Signal() 15 | 16 | var helloCount = 0 17 | let bracket = source.bracketed(hello: { helloCount += 1; return 0 }, 18 | goodbye: { return nil }) 19 | 20 | 21 | XCTAssertFalse(source.isConnected) 22 | 23 | let s1 = MockSink() 24 | 25 | s1.expecting(0) { 26 | bracket.add(s1) 27 | } 28 | XCTAssertTrue(source.isConnected) 29 | XCTAssertEqual(helloCount, 1) 30 | 31 | s1.expecting(1) { 32 | source.send(1) 33 | } 34 | 35 | let s2 = MockSink() 36 | s2.expecting(0) { 37 | bracket.add(s2) 38 | } 39 | XCTAssertEqual(helloCount, 2) 40 | 41 | s1.expecting(2) { 42 | s2.expecting(2) { 43 | source.send(2) 44 | } 45 | } 46 | 47 | s1.expectingNothing { 48 | _ = bracket.remove(s1) 49 | } 50 | 51 | s2.expecting(3) { 52 | source.send(3) 53 | } 54 | 55 | s2.expectingNothing { 56 | _ = bracket.remove(s2) 57 | } 58 | 59 | XCTAssertFalse(source.isConnected) 60 | XCTAssertEqual(helloCount, 2) 61 | } 62 | 63 | func testGoodbye() { 64 | let source = Signal() 65 | 66 | var goodbyeCount = 0 67 | let bracket = source.bracketed(hello: { return nil }, 68 | goodbye: { goodbyeCount += 1; return 0 }) 69 | 70 | 71 | XCTAssertFalse(source.isConnected) 72 | 73 | let s1 = MockSink() 74 | 75 | s1.expectingNothing { 76 | bracket.add(s1) 77 | } 78 | XCTAssertTrue(source.isConnected) 79 | XCTAssertEqual(goodbyeCount, 0) 80 | 81 | s1.expecting(1) { 82 | source.send(1) 83 | } 84 | 85 | let s2 = MockSink() 86 | s2.expectingNothing { 87 | bracket.add(s2) 88 | } 89 | XCTAssertEqual(goodbyeCount, 0) 90 | 91 | s1.expecting(2) { 92 | s2.expecting(2) { 93 | source.send(2) 94 | } 95 | } 96 | 97 | s1.expecting(0) { 98 | _ = bracket.remove(s1) 99 | } 100 | XCTAssertEqual(goodbyeCount, 1) 101 | 102 | s2.expecting(3) { 103 | source.send(3) 104 | } 105 | 106 | s2.expecting(0) { 107 | _ = bracket.remove(s2) 108 | } 109 | XCTAssertFalse(source.isConnected) 110 | XCTAssertEqual(goodbyeCount, 2) 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/BufferedSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BufferedSourceTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-26. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | private class TestSource: SourceType { 13 | typealias Value = Int 14 | 15 | var added = 0 16 | var removed = 0 17 | var sinks: Set> = [] 18 | 19 | init() {} 20 | 21 | func add(_ sink: Sink) where Sink.Value == Int { 22 | added += 1 23 | let (inserted, _) = sinks.insert(sink.anySink) 24 | precondition(inserted) 25 | } 26 | 27 | @discardableResult 28 | func remove(_ sink: Sink) -> Sink where Sink.Value == Int { 29 | removed += 1 30 | let old = sinks.remove(sink.anySink)! 31 | return old.opened()! 32 | } 33 | 34 | func send(_ value: Int) { 35 | for sink in sinks { 36 | if sinks.contains(sink) { 37 | sink.receive(value) 38 | } 39 | } 40 | } 41 | } 42 | 43 | class BufferedSourceTests: XCTestCase { 44 | func testRetainsInput() { 45 | weak var weakSource: TestSource? = nil 46 | var buffered: AnySource? = nil 47 | do { 48 | let source = TestSource() 49 | weakSource = source 50 | buffered = source.buffered() 51 | } 52 | 53 | XCTAssertNotNil(weakSource) 54 | XCTAssertNotNil(buffered) 55 | 56 | buffered = nil 57 | 58 | XCTAssertNil(weakSource) 59 | } 60 | 61 | func testSubscribesOnceWhileActive() { 62 | let source = TestSource() 63 | let buffered = source.buffered() 64 | 65 | XCTAssertEqual(source.added, 0) 66 | XCTAssertEqual(source.removed, 0) 67 | 68 | let sink = MockSink() 69 | buffered.add(sink) 70 | 71 | XCTAssertEqual(source.added, 1) 72 | XCTAssertEqual(source.removed, 0) 73 | 74 | let sink2 = MockSink() 75 | buffered.add(sink2) 76 | 77 | XCTAssertEqual(source.added, 1) 78 | XCTAssertEqual(source.removed, 0) 79 | 80 | buffered.remove(sink) 81 | 82 | XCTAssertEqual(source.added, 1) 83 | XCTAssertEqual(source.removed, 0) 84 | 85 | buffered.remove(sink2) 86 | 87 | XCTAssertEqual(source.added, 1) 88 | XCTAssertEqual(source.removed, 1) 89 | 90 | withExtendedLifetime(buffered) {} 91 | } 92 | 93 | func testReceivesValuesFromSource() { 94 | let source = TestSource() 95 | let buffered = source.buffered() 96 | 97 | let sink = MockSink() 98 | buffered.add(sink) 99 | 100 | sink.expecting(1) { 101 | source.send(1) 102 | } 103 | 104 | let sink2 = MockSink() 105 | buffered.add(sink2) 106 | 107 | sink.expecting(2) { 108 | sink2.expecting(2) { 109 | source.send(2) 110 | } 111 | } 112 | 113 | buffered.remove(sink) 114 | 115 | sink2.expecting(3) { 116 | source.send(3) 117 | } 118 | 119 | buffered.remove(sink2) 120 | 121 | sink.expectingNothing { 122 | sink2.expectingNothing { 123 | source.send(4) 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/ChangeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Abstract Observables.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-26. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class ChangeTests: XCTestCase { 13 | func testDefaultApplied() { 14 | let change = TestChange([1, 2]) 15 | XCTAssertEqual(change.applied(on: 1), 2) 16 | } 17 | 18 | func testDefaultMerged() { 19 | let change = TestChange([1, 2]) 20 | let next = TestChange([2, 3]) 21 | XCTAssertEqual(change.merged(with: next).values, [1, 2, 3]) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/ChangesSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChangesSourceTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-26. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | @testable import GlueKit 11 | 12 | class ChangesSourceTests: XCTestCase { 13 | func testRetainsObservable() { 14 | weak var weakObservable: TestObservable? = nil 15 | var changes: AnySource? = nil 16 | do { 17 | let observable = TestObservable(0) 18 | weakObservable = observable 19 | changes = observable.changes 20 | } 21 | XCTAssertNotNil(changes) 22 | XCTAssertNotNil(weakObservable) 23 | changes = nil 24 | XCTAssertNil(weakObservable) 25 | } 26 | 27 | func testSubscribingToChangesSubscribesToUpdates() { 28 | let observable = TestObservable(0) 29 | let changes = observable.changes 30 | 31 | let sink = MockSink() 32 | 33 | XCTAssertFalse(observable.isConnected) 34 | 35 | changes.add(sink) 36 | 37 | XCTAssertTrue(observable.isConnected) 38 | 39 | changes.remove(sink) 40 | 41 | XCTAssertFalse(observable.isConnected) 42 | } 43 | 44 | func testChangesSendsCompletedChanges() { 45 | let observable = TestObservable(0) 46 | let changes = observable.changes 47 | 48 | let sink = MockSink() 49 | 50 | changes.add(sink) 51 | 52 | sink.expectingNothing { 53 | observable.beginTransaction() 54 | observable.value = 1 55 | observable.value = 2 56 | } 57 | 58 | sink.expecting(TestChange([0, 1, 2])) { 59 | observable.endTransaction() 60 | } 61 | 62 | changes.remove(sink) 63 | } 64 | 65 | func testRemovingASinkDuringATransactionSendsPartialChanges() { 66 | let observable = TestObservable(0) 67 | let changes = observable.changes 68 | 69 | let sink = MockSink() 70 | changes.add(sink) 71 | observable.beginTransaction() 72 | observable.value = 1 73 | observable.value = 2 74 | sink.expecting(TestChange([0, 1, 2])) { 75 | _ = changes.remove(sink) 76 | } 77 | observable.value = 3 78 | observable.endTransaction() 79 | } 80 | 81 | func testDifferentSinksMayReceiveDifferentChanges() { 82 | let observable = TestObservable(0) 83 | let changes = observable.changes 84 | 85 | let sink1 = MockSink() 86 | changes.add(sink1) 87 | 88 | observable.beginTransaction() 89 | observable.value = 1 90 | 91 | let sink2 = MockSink() 92 | changes.add(sink2) 93 | 94 | observable.value = 2 95 | 96 | sink1.expecting(TestChange([0, 1, 2])) { 97 | _ = changes.remove(sink1) 98 | } 99 | 100 | observable.value = 3 101 | 102 | sink2.expecting(TestChange([1, 2, 3])) { 103 | observable.endTransaction() 104 | } 105 | 106 | changes.remove(sink2) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/ConnectorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectorTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-10. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | @testable import GlueKit 11 | 12 | class TestConnection: Connection { 13 | var callback: (() -> ())? 14 | 15 | init(_ callback: @escaping () -> ()) { 16 | self.callback = callback 17 | super.init() 18 | } 19 | 20 | deinit { 21 | disconnect() 22 | } 23 | 24 | override func disconnect() { 25 | guard let callback = self.callback else { return } 26 | self.callback = nil 27 | callback() 28 | } 29 | } 30 | class ConnectorTests: XCTestCase { 31 | 32 | func test_EmptyConnector() { 33 | let connector = Connector() 34 | connector.disconnect() 35 | } 36 | 37 | func test_ReleasingTheConnectorDisconnectsItsConnections() { 38 | var actual: [Int] = [] 39 | do { 40 | let connector = Connector() 41 | let c = TestConnection { actual.append(1) } 42 | c.putInto(connector) 43 | 44 | XCTAssertEqual(actual, []) 45 | } 46 | XCTAssertEqual(actual, [1]) 47 | } 48 | 49 | func test_DisconnectingTheConnectorDisconnectsItsConnections() { 50 | var actual: [Int] = [] 51 | 52 | do { 53 | let connector = Connector() 54 | let c = TestConnection { actual.append(1) } 55 | c.putInto(connector) 56 | XCTAssertEqual(actual, []) 57 | connector.disconnect() 58 | XCTAssertEqual(actual, [1]) 59 | withExtendedLifetime(connector) {} 60 | } 61 | XCTAssertEqual(actual, [1]) 62 | } 63 | 64 | func test_ConnectorsCanBeRestarted() { 65 | var actual: [Int] = [] 66 | 67 | do { 68 | let connector = Connector() 69 | let c1 = TestConnection { actual.append(1) } 70 | c1.putInto(connector) 71 | XCTAssertEqual(actual, []) 72 | connector.disconnect() 73 | XCTAssertEqual(actual, [1]) 74 | 75 | let c2 = TestConnection { actual.append(2) } 76 | c2.putInto(connector) 77 | XCTAssertEqual(actual, [1]) 78 | connector.disconnect() 79 | XCTAssertEqual(actual, [1, 2]) 80 | 81 | withExtendedLifetime(connector) {} 82 | } 83 | XCTAssertEqual(actual, [1, 2]) 84 | } 85 | 86 | func test_ConnectingASourceToAClosure() { 87 | let signal = Signal() 88 | let connector = Connector() 89 | 90 | var expected: [Int] = [] 91 | var actual: [Int] = [] 92 | connector.connect(signal) { value in actual.append(value) } 93 | 94 | XCTAssertEqual(actual, expected) 95 | 96 | expected.append(42) 97 | signal.send(42) 98 | XCTAssertEqual(actual, expected) 99 | 100 | connector.disconnect() 101 | signal.send(23) 102 | XCTAssertEqual(actual, expected) 103 | 104 | withExtendedLifetime(connector) {} 105 | } 106 | 107 | #if false // TODO Compiler crash in Xcode 8.3.2 108 | func test_ConnectingAnObservableToAChangeClosure() { 109 | let variable = Variable(0) 110 | let connector = Connector() 111 | 112 | var expected: [ValueChange] = [] 113 | var actual: [ValueChange] = [] 114 | 115 | connector.connect(variable) { change in actual.append(change) } 116 | 117 | XCTAssertTrue(actual.elementsEqual(expected, by: ==)) 118 | 119 | expected.append(.init(from: 0, to: 42)) 120 | variable.value = 42 121 | 122 | XCTAssertTrue(actual.elementsEqual(expected, by: ==)) 123 | 124 | connector.disconnect() 125 | variable.value = 23 126 | 127 | XCTAssertTrue(actual.elementsEqual(expected, by: ==)) 128 | 129 | withExtendedLifetime(connector) {} 130 | } 131 | #endif 132 | } 133 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/DispatchSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchSourceTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-11-02. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import GlueKit 12 | 13 | 14 | class DispatchSourceTests: XCTestCase { 15 | func testDispatchQueue() { 16 | let signal = Signal() 17 | 18 | let queue = DispatchQueue(label: "org.attaswift.GlueKit.test") 19 | let semaphore = DispatchSemaphore(value: 1) 20 | var r: [Int] = [] 21 | 22 | let connection = signal.dispatch(on: queue).subscribe { value in 23 | semaphore.wait() 24 | r.append(value) 25 | semaphore.signal() 26 | } 27 | 28 | semaphore.wait() 29 | signal.send(1) 30 | XCTAssertEqual(r, []) 31 | semaphore.signal() 32 | queue.sync { 33 | XCTAssertEqual(r, [1]) 34 | } 35 | 36 | connection.disconnect() 37 | } 38 | 39 | func testOperationQueue() { 40 | let signal = Signal() 41 | 42 | let queue = OperationQueue() 43 | queue.maxConcurrentOperationCount = 1 44 | let semaphore = DispatchSemaphore(value: 1) 45 | var r: [Int] = [] 46 | 47 | let connection = signal.dispatch(on: queue).subscribe { value in 48 | XCTAssertEqual(OperationQueue.current, queue) 49 | semaphore.wait() 50 | r.append(value) 51 | semaphore.signal() 52 | } 53 | 54 | semaphore.wait() 55 | signal.send(1) 56 | XCTAssertEqual(r, []) 57 | semaphore.signal() 58 | 59 | queue.waitUntilAllOperationsAreFinished() 60 | XCTAssertEqual(r, [1]) 61 | 62 | queue.addOperation { 63 | signal.send(2) 64 | } 65 | 66 | queue.waitUntilAllOperationsAreFinished() 67 | 68 | semaphore.wait() 69 | XCTAssertEqual(r, [1, 2]) 70 | semaphore.signal() 71 | 72 | connection.disconnect() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/DistinctUnionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DistinctUnionTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-04. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import GlueKit 12 | 13 | class DistinctUnionTests: XCTestCase { 14 | 15 | func test_getters() { 16 | let array: ArrayVariable = [0, 1, 2, 2, 3, 4, 5, 5, 5] 17 | let set = array.distinctUnion() 18 | 19 | XCTAssertTrue(set.isBuffered) 20 | XCTAssertEqual(set.count, 6) 21 | XCTAssertEqual(set.value, Set(0 ..< 6)) 22 | XCTAssertTrue(set.contains(0)) 23 | XCTAssertFalse(set.contains(7)) 24 | 25 | XCTAssertTrue(set.isSubset(of: Set(0 ..< 100))) 26 | XCTAssertTrue(set.isSubset(of: Set(0 ..< 6))) 27 | XCTAssertFalse(set.isSubset(of: Set(-5 ..< 5))) 28 | XCTAssertFalse(set.isSubset(of: Set(-5 ..< 0))) 29 | 30 | XCTAssertTrue(set.isSuperset(of: Set(0 ..< 4))) 31 | XCTAssertTrue(set.isSuperset(of: Set(0 ..< 6))) 32 | XCTAssertFalse(set.isSuperset(of: Set(-1 ..< 6))) 33 | XCTAssertFalse(set.isSuperset(of: Set(-1 ..< 3))) 34 | } 35 | 36 | func test_updates() { 37 | let array: ArrayVariable = [0] 38 | 39 | let set = array.distinctUnion() 40 | 41 | let mock = MockSetObserver(set) 42 | 43 | mock.expecting(["begin", "[]/[1]", "end"]) { 44 | array.append(1) 45 | } 46 | 47 | mock.expecting(["begin", "[]/[2]", "end"]) { 48 | array.append(2) 49 | } 50 | XCTAssertEqual(set.value, Set([0, 1, 2])) 51 | 52 | mock.expecting(["begin", "end"]) { 53 | array.append(1) 54 | } 55 | XCTAssertEqual(array.value, [0, 1, 2, 1]) 56 | XCTAssertEqual(set.value, Set([0, 1, 2])) 57 | 58 | mock.expecting(["begin", "end"]) { 59 | _ = array.remove(at: 3) 60 | } 61 | XCTAssertEqual(array.value, [0, 1, 2]) 62 | XCTAssertEqual(set.value, Set([0, 1, 2])) 63 | 64 | mock.expecting(["begin", "[1]/[]", "end"]) { 65 | _ = array.remove(at: 1) 66 | } 67 | XCTAssertEqual(array.value, [0, 2]) 68 | XCTAssertEqual(set.value, Set([0, 2])) 69 | 70 | mock.expecting(["begin", "[2]/[3]", "end"]) { 71 | array[1] = 3 72 | } 73 | XCTAssertEqual(array.value, [0, 3]) 74 | XCTAssertEqual(set.value, Set([0, 3])) 75 | 76 | mock.expecting(["begin", "[]/[4, 5, 6]", "end"]) { 77 | array.append(contentsOf: [4, 4, 4, 5, 5, 6]) 78 | } 79 | XCTAssertEqual(array.value, [0, 3, 4, 4, 4, 5, 5, 6]) 80 | XCTAssertEqual(set.value, Set([0, 3, 4, 5, 6])) 81 | 82 | mock.expecting(["begin", "[6]/[]", "[4]/[]", "[0]/[]", "end"]) { 83 | // Remove even values 84 | array.withTransaction { 85 | for i in (0 ..< array.count).reversed() { 86 | if array[i] & 1 == 0 { 87 | array.remove(at: i) 88 | } 89 | } 90 | } 91 | } 92 | XCTAssertEqual(array.value, [3, 5, 5]) 93 | XCTAssertEqual(set.value, Set([3, 5])) 94 | 95 | mock.expecting(["begin", "[3, 5]/[]", "end"]) { 96 | array.removeAll() 97 | } 98 | XCTAssertEqual(set.value, Set()) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/MergedSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MergedSourceTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-12-04. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class MergedSourceTests: XCTestCase { 13 | 14 | func testSimpleMerge() { 15 | let s1 = Signal() 16 | let s2 = Signal() 17 | 18 | let source = s1.merged(with: s2) 19 | 20 | let sink = MockSink() 21 | source.add(sink) 22 | 23 | sink.expecting(11) { s1.send(11) } 24 | sink.expecting(21) { s2.send(21) } 25 | sink.expecting(12) { s1.send(12) } 26 | sink.expecting(13) { s1.send(13) } 27 | sink.expecting(22) { s2.send(22) } 28 | sink.expecting(23) { s2.send(23) } 29 | 30 | source.remove(sink) 31 | } 32 | 33 | func testNaryMerge() { 34 | var signals: [Signal] = [] 35 | (0 ..< 10).forEach { _ in signals.append(Signal()) } 36 | 37 | let merge = Signal.merge(signals) 38 | 39 | let sink = MockSink() 40 | merge.add(sink) 41 | 42 | sink.expecting(Array(0 ..< 20)) { 43 | for i in 0 ..< 20 { 44 | signals[i % signals.count].send(i) 45 | } 46 | } 47 | merge.remove(sink) 48 | } 49 | 50 | func testReentrantMerge() { 51 | let s1 = Signal() 52 | let s2 = Signal() 53 | 54 | let source = s1.merged(with: s2) 55 | 56 | var s = "" 57 | let c = source.subscribe { i in 58 | s += " (\(i)" 59 | if i > 0 { 60 | s2.send(i - 1) 61 | } 62 | s += ")" 63 | } 64 | 65 | s1.send(3) 66 | 67 | XCTAssertEqual(s, " (3) (2) (1) (0)") 68 | c.disconnect() 69 | } 70 | 71 | func testVariadicMerge() { 72 | let s1 = Signal() 73 | let s2 = Signal() 74 | let s3 = Signal() 75 | let s4 = Signal() 76 | 77 | let merge = Signal.merge(s1, s2, s3, s4) 78 | 79 | let sink = MockSink() 80 | merge.add(sink) 81 | 82 | sink.expecting(1) { s1.send(1) } 83 | sink.expecting(2) { s1.send(2) } 84 | sink.expecting(3) { s1.send(3) } 85 | sink.expecting(4) { s1.send(4) } 86 | 87 | merge.remove(sink) 88 | } 89 | 90 | func testMergeChaining() { 91 | let s1 = Signal() 92 | let s2 = Signal() 93 | let s3 = Signal() 94 | let s4 = Signal() 95 | 96 | // This does not chain three merged sources together; it creates a single merged source containing all sources. 97 | let merge = s1.merged(with: s2).merged(with: s3).merged(with: s4) 98 | 99 | let sink = MockSink() 100 | merge.add(sink) 101 | 102 | sink.expecting(1) { s1.send(1) } 103 | sink.expecting(2) { s1.send(2) } 104 | sink.expecting(3) { s1.send(3) } 105 | sink.expecting(4) { s1.send(4) } 106 | 107 | merge.remove(sink) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/MockArrayObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockArrayObserver.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-06. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import GlueKit 12 | 13 | internal func describe(_ update: Update>?) -> String { 14 | guard let update = update else { return "nil" } 15 | switch update { 16 | case .beginTransaction: 17 | return "begin" 18 | case .change(let change): 19 | let mods = change.modifications.map { mod in "\(mod)" } 20 | return "\(change.initialCount)\(mods.joined())" 21 | case .endTransaction: 22 | return "end" 23 | } 24 | } 25 | 26 | class MockArrayObserver: MockSinkProtocol { 27 | typealias Change = ArrayChange 28 | 29 | let state: MockSinkState, String> 30 | 31 | init() { 32 | state = .init({ describe($0) }) 33 | } 34 | 35 | init(_ source: Source) where Source.Value == Update { 36 | state = .init({ describe($0) }) 37 | self.subscribe(to: source) 38 | } 39 | 40 | convenience init(_ observable: Observable) where Observable.Change == Change { 41 | self.init(observable.updates) 42 | } 43 | 44 | func receive(_ value: Update) { 45 | state.receive(value) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/MockSetObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockSetObserver.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-06. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import GlueKit 12 | 13 | func describe(_ update: SetUpdate) -> String { 14 | switch update { 15 | case .beginTransaction: 16 | return "begin" 17 | case .change(let change): 18 | let removed = change.removed.sorted().map { "\($0)" }.joined(separator: ", ") 19 | let inserted = change.inserted.sorted().map { "\($0)" }.joined(separator: ", ") 20 | return "[\(removed)]/[\(inserted)]" 21 | case .endTransaction: 22 | return "end" 23 | } 24 | } 25 | 26 | class MockSetObserver: MockSinkProtocol { 27 | typealias Change = SetChange 28 | 29 | let state: MockSinkState, String> 30 | 31 | init() { 32 | state = .init({ describe($0) }) 33 | } 34 | 35 | init(_ source: Source) where Source.Value == Update { 36 | state = .init({ describe($0) }) 37 | self.subscribe(to: source) 38 | } 39 | 40 | convenience init(_ observable: Observable) where Observable.Change == Change { 41 | self.init(observable.updates) 42 | } 43 | 44 | func receive(_ value: Update) { 45 | state.receive(value) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/MockUpdateSink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockUpdateSink.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-09. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | internal func describe(_ update: Update?) -> String { 13 | guard let update = update else { return "nil" } 14 | switch update { 15 | case .beginTransaction: return "begin" 16 | case .change(let change): return "\(change)" 17 | case .endTransaction: return "end" 18 | } 19 | } 20 | 21 | class MockUpdateSink: MockSinkProtocol { 22 | let state: MockSinkState, String> 23 | 24 | init() { 25 | state = .init({ describe($0) }) 26 | } 27 | 28 | init(_ source: Source) where Source.Value == Update { 29 | state = .init({ describe($0) }) 30 | self.subscribe(to: source) 31 | } 32 | 33 | convenience init(_ observable: Observable) where Observable.Change == Change { 34 | self.init(observable.updates) 35 | } 36 | 37 | func receive(_ value: Update) { 38 | state.receive(value) 39 | } 40 | } 41 | 42 | typealias MockValueUpdateSink = MockUpdateSink> 43 | 44 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/ObservableTypeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableTypeTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-28. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class ObservableTypeTests: XCTestCase { 13 | func test_UpdatableType_withTransaction() { 14 | let test = TestUpdatable(0) 15 | 16 | let sink = MockUpdateSink() 17 | test.updates.add(sink) 18 | 19 | sink.expecting(["begin", "end"]) { 20 | test.withTransaction {} 21 | } 22 | 23 | sink.expecting(["begin", "0 -> 1", "end"]) { 24 | test.withTransaction { 25 | test.apply(TestChange(from: 0, to: 1)) 26 | } 27 | } 28 | 29 | sink.expecting(["begin", "1 -> 2", "2 -> 3", "end"]) { 30 | test.withTransaction { 31 | test.apply(TestChange(from: 1, to: 2)) 32 | test.apply(TestChange(from: 2, to: 3)) 33 | } 34 | } 35 | 36 | sink.expecting(["begin", "3 -> 4", "end"]) { 37 | test.withTransaction { 38 | test.withTransaction { 39 | test.apply(TestChange(from: 3, to: 4)) 40 | } 41 | } 42 | } 43 | 44 | test.updates.remove(sink) 45 | } 46 | 47 | func test_UpdatableType_applyChange() { 48 | let test = TestUpdatable(0) 49 | 50 | let sink = MockUpdateSink() 51 | test.updates.add(sink) 52 | 53 | sink.expecting(["begin", "0 -> 1", "end"]) { 54 | test.apply(TestChange([0, 1])) 55 | } 56 | 57 | test.updates.remove(sink) 58 | } 59 | 60 | #if false // TODO Compiler crash in Xcode 8.3.2 61 | func test_Connector_connectObservableToUpdateSink() { 62 | let observable = TestObservable(0) 63 | 64 | let connector = Connector() 65 | 66 | var received: [Update] = [] 67 | connector.connect(observable) { update in received.append(update) } 68 | 69 | observable.value = 1 70 | 71 | XCTAssertEqual(received.map { "\($0)" }, ["beginTransaction", "change(0 -> 1)", "endTransaction"]) 72 | received = [] 73 | 74 | connector.disconnect() 75 | 76 | observable.value = 2 77 | 78 | XCTAssertEqual(received.map { "\($0)" }, []) 79 | } 80 | #endif 81 | 82 | #if false // TODO Compiler crash in Xcode 8.3.2 83 | func test_Connector_connectObservableToChangeSink() { 84 | let observable = TestObservable(0) 85 | 86 | let connector = Connector() 87 | 88 | var received: [TestChange] = [] 89 | connector.connect(observable) { change in received.append(change) } 90 | 91 | observable.value = 1 92 | 93 | XCTAssertEqual(received.map { "\($0)" }, ["0 -> 1"]) 94 | received = [] 95 | 96 | connector.disconnect() 97 | 98 | observable.value = 2 99 | 100 | XCTAssertEqual(received.map { "\($0)" }, []) 101 | } 102 | #endif 103 | } 104 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/SetBufferingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetBufferingTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-11-02. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | @testable import GlueKit 11 | 12 | class SetBufferingTests: XCTestCase { 13 | func test_connectsImmediately() { 14 | let observable = TestObservableSet([1, 2, 3]) 15 | 16 | do { 17 | let buffered = observable.buffered() 18 | XCTAssertTrue(observable.isConnected) 19 | withExtendedLifetime(buffered) {} 20 | } 21 | XCTAssertFalse(observable.isConnected) 22 | } 23 | 24 | func test_properties() { 25 | let observable = TestObservableSet([1, 2, 3]) 26 | let buffered = observable.buffered() 27 | 28 | for b in [buffered, buffered.buffered()] { 29 | XCTAssertEqual(b.isBuffered, true) 30 | XCTAssertEqual(b.count, 3) 31 | XCTAssertEqual(b.value, [1, 2, 3]) 32 | XCTAssertEqual(b.contains(0), false) 33 | XCTAssertEqual(b.contains(1), true) 34 | XCTAssertEqual(b.isSubset(of: [1, 2, 3, 4]), true) 35 | XCTAssertEqual(b.isSubset(of: [2, 3, 4, 5]), false) 36 | XCTAssertEqual(b.isSuperset(of: [1, 2]), true) 37 | XCTAssertEqual(b.isSuperset(of: [3, 4]), false) 38 | } 39 | 40 | observable.apply(SetChange(removed: [3], inserted: [0])) 41 | XCTAssertEqual(buffered.value, [0, 1, 2]) 42 | 43 | } 44 | 45 | func test_updates() { 46 | let observable = TestObservableSet([1, 2, 3]) 47 | let buffered = observable.buffered() 48 | 49 | let sink = MockSetObserver(buffered) 50 | 51 | sink.expecting(["begin", "[3]/[]", "end"]) { 52 | observable.apply(SetChange(removed: [3])) 53 | } 54 | XCTAssertEqual(buffered.value, [1, 2]) 55 | 56 | sink.expecting("begin") { 57 | observable.beginTransaction() 58 | } 59 | 60 | sink.expectingNothing { 61 | observable.apply(SetChange(removed: [2], inserted: [4])) 62 | } 63 | XCTAssertEqual(buffered.value, [1, 2]) 64 | 65 | sink.expectingNothing { 66 | observable.apply(SetChange(inserted: [9])) 67 | } 68 | XCTAssertEqual(buffered.value, [1, 2]) 69 | 70 | sink.expecting(["[2]/[4, 9]", "end"]) { 71 | observable.endTransaction() 72 | } 73 | XCTAssertEqual(buffered.value, [1, 4, 9]) 74 | 75 | sink.disconnect() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/SetFoldingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetFoldingTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-09. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class SetFoldingTests: XCTestCase { 13 | 14 | func testSum() { 15 | let set = SetVariable([1, 2, 3]) 16 | let sum = set.sum() 17 | 18 | XCTAssertEqual(sum.value, 6) 19 | 20 | set.insert(4) 21 | XCTAssertEqual(sum.value, 10) 22 | 23 | set.formUnion([5, 6]) 24 | XCTAssertEqual(sum.value, 21) 25 | 26 | set.remove(1) 27 | XCTAssertEqual(sum.value, 20) 28 | 29 | set.subtract([2, 4]) 30 | XCTAssertEqual(sum.value, 14) 31 | 32 | set.removeAll() 33 | XCTAssertEqual(sum.value, 0) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/SetReferenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetReferenceTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-11-02. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class SetReferenceTests: XCTestCase { 13 | func testReference() { 14 | let a: SetVariable = [1, 2, 3] 15 | let b: SetVariable = [10, 20] 16 | let c: SetVariable = [7] 17 | let ref = Variable>(a) 18 | 19 | XCTAssertEqual(ref.value.value, Set([1, 2, 3])) 20 | a.insert(4) 21 | XCTAssertEqual(ref.value.value, Set([1, 2, 3, 4])) 22 | 23 | let unpacked = ref.unpacked() 24 | 25 | XCTAssertEqual(unpacked.isBuffered, false) 26 | XCTAssertEqual(unpacked.count, 4) 27 | XCTAssertEqual(unpacked.value, Set([1, 2, 3, 4])) 28 | XCTAssertEqual(unpacked.contains(0), false) 29 | XCTAssertEqual(unpacked.contains(1), true) 30 | XCTAssertEqual(unpacked.isSubset(of: [1, 2, 3, 4, 5]), true) 31 | XCTAssertEqual(unpacked.isSubset(of: [2, 3, 4, 5, 6]), false) 32 | XCTAssertEqual(unpacked.isSuperset(of: [1, 2, 3]), true) 33 | XCTAssertEqual(unpacked.isSuperset(of: [3, 4, 5]), false) 34 | 35 | a.remove(2) 36 | XCTAssertEqual(unpacked.value, [1, 3, 4]) 37 | 38 | let sink = MockSetObserver(unpacked) 39 | 40 | sink.expecting(["begin", "[1]/[]", "end"]) { 41 | a.remove(1) 42 | } 43 | 44 | sink.expecting(["begin", "[3, 4]/[10, 20]", "end"]) { 45 | ref.value = b 46 | } 47 | 48 | sink.expecting("begin") { 49 | b.apply(.beginTransaction) 50 | } 51 | 52 | sink.expectingNothing { 53 | ref.apply(.beginTransaction) 54 | } 55 | 56 | sink.expecting("[]/[15]") { 57 | b.insert(15) 58 | } 59 | 60 | sink.expecting("[10, 15, 20]/[7]") { 61 | ref.value = c 62 | } 63 | 64 | sink.expecting("[]/[8]") { 65 | c.insert(8) 66 | } 67 | 68 | sink.expectingNothing { 69 | b.apply(.endTransaction) 70 | } 71 | 72 | sink.expecting("end") { 73 | ref.apply(.endTransaction) 74 | } 75 | 76 | sink.disconnect() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/SetVariableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetVariableTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-10. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | private class Foo: Hashable, Comparable, CustomStringConvertible { 13 | var i: Int 14 | 15 | init(_ i: Int) { self.i = i } 16 | 17 | var hashValue: Int { return i.hashValue } 18 | static func ==(a: Foo, b: Foo) -> Bool { return a.i == b.i } 19 | static func <(a: Foo, b: Foo) -> Bool { return a.i < b.i } 20 | var description: String { return "\(i)" } 21 | } 22 | 23 | class SetVariableTests: XCTestCase { 24 | 25 | func testInitialization() { 26 | let s1 = SetVariable() 27 | XCTAssertEqual(s1.value, []) 28 | 29 | let s2 = SetVariable([1, 2, 3]) 30 | XCTAssertEqual(s2.value, [1, 2, 3]) 31 | 32 | let s3 = SetVariable(Set([1, 2, 3])) 33 | XCTAssertEqual(s3.value, [1, 2, 3]) 34 | 35 | let s4 = SetVariable(1 ... 10) 36 | XCTAssertEqual(s4.value, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 37 | 38 | let s5 = SetVariable(elements: 1, 2, 3) 39 | XCTAssertEqual(s5.value, [1, 2, 3]) 40 | } 41 | 42 | func testUpdate() { 43 | let f1 = Foo(1) 44 | let f2 = Foo(2) 45 | let f3 = Foo(3) 46 | 47 | let set: SetVariable = [f1, f2, f3] 48 | let mock = MockSetObserver(set) 49 | 50 | let f2p = Foo(2) 51 | let a = mock.expecting(["begin", "[2]/[2]", "end"]) { set.update(with: f2p) } 52 | XCTAssertTrue(a === f2) 53 | let b = mock.expecting(["begin", "[2]/[2]", "end"]) { set.update(with: Foo(2)) } 54 | XCTAssertTrue(b === f2p) 55 | let c = mock.expecting(["begin", "[]/[4]", "end"]) { set.update(with: Foo(4)) } 56 | XCTAssertNil(c) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/SimpleSourcesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleSourcesTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-12-03. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class SimpleSourcesTests: XCTestCase { 13 | 14 | func testEmptySource() { 15 | let source = AnySource.empty() 16 | 17 | let sink = MockSink() 18 | source.add(sink) 19 | 20 | sink.expectingNothing { 21 | // Ah, uhm, not sure what to test here, really 22 | } 23 | 24 | source.remove(sink) 25 | } 26 | 27 | func testNeverSource() { 28 | let source = AnySource.never() 29 | 30 | let sink = MockSink() 31 | source.add(sink) 32 | 33 | sink.expectingNothing { 34 | // Ah, uhm, not sure what to test here, really 35 | } 36 | 37 | source.remove(sink) 38 | } 39 | 40 | 41 | func testJustSource() { 42 | let source = AnySource.just(42) 43 | 44 | let sink = MockSink() 45 | 46 | _ = sink.expecting(42) { 47 | source.add(sink) 48 | } 49 | 50 | source.remove(sink) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/TestChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestChange.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-26. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | internal struct TestChange: ChangeType, Equatable, CustomStringConvertible { 13 | typealias Value = Int 14 | 15 | var values: [Int] 16 | 17 | init(_ values: [Int]) { 18 | self.values = values 19 | } 20 | 21 | init(from oldValue: Int, to newValue: Int) { 22 | values = [oldValue, newValue] 23 | } 24 | 25 | var isEmpty: Bool { 26 | return values.isEmpty 27 | } 28 | 29 | func apply(on value: inout Int) { 30 | XCTAssertEqual(value, values.first!) 31 | value = values.last! 32 | } 33 | 34 | mutating func merge(with next: TestChange) { 35 | XCTAssertEqual(self.values.last!, next.values.first!) 36 | values += next.values.dropFirst() 37 | } 38 | 39 | func reversed() -> TestChange { 40 | return TestChange(values.reversed()) 41 | } 42 | 43 | public var description: String { 44 | return values.map { "\($0)" }.joined(separator: " -> ") 45 | } 46 | 47 | static func ==(left: TestChange, right: TestChange) -> Bool { 48 | return left.values == right.values 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/TestObservable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestObservable.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-26. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | @testable import GlueKit 11 | 12 | class TestObservable: ObservableType, TransactionalThing { 13 | typealias Change = TestChange 14 | typealias Value = Int 15 | 16 | var _signal: TransactionalSignal? = nil 17 | var _transactionCount: Int = 0 18 | var _value: Value 19 | 20 | init(_ value: Value) { 21 | self._value = value 22 | } 23 | 24 | var value: Value { 25 | get { return _value } 26 | set { 27 | let old = _value 28 | beginTransaction() 29 | _value = newValue 30 | sendChange(.init(from: old, to: _value)) 31 | endTransaction() 32 | } 33 | } 34 | 35 | func add(_ sink: Sink) where Sink.Value == Update { 36 | signal.add(sink) 37 | } 38 | func remove(_ sink: Sink) -> Sink where Sink.Value == Update { 39 | return signal.remove(sink) 40 | } 41 | } 42 | 43 | class TestObservableValue: ObservableValueType, TransactionalThing { 44 | typealias Change = ValueChange 45 | 46 | var _signal: TransactionalSignal>? = nil 47 | var _transactionCount: Int = 0 48 | var _value: Value 49 | 50 | init(_ value: Value) { 51 | self._value = value 52 | } 53 | 54 | var value: Value { 55 | get { return _value } 56 | set { 57 | let old = _value 58 | beginTransaction() 59 | _value = newValue 60 | sendChange(.init(from: old, to: _value)) 61 | endTransaction() 62 | } 63 | } 64 | 65 | func add(_ sink: Sink) where Sink.Value == Update { 66 | signal.add(sink) 67 | } 68 | func remove(_ sink: Sink) -> Sink where Sink.Value == Update { 69 | return signal.remove(sink) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/TestUpdatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestUpdatable.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-26. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | @testable import GlueKit 11 | 12 | class TestUpdatable: UpdatableType, TransactionalThing { 13 | typealias Change = TestChange 14 | typealias Value = Int 15 | 16 | var _signal: TransactionalSignal? = nil 17 | var _transactionCount: Int = 0 18 | var _value: Value 19 | 20 | init(_ value: Value) { 21 | self._value = value 22 | } 23 | 24 | var value: Value { 25 | get { return _value } 26 | set { 27 | let old = _value 28 | beginTransaction() 29 | _value = newValue 30 | sendChange(.init(from: old, to: _value)) 31 | endTransaction() 32 | } 33 | } 34 | 35 | func apply(_ update: Update) { 36 | switch update { 37 | case .beginTransaction: 38 | beginTransaction() 39 | case .change(let change): 40 | change.apply(on: &_value) 41 | sendChange(change) 42 | case .endTransaction: 43 | endTransaction() 44 | } 45 | } 46 | 47 | func add(_ sink: Sink) where Sink.Value == Update { 48 | signal.add(sink) 49 | } 50 | func remove(_ sink: Sink) -> Sink where Sink.Value == Update { 51 | return signal.remove(sink) 52 | } 53 | } 54 | 55 | class TestUpdatableValue: UpdatableValueType, TransactionalThing { 56 | typealias Change = ValueChange 57 | 58 | var _signal: TransactionalSignal>? = nil 59 | var _transactionCount: Int = 0 60 | var _value: Value 61 | 62 | init(_ value: Value) { 63 | self._value = value 64 | } 65 | 66 | var value: Value { 67 | get { return _value } 68 | set { 69 | let old = _value 70 | beginTransaction() 71 | _value = newValue 72 | sendChange(.init(from: old, to: _value)) 73 | endTransaction() 74 | } 75 | } 76 | 77 | func apply(_ update: Update) { 78 | switch update { 79 | case .beginTransaction: 80 | beginTransaction() 81 | case .change(let change): 82 | change.apply(on: &_value) 83 | sendChange(change) 84 | case .endTransaction: 85 | endTransaction() 86 | } 87 | } 88 | 89 | func add(_ sink: Sink) where Sink.Value == Update { 90 | signal.add(sink) 91 | } 92 | func remove(_ sink: Sink) -> Sink where Sink.Value == Update { 93 | return signal.remove(sink) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/TestUtilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestUtilities.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-12-03. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import Foundation 11 | @testable import GlueKit 12 | 13 | @inline(never) 14 | func noop(_ value: Value) { 15 | } 16 | 17 | func XCTAssertEqual(_ a: @autoclosure () -> [[E]], _ b: @autoclosure () -> [[E]], message: String? = nil, file: StaticString = #file, line: UInt = #line) { 18 | let av = a() 19 | let bv = b() 20 | if !av.elementsEqual(bv, by: ==) { 21 | XCTFail(message ?? "\(av) is not equal to \(bv)", file: file, line: line) 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/TimerSourceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerSourceTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2015-12-03. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | @testable import GlueKit 11 | 12 | class TimerSourceTests: XCTestCase { 13 | 14 | func testNeverFiringTimer() { 15 | let queue = DispatchQueue(label: "testqueue", attributes: []) 16 | 17 | var timerTimes = [Date]() 18 | let timerSemaphore = DispatchSemaphore(value: 0) 19 | 20 | let source = TimerSource(queue: queue) { 21 | timerTimes.append(Date()) 22 | timerSemaphore.signal() 23 | return nil 24 | } 25 | 26 | XCTAssertEqual(timerTimes, []) 27 | 28 | var sinkTimes = [Date]() 29 | let sinkSemaphore = DispatchSemaphore(value: 0) 30 | let connection = source.subscribe { 31 | sinkTimes.append(Date()) 32 | sinkSemaphore.signal() 33 | } 34 | 35 | XCTAssertEqual(timerSemaphore.wait(timeout: DispatchTime.now() + 3.0), .success, "Timer source should call timer closure when it is first connected") 36 | 37 | XCTAssertEqual(timerTimes.count, 1) 38 | XCTAssertEqual(sinkTimes, []) 39 | 40 | connection.disconnect() 41 | } 42 | 43 | func testRefreshingTimerSource() { 44 | let queue = DispatchQueue(label: "testqueue", attributes: []) 45 | 46 | var signal = false 47 | var timerTimes = [Date]() 48 | let timerSemaphore = DispatchSemaphore(value: 0) 49 | 50 | var triggerDate: Date? = nil 51 | 52 | let source = TimerSource(queue: queue) { 53 | timerTimes.append(Date()) 54 | if let date = triggerDate { 55 | triggerDate = nil 56 | return date 57 | } 58 | else { 59 | if signal { 60 | timerSemaphore.signal() 61 | } 62 | return nil 63 | } 64 | } 65 | 66 | XCTAssertEqual(timerTimes, []) 67 | 68 | var sinkTimes = [Date]() 69 | let connection = source.subscribe { 70 | sinkTimes.append(Date()) 71 | } 72 | // Timer should have returned nil -> No firing yet 73 | 74 | XCTAssertEqual(sinkTimes, []) 75 | 76 | queue.sync { 77 | signal = true 78 | triggerDate = Date(timeIntervalSinceNow: 0.1) 79 | source.start() 80 | } 81 | 82 | // Timer should return non-nil, clear triggerDate and signal the semaphore. 83 | 84 | XCTAssertEqual(.success, timerSemaphore.wait(timeout: DispatchTime.now() + 3.0)) 85 | 86 | XCTAssertEqual(timerTimes.count, 3) // 1: subscribe, 2: start, 3: after first firing 87 | XCTAssertEqual(sinkTimes.count, 1) // Should fire only once 88 | 89 | connection.disconnect() 90 | } 91 | 92 | func testSimplePeriodicSignal() { 93 | let queue = DispatchQueue(label: "testqueue", attributes: []) 94 | let start = Date().addingTimeInterval(0.2) 95 | let interval: TimeInterval = 0.2 96 | 97 | let source = TimerSource(queue: queue, start: start, interval: interval) 98 | 99 | var ticks = [TimeInterval]() 100 | var count = 0 101 | let sem = DispatchSemaphore(value: 0) 102 | let connection = source.subscribe { i in 103 | let elapsed = Date().timeIntervalSince(start) 104 | ticks.append(elapsed) 105 | NSLog("tick \(count) at \(elapsed)") 106 | count += 1 107 | if count >= 3 { 108 | sem.signal() 109 | } 110 | } 111 | 112 | XCTAssertEqual(.success, sem.wait(timeout: DispatchTime.now() + 3.0)) 113 | connection.disconnect() 114 | 115 | let diffs: [TimeInterval] = ticks.enumerated().map { tick, elapsed in 116 | let ideal = TimeInterval(tick) * interval 117 | return elapsed - ideal 118 | } 119 | XCTAssertEqual(diffs.filter { $0 < 0 }, []) 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/UpdateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-26. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class UpdateTests: XCTestCase { 13 | func testChangeExtraction() { 14 | let change = TestChange([1, 2, 3]) 15 | let updates: [Update] = [ 16 | .beginTransaction, 17 | .change(change), 18 | .endTransaction, 19 | ] 20 | let expected: [String] = ["nil", "1 -> 2 -> 3", "nil"] 21 | let actual = updates.map { update -> String in 22 | if let change = update.change { 23 | return "\(change)" 24 | } 25 | else { 26 | return "nil" 27 | } 28 | } 29 | XCTAssertEqual(actual, expected) 30 | } 31 | 32 | func testFilter() { 33 | let change = TestChange([1, 2, 3]) 34 | let updates: [Update] = [ 35 | .beginTransaction, 36 | .change(change), 37 | .endTransaction, 38 | ] 39 | let expected1: [String] = ["begin", "1 -> 2 -> 3", "end"] 40 | let actual1 = updates.map { describe($0.filter { _ in true }) } 41 | XCTAssertEqual(actual1, expected1) 42 | 43 | let expected2: [String] = ["begin", "nil", "end"] 44 | let actual2 = updates.map { describe($0.filter { _ in false }) } 45 | XCTAssertEqual(actual2, expected2) 46 | } 47 | 48 | func testMap() { 49 | let change = TestChange([1, 2, 3]) 50 | let updates: [Update] = [ 51 | .beginTransaction, 52 | .change(change), 53 | .endTransaction, 54 | ] 55 | 56 | let expected: [String] = ["begin", "2 -> 4 -> 6", "end"] 57 | let actual = updates.map { describe($0.map { TestChange($0.values.map { 2 * $0 }) }) } 58 | XCTAssertEqual(actual, expected) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/ValueBufferingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BufferedValueTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-28. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | @testable import GlueKit 11 | 12 | class BufferedValueTests: XCTestCase { 13 | 14 | func test_connectsImmediately() { 15 | let observable = TestObservableValue(0) 16 | 17 | do { 18 | let buffered = observable.buffered() 19 | XCTAssertTrue(observable.isConnected) 20 | withExtendedLifetime(buffered) {} 21 | } 22 | XCTAssertFalse(observable.isConnected) 23 | } 24 | 25 | func test_isntRetainedByObservable() { 26 | let observable = TestObservableValue(0) 27 | weak var weakSink: MockValueUpdateSink? = nil 28 | do { 29 | let buffered = observable.buffered() 30 | let sink = MockValueUpdateSink() 31 | weakSink = sink 32 | sink.subscribe(to: buffered.updates) 33 | withExtendedLifetime(buffered) {} 34 | } 35 | // If the sink is still alive, the buffered observable wasn't deallocated. 36 | XCTAssertNil(weakSink, "Possible retain cycle") 37 | } 38 | 39 | func test_updates() { 40 | let observable = TestObservableValue(0) 41 | let buffered = observable.buffered() 42 | 43 | XCTAssertEqual(buffered.value, 0) 44 | observable.value = 1 45 | XCTAssertEqual(buffered.value, 1) 46 | 47 | let sink = MockValueUpdateSink(buffered) 48 | 49 | sink.expecting(["begin", "1 -> 2", "end"]) { 50 | observable.value = 2 51 | } 52 | 53 | sink.expecting("begin") { 54 | observable.beginTransaction() 55 | } 56 | 57 | sink.expectingNothing { 58 | observable.value = 3 59 | } 60 | 61 | sink.expectingNothing { 62 | observable.value = 4 63 | } 64 | 65 | sink.expecting(["2 -> 4", "end"]) { 66 | observable.endTransaction() 67 | } 68 | 69 | sink.disconnect() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/ValueChangeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueChangeTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-10-27. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class ValueChangeTests: XCTestCase { 13 | 14 | func test() { 15 | let change = ValueChange(from: 1, to: 2) 16 | XCTAssertEqual(change.old, 1) 17 | XCTAssertEqual(change.new, 2) 18 | XCTAssertFalse(change.isEmpty) 19 | 20 | var value = 1 21 | change.apply(on: &value) 22 | XCTAssertEqual(value, 2) 23 | 24 | XCTAssertEqual(change.applied(on: 1), 2) 25 | 26 | var m = ValueChange(from: 0, to: 1) 27 | m.merge(with: change) 28 | XCTAssertEqual(m.old, 0) 29 | XCTAssertEqual(m.new, 2) 30 | 31 | let m2 = ValueChange(from: 0, to: 1).merged(with: change) 32 | XCTAssertEqual(m2.old, 0) 33 | XCTAssertEqual(m2.new, 2) 34 | 35 | let reversed = change.reversed() 36 | XCTAssertEqual(reversed.old, 2) 37 | XCTAssertEqual(reversed.new, 1) 38 | 39 | let transformed = change.map { 2 * $0 } 40 | XCTAssertEqual(transformed.old, 2) 41 | XCTAssertEqual(transformed.new, 4) 42 | 43 | XCTAssertTrue(change == ValueChange(from: 1, to: 2)) 44 | XCTAssertFalse(change == ValueChange(from: 0, to: 2)) 45 | XCTAssertFalse(change == ValueChange(from: 1, to: 4)) 46 | 47 | XCTAssertFalse(change != ValueChange(from: 1, to: 2)) 48 | XCTAssertTrue(change != ValueChange(from: 0, to: 2)) 49 | XCTAssertTrue(change != ValueChange(from: 1, to: 4)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/GlueKitTests/ValueReferenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueReferenceTests.swift 3 | // GlueKit 4 | // 5 | // Created by Károly Lőrentey on 2016-11-02. 6 | // Copyright © 2015–2017 Károly Lőrentey. 7 | // 8 | 9 | import XCTest 10 | import GlueKit 11 | 12 | class ValueReferenceTests: XCTestCase { 13 | func testReference() { 14 | let a = Variable(0) 15 | let b = Variable(10) 16 | let c = Variable(20) 17 | let ref = Variable>(a.anyObservableValue) 18 | 19 | XCTAssertEqual(ref.value.value, 0) 20 | a.value = 1 21 | XCTAssertEqual(ref.value.value, 1) 22 | 23 | let unpacked = ref.unpacked() 24 | 25 | XCTAssertEqual(unpacked.value, 1) 26 | a.value = 2 27 | XCTAssertEqual(unpacked.value, 2) 28 | 29 | let sink = MockValueUpdateSink(unpacked) 30 | 31 | sink.expecting(["begin", "2 -> 3", "end"]) { 32 | a.value = 3 33 | } 34 | 35 | sink.expecting(["begin", "3 -> 10", "end"]) { 36 | ref.value = b.anyObservableValue 37 | } 38 | 39 | sink.expecting(["begin", "10 -> 11", "end"]) { 40 | b.value = 11 41 | } 42 | 43 | sink.expecting("begin") { 44 | b.apply(.beginTransaction) 45 | } 46 | 47 | sink.expectingNothing { 48 | ref.apply(.beginTransaction) 49 | } 50 | 51 | sink.expecting("11 -> 12") { 52 | b.value = 12 53 | } 54 | 55 | sink.expecting("12 -> 20") { 56 | ref.value = c.anyObservableValue 57 | } 58 | 59 | sink.expecting("20 -> 21") { 60 | c.value = 21 61 | } 62 | 63 | sink.expectingNothing { 64 | b.apply(.endTransaction) 65 | } 66 | 67 | sink.expecting("end") { 68 | ref.apply(.endTransaction) 69 | } 70 | 71 | sink.disconnect() 72 | } 73 | 74 | func testDerivedObservable() { 75 | let a = Variable(0) 76 | let double = a.map { 2 * $0 } 77 | 78 | let ref = Variable>(a.anyObservableValue) 79 | 80 | let sink = MockValueUpdateSink(ref.unpacked()) 81 | 82 | sink.expecting(["begin", "0 -> 1", "end"]) { 83 | a.value = 1 84 | } 85 | 86 | sink.expecting(["begin", "1 -> 2", "end"]) { 87 | ref.value = double.anyObservableValue 88 | } 89 | 90 | sink.expecting(["begin", "2 -> 4", "end"]) { 91 | a.value = 2 92 | } 93 | 94 | sink.disconnect() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Tests/PerformanceTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /jazzy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | jazzy \ 4 | --clean \ 5 | --author "Károly Lőrentey" \ 6 | --author_url "https://twitter.com/lorentey" \ 7 | --github_url https://github.com/lorentey/GlueKit \ 8 | --github-file-prefix https://github.com/lorentey/GlueKit/tree/master \ 9 | --module-version 1.0.0-alpha.1 \ 10 | --xcodebuild-arguments -scheme,GlueKit \ 11 | --module GlueKit \ 12 | --root-url https://lorentey.github.io/GlueKit/reference/ \ 13 | --output jazzy/output \ 14 | --swift-version 2.1.1 15 | 16 | #--template-directory jazzy/templates \ 17 | -------------------------------------------------------------------------------- /version.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // version.xcconfig 3 | // 4 | // Created by Károly Lőrentey on 2016-03-08. 5 | // Copyright © 2015–2017 Károly Lőrentey. 6 | // 7 | 8 | // Increment the build number whenever you modify the version string. 9 | VERSION_STRING = 0.2.0 10 | BUILD_NUMBER = 2 11 | 12 | PROJECT_NAME = GlueKit 13 | BUNDLE_IDENTIFIER_BASE = org.attaswift.$(PROJECT_NAME) 14 | 15 | IPHONEOS_DEPLOYMENT_TARGET = 9.3 16 | MACOSX_DEPLOYMENT_TARGET = 10.11 17 | WATCHOS_DEPLOYMENT_TARGET = 3.0 18 | TVOS_DEPLOYMENT_TARGET = 10.0 19 | 20 | APPLICATION_EXTENSION_API_ONLY = YES 21 | --------------------------------------------------------------------------------