diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..ab5e09e3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @stephanecopin @metatheoretic @heymansmile @notbenoit diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 00000000..51e2f5ee --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,3 @@ +ignore: + - "Pods" + - "Tests" diff --git a/.gitignore b/.gitignore index 2e525462..f657dea3 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,7 @@ playground.xcworkspace # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # -# Pods/ +Pods/ # Carthage # diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c2c3544a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,51 @@ +## Main + +##### New Features/Enhancements + +- Add `ActionProtocol` +- Add `AnyAction`, allowing to type-erase any actions represented by a `ActionProtocol` +- Add `CoalescingAction` & `OverridingAction` +- Add a `CombineExtensions` & `CombineExtensionsProvider` protocol, replicating the `.reactive` of ReactiveSwift (so as not to pollute too much the global namespace) +- All `NSObject` now can have a `cancellables` object attached to them via `.combineExtensions.cancellables`. It's created lazily, so there's impact if not used. +- Extend `Publisher.handleEvents` to add two new hooks: + - `receiveTermination`: When either a completion or a cancellation is received + - `receiveResult`: Takes a `Result`, and called when either values are received or an error is received +- Add `Publisher.promoteOptional()` +- Add `then(receiveResult:)`, which takes a closure with a `Result`, allowing to handle values & error in the same place +- Add `sinkForLifetimeOf(_:)` methods family, allowing to sink on a publisher and link to the lifetime of a given `CombineExtensionsProvider & AnyObject`. The goal of this is to avoid having to write the classic boilerplate code in Combine handling with having to create `cancellables` for every single object (this used the `cancellables` extension mentioned above). +- Add `performDuringLifetimeOf(_:action:)`, allowing to link an action with the lifetime of an object. This act as an equivalent for `makeBindingTarget` from `ReactiveSwift` when calling functions or assigning multiple variables. +- Add `assign(to:forLifetimeOf:)`, allowing to assign the output of the producer to a keyPath, keeping it alive until the specified object is deallocated. +- Add `TapAction` and `.combineExtensions.tapped`, allowing to link an `Action` to a button, without having to do the bindings manually (similar to `UIButton.reactive.pressed` in ReactiveCocoa) +- Add `.publisherForControlEvent(_:)`, to get a publisher that triggers on any control events. +- Add `(UITextField/UITextView).(textValues|continuousTextValues)`, which are equivalent to same thing as for ReactiveCocoa. + [Stéphane Copin](https://github.com/stephanecopin) + [#54](https://github.com/Fueled/ios-utilities/pull/54) + +- Add an optional `insets` parameter to `addAndFitSubview()` +- Make `removeArrangedSubviews()`'s `removeFromHierachy` parameter default to `true` +- Add `tapped` helper to link any `ReactiveActionProtocol` to any `UIControl` +- Add `AnyIdentifiable` & `AnyAction` for type-erased `Identifiable` & `ReactiveActionProtocol` respectively +- Add `OverridingAction`, a new `Action` that if executed when already executing, will cancel the previous producer and start a new one +- Make `OrderedSet` conform to `SetAlgebra` + [Stéphane Copin](https://github.com/stephanecopin) + [#53](https://github.com/Fueled/ios-utilities/pull/53) + +##### Bug Fixes + +- Fix a bug in `CombineLatestMany` where cancelling the resulting publisher would not cancel the array of publishers themselves. + [Stéphane Copin](https://github.com/stephanecopin) + [#55](https://github.com/Fueled/ios-utilities/pull/55) + +- Fix a bug in `Action` where a cancellation would be ignored and not set `isExecuting` to `false` + [Stéphane Copin](https://github.com/stephanecopin) + [#54](https://github.com/Fueled/ios-utilities/pull/54) + +- Fix an internal state corruption issue in `OrderedSet` + [Stéphane Copin](https://github.com/stephanecopin) + [#53](https://github.com/Fueled/ios-utilities/pull/53) + +##### Breaking changes + +- The original `TapAction`, `OverridingAction` and `AnyAction` were all prefixed with `Reactive`. + [Stéphane Copin](https://github.com/stephanecopin) + [#54](https://github.com/Fueled/ios-utilities/pull/54) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c550fe76..307aa6be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ In order to properly clone the project and be ready to submit bug fixes/new feat ```shell git submodule update --init --recursive ``` -2. Open `FueledUtils.xcworkspace` +2. Open `Test/FueledUtils.xcworkspace` and update the *FueledUtils* Pod in `Development Pods` 3. You're ready to go! ## How to contribute diff --git a/Cartfile b/Cartfile deleted file mode 100644 index b088968f..00000000 --- a/Cartfile +++ /dev/null @@ -1,2 +0,0 @@ -github "ReactiveCocoa/ReactiveSwift" ~> 6.0 -github "ReactiveCocoa/ReactiveCocoa" ~> 10.0 diff --git a/Cartfile.private b/Cartfile.private deleted file mode 100644 index 5791b6e3..00000000 --- a/Cartfile.private +++ /dev/null @@ -1,2 +0,0 @@ -github "Quick/Quick" ~> 2.0 -github "Quick/Nimble" ~> 8.0 diff --git a/Cartfile.resolved b/Cartfile.resolved deleted file mode 100644 index 56303f04..00000000 --- a/Cartfile.resolved +++ /dev/null @@ -1,4 +0,0 @@ -github "Quick/Nimble" "v8.0.4" -github "Quick/Quick" "v2.2.0" -github "ReactiveCocoa/ReactiveCocoa" "10.1.0" -github "ReactiveCocoa/ReactiveSwift" "6.1.0" diff --git a/Carthage/Checkouts/Nimble b/Carthage/Checkouts/Nimble deleted file mode 160000 index 6abeb3f5..00000000 --- a/Carthage/Checkouts/Nimble +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6abeb3f5c03beba2b9e4dbe20886e773b5b629b6 diff --git a/Carthage/Checkouts/Quick b/Carthage/Checkouts/Quick deleted file mode 160000 index 33682c2f..00000000 --- a/Carthage/Checkouts/Quick +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 33682c2f6230c60614861dfc61df267e11a1602f diff --git a/Carthage/Checkouts/ReactiveCocoa b/Carthage/Checkouts/ReactiveCocoa deleted file mode 160000 index 21deb683..00000000 --- a/Carthage/Checkouts/ReactiveCocoa +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 21deb683540db0d059dd7945627493275936b7db diff --git a/Carthage/Checkouts/ReactiveSwift b/Carthage/Checkouts/ReactiveSwift deleted file mode 160000 index b772fa0b..00000000 --- a/Carthage/Checkouts/ReactiveSwift +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b772fa0b624926e6e2f21acbb79297736a05c585 diff --git a/Dangerfile b/Dangerfile new file mode 100644 index 00000000..2a58fe91 --- /dev/null +++ b/Dangerfile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +has_app_changes = !git.modified_files.grep(/FueledUtils/).empty? +if !git.modified_files.include?('CHANGELOG.md') && has_app_changes + warn("Please include a CHANGELOG entry to credit yourself! \nYou can find it at [CHANGELOG.md](https://github.com/Fueled/ios-utilities/blob/develop/CHANGELOG.md).", :sticky => false) + markdown <<-MARKDOWN +Here's an example of your CHANGELOG entry: +```markdown +- #{github.pr_title}\s\s + [#{github.pr_author}](https://github.com/#{github.pr_author}) + [#pull_request_number](https://github.com/Fueled/ios-utilities/pulls/pull_request_number) +``` +*note*: There are two invisible spaces after the entry's text. +MARKDOWN +end diff --git a/FueledUtils.playground/Contents.swift b/FueledUtils.playground/Contents.swift deleted file mode 100644 index 09d25acd..00000000 --- a/FueledUtils.playground/Contents.swift +++ /dev/null @@ -1,12 +0,0 @@ -import FueledUtils -import PlaygroundSupport -import UIKit - -let gradientView = GradientView(frame: CGRect(origin: .zero, size: CGSize(width: 500.0, height: 500.0))) - -gradientView.type = .radial(startCenter: CGPoint(x: 0.5, y: 0.5), startRadius: 10.0, endCenter: CGPoint(x: 0.5, y: 0.5), endRadius: 50.0) -gradientView.definition = .custom([(.black, 0.0), (.red, 0.5), (.blue, 1.0)]) - -let test: [Int] = [2, 3] -let result = test.splitBetween { $0 == 2 && $1 == 3 } -result diff --git a/FueledUtils.playground/contents.xcplayground b/FueledUtils.playground/contents.xcplayground deleted file mode 100644 index 9f5f2f40..00000000 --- a/FueledUtils.playground/contents.xcplayground +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/FueledUtils.podspec b/FueledUtils.podspec index 63812ce9..3966e47b 100644 --- a/FueledUtils.podspec +++ b/FueledUtils.podspec @@ -1,27 +1,97 @@ +# frozen_string_literal: true + Pod::Spec.new do |s| - s.name = 'FueledUtils' - s.version = '2.0.3' - s.summary = 'A collection of utilities used at Fueled' - s.description = 'This is a collection of classes, extensions, methods and functions used within Fueled projects that aims at decomplexifying tasks that should be easy.' - s.swift_version = '5' - - s.homepage = 'https://github.com/Fueled/ios-utilities' - s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } - s.author = { 'Vadim-Yelagin' => 'vadim.yelagin@gmail.com', 'stephanecopin' => 'stephane@fueled.com', 'leontiy' => 'leonty@fueled.com', 'bastienFalcou' => 'bastien@fueled.com', 'heymansmile' => 'ivan@fueled.com', 'thib4ult' => 'thibault@fueled.com', 'notbenoit' => 'benoit@fueled.com' } - s.source = { :git => 'https://github.com/Fueled/ios-utilities.git', :tag => s.version.to_s } - s.documentation_url = 'https://cdn.rawgit.com/Fueled/ios-utilities/master/docs/index.html' - - s.ios.deployment_target = '8.0' - s.osx.deployment_target = '10.9' - s.watchos.deployment_target = '2.0' - s.tvos.deployment_target = '9.0' - - s.source_files = 'FueledUtils/**/*.swift' - s.osx.exclude_files = ['FueledUtils/FueledUtils.h', 'FueledUtils/ButtonWithTitleAdjustment.swift', 'FueledUtils/DecoratingTextFieldDelegate.swift', 'FueledUtils/DimmingButton.swift', 'FueledUtils/HairlineView.swift', 'FueledUtils/HairlineView.swift', 'FueledUtils/KeyboardInsetHelper.swift', 'FueledUtils/LabelWithTitleAdjustment.swift', 'FueledUtils/ReactiveCocoaExtensions.swift', 'FueledUtils/ScrollViewPage.swift', 'FueledUtils/SetRootViewController.swift', 'FueledUtils/SignalingAlert.swift', 'FueledUtils/UIExtensions.swift', 'FueledUtils/GradientView.swift'] - s.ios.exclude_files = ['FueledUtils/FueledUtils.h'] - s.watchos.exclude_files = ['FueledUtils/FueledUtils.h', 'FueledUtils/ButtonWithTitleAdjustment.swift', 'FueledUtils/DecoratingTextFieldDelegate.swift', 'FueledUtils/DimmingButton.swift', 'FueledUtils/HairlineView.swift', 'FueledUtils/HairlineView.swift', 'FueledUtils/KeyboardInsetHelper.swift', 'FueledUtils/LabelWithTitleAdjustment.swift', 'FueledUtils/ReactiveCocoaExtensions.swift', 'FueledUtils/ScrollViewPage.swift', 'FueledUtils/SetRootViewController.swift', 'FueledUtils/SignalingAlert.swift', 'FueledUtils/UIExtensions.swift', 'FueledUtils/GradientView.swift'] - s.tvos.exclude_files = ['FueledUtils/FueledUtils.h', 'FueledUtils/KeyboardInsetHelper.swift'] - - s.dependency 'ReactiveSwift', '~> 6.0' - s.dependency 'ReactiveCocoa', '~> 10.0' + s.name = 'FueledUtils' + s.version = '3.0.0' + s.summary = 'A collection of utilities used at Fueled' + s.description = 'This is a collection of classes, extensions, methods and functions used within Fueled projects that aims at decomplexifying tasks that should be easy.' + s.swift_version = '5' + + s.homepage = 'https://github.com/Fueled/ios-utilities' + s.license = { type: 'Apache License, Version 2.0', file: 'LICENSE' } + s.author = { 'Vadim-Yelagin' => 'vadim.yelagin@gmail.com', 'stephanecopin' => 'stephane@fueled.com', 'leontiy' => 'leonty@fueled.com', 'bastienFalcou' => 'bastien@fueled.com', 'heymansmile' => 'ivan@fueled.com', 'thib4ult' => 'thibault@fueled.com', 'notbenoit' => 'benoit@fueled.com' } + s.source = { git: 'https://github.com/Fueled/ios-utilities.git', tag: s.version.to_s } + s.documentation_url = 'https://cdn.rawgit.com/Fueled/ios-utilities/master/docs/index.html' + + s.ios.deployment_target = '9.0' + s.osx.deployment_target = '10.12' + s.watchos.deployment_target = '2.0' + s.tvos.deployment_target = '9.0' + + s.subspec 'Core' do |s| + s.source_files = 'FueledUtils/Core/**/*.swift' + end + + s.subspec 'ReactiveCommon' do |s| + s.source_files = 'FueledUtils/ReactiveCommon/**/*.swift' + + s.dependency 'FueledUtils/Core' + end + + s.subspec 'ReactiveSwift' do |s| + s.dependency 'FueledUtils/ReactiveCommon' + s.dependency 'ReactiveSwift', '~> 6.0' + s.dependency 'ReactiveCocoa', '~> 10.0' + + s.source_files = 'FueledUtils/ReactiveSwift/**/*.swift' + end + + s.subspec 'UIKit' do |s| + s.dependency 'FueledUtils/Core' + s.ios.source_files = 'FueledUtils/UIKit/**/*.swift' + end + + s.subspec 'ReactiveSwiftUIKit' do |s| + s.dependency 'FueledUtils/ReactiveSwift' + s.dependency 'FueledUtils/UIKit' + + s.ios.source_files = 'FueledUtils/ReactiveSwiftUIKit/**/*.swift' + end + + s.subspec 'Combine' do |s| + # Update the above with the following versions when we drop support for iOS < 13.0 or + # uncomment below if https://github.com/CocoaPods/CocoaPods/issues/7333 is implemented + # s.ios.deployment_target = '13.0' + # s.osx.deployment_target = '10.15' + # s.watchos.deployment_target = '6.0' + # s.tvos.deployment_target = '13.0' + + s.dependency 'FueledUtils/ReactiveCommon' + + s.source_files = 'FueledUtils/Combine/**/*.swift' + end + + s.subspec 'CombineOperators' do |s| + s.dependency 'FueledUtils/Combine' + + s.source_files = 'FueledUtils/CombineOperators/**/*.swift' + end + + s.subspec 'CombineUIKit' do |s| + s.dependency 'FueledUtils/Combine' + s.dependency 'FueledUtils/UIKit' + + s.ios.source_files = 'FueledUtils/CombineUIKit/**/*.swift' + end + + s.subspec 'SwiftUI' do |s| + s.dependency 'FueledUtils/Core' + s.dependency 'FueledUtils/Combine' + + s.source_files = 'FueledUtils/SwiftUI/**/*.swift' + end + + s.subspec 'ReactiveCombineBridge' do |s| + s.dependency 'FueledUtils/ReactiveSwift' + s.dependency 'FueledUtils/Combine' + + s.source_files = 'FueledUtils/ReactiveCombineBridge/**/*.swift' + end + + s.osx.exclude_files = ['FueledUtils/FueledUtils.h', 'FueledUtils/ButtonWithTitleAdjustment.swift', 'FueledUtils/DecoratingTextFieldDelegate.swift', 'FueledUtils/DimmingButton.swift', 'FueledUtils/HairlineView.swift', 'FueledUtils/HairlineView.swift', 'FueledUtils/KeyboardInsetHelper.swift', 'FueledUtils/LabelWithTitleAdjustment.swift', 'FueledUtils/ReactiveCocoaExtensions.swift', 'FueledUtils/ScrollViewPage.swift', 'FueledUtils/SetRootViewController.swift', 'FueledUtils/SignalingAlert.swift', 'FueledUtils/UIExtensions.swift', 'FueledUtils/GradientView.swift'] + s.ios.exclude_files = ['FueledUtils/FueledUtils.h'] + s.watchos.exclude_files = ['FueledUtils/FueledUtils.h', 'FueledUtils/ButtonWithTitleAdjustment.swift', 'FueledUtils/DecoratingTextFieldDelegate.swift', 'FueledUtils/DimmingButton.swift', 'FueledUtils/HairlineView.swift', 'FueledUtils/HairlineView.swift', 'FueledUtils/KeyboardInsetHelper.swift', 'FueledUtils/LabelWithTitleAdjustment.swift', 'FueledUtils/ReactiveCocoaExtensions.swift', 'FueledUtils/ScrollViewPage.swift', 'FueledUtils/SetRootViewController.swift', 'FueledUtils/SignalingAlert.swift', 'FueledUtils/UIExtensions.swift', 'FueledUtils/GradientView.swift'] + s.tvos.exclude_files = ['FueledUtils/FueledUtils.h', 'FueledUtils/KeyboardInsetHelper.swift'] + + s.default_subspecs = 'Core' end diff --git a/FueledUtils.xcodeproj/project.pbxproj b/FueledUtils.xcodeproj/project.pbxproj deleted file mode 100644 index 1c766aa7..00000000 --- a/FueledUtils.xcodeproj/project.pbxproj +++ /dev/null @@ -1,1166 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 0238E9351E69B53200BF26D4 /* ScrollViewPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0238E9341E69B53200BF26D4 /* ScrollViewPage.swift */; }; - 024E88731DA1597900826334 /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 024E88721DA1597900826334 /* ReactiveSwift.framework */; }; - 02DE3C221D258C79002B58E2 /* FueledUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 02DE3C211D258C79002B58E2 /* FueledUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 02DE3C2A1D258E02002B58E2 /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DE3C291D258E02002B58E2 /* ReactiveCocoa.framework */; }; - 02DE3C3C1D258FD1002B58E2 /* ReactiveSwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C351D258FD1002B58E2 /* ReactiveSwiftExtensions.swift */; }; - 02DE3C3E1D258FD1002B58E2 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C371D258FD1002B58E2 /* SequenceExtensions.swift */; }; - 02DE3C3F1D258FD1002B58E2 /* SignalingAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C381D258FD1002B58E2 /* SignalingAlert.swift */; }; - 02DE3C421D258FD1002B58E2 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C3B1D258FD1002B58E2 /* StringExtensions.swift */; }; - 02DE3C441D25928F002B58E2 /* KeyboardInsetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C431D25928F002B58E2 /* KeyboardInsetHelper.swift */; }; - 02DE3C461D25933E002B58E2 /* HairlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C451D25933E002B58E2 /* HairlineView.swift */; }; - 02DE3C491D25952A002B58E2 /* NSDecimalNumberOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C471D25952A002B58E2 /* NSDecimalNumberOperators.swift */; }; - 02DE3C4A1D25952A002B58E2 /* Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C481D25952A002B58E2 /* Regex.swift */; }; - 02DE3C4D1D259628002B58E2 /* ButtonWithTitleAdjustment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C4B1D259628002B58E2 /* ButtonWithTitleAdjustment.swift */; }; - 02DE3C4E1D259628002B58E2 /* LabelWithTitleAdjustment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C4C1D259628002B58E2 /* LabelWithTitleAdjustment.swift */; }; - 02DE3C501D259966002B58E2 /* SetRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C4F1D259966002B58E2 /* SetRootViewController.swift */; }; - 02DE3C551D259FA9002B58E2 /* DimmingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C541D259FA9002B58E2 /* DimmingButton.swift */; }; - 02DE3C571D25A24B002B58E2 /* DecoratingTextFieldDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C561D25A24B002B58E2 /* DecoratingTextFieldDelegate.swift */; }; - 02DF8ECB1E6464AB009AB29C /* ReactiveLifetimeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DF8ECA1E6464AB009AB29C /* ReactiveLifetimeProvider.swift */; }; - 02E626091D340F0C0041E512 /* LoadingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E626081D340F0C0041E512 /* LoadingState.swift */; }; - 35996E0F1F55C3E1004D6AC0 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35996E0E1F55C3E1004D6AC0 /* FoundationExtensions.swift */; }; - 991267652257405F00D39A08 /* ReactiveCocoaExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574121F909290006FB3F /* ReactiveCocoaExtensions.swift */; }; - 99126766225740DB00D39A08 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C3A1D258FD1002B58E2 /* UIExtensions.swift */; }; - DFA57F0621E7A64200467647 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA57F0521E7A64200467647 /* GradientView.swift */; }; - F40C574221F9092A0006FB3F /* ReactiveCocoaExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574121F909290006FB3F /* ReactiveCocoaExtensions.swift */; }; - F40C574421F909A60006FB3F /* ActionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574321F909A60006FB3F /* ActionProtocol.swift */; }; - F40C574621F90BD10006FB3F /* TypedSerialDisposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574521F90BD10006FB3F /* TypedSerialDisposable.swift */; }; - F40C574821F9182B0006FB3F /* TransferState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574721F9182B0006FB3F /* TransferState.swift */; }; - F45268392355E83200DA9B9B /* CoalescingActionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45268382355E83200DA9B9B /* CoalescingActionSpec.swift */; }; - F463DFC1222D83060031AAA5 /* ActionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574321F909A60006FB3F /* ActionProtocol.swift */; }; - F463DFC2222D83060031AAA5 /* ButtonWithTitleAdjustment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C4B1D259628002B58E2 /* ButtonWithTitleAdjustment.swift */; }; - F463DFC3222D83060031AAA5 /* InputCoalescingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F464775C21FA3FA5005F8B7E /* InputCoalescingAction.swift */; }; - F463DFC4222D83060031AAA5 /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A619541F47263100777BB2 /* CollectionExtensions.swift */; }; - F463DFC5222D83060031AAA5 /* DecoratingTextFieldDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C561D25A24B002B58E2 /* DecoratingTextFieldDelegate.swift */; }; - F463DFC6222D83060031AAA5 /* DimmingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C541D259FA9002B58E2 /* DimmingButton.swift */; }; - F463DFC7222D83060031AAA5 /* TransferState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574721F9182B0006FB3F /* TransferState.swift */; }; - F463DFC8222D83060031AAA5 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35996E0E1F55C3E1004D6AC0 /* FoundationExtensions.swift */; }; - F463DFC9222D83060031AAA5 /* HairlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C451D25933E002B58E2 /* HairlineView.swift */; }; - F463DFCB222D83060031AAA5 /* LabelWithTitleAdjustment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C4C1D259628002B58E2 /* LabelWithTitleAdjustment.swift */; }; - F463DFCC222D83060031AAA5 /* LoadingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E626081D340F0C0041E512 /* LoadingState.swift */; }; - F463DFCD222D83060031AAA5 /* NSDecimalNumberOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C471D25952A002B58E2 /* NSDecimalNumberOperators.swift */; }; - F463DFCF222D83060031AAA5 /* ReactiveLifetimeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DF8ECA1E6464AB009AB29C /* ReactiveLifetimeProvider.swift */; }; - F463DFD0222D83060031AAA5 /* ReactiveSwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C351D258FD1002B58E2 /* ReactiveSwiftExtensions.swift */; }; - F463DFD1222D83060031AAA5 /* Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C481D25952A002B58E2 /* Regex.swift */; }; - F463DFD2222D83060031AAA5 /* ScrollViewPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0238E9341E69B53200BF26D4 /* ScrollViewPage.swift */; }; - F463DFD3222D83060031AAA5 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C371D258FD1002B58E2 /* SequenceExtensions.swift */; }; - F463DFD4222D83060031AAA5 /* SetRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C4F1D259966002B58E2 /* SetRootViewController.swift */; }; - F463DFD5222D83060031AAA5 /* SignalingAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C381D258FD1002B58E2 /* SignalingAlert.swift */; }; - F463DFD6222D83060031AAA5 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C3B1D258FD1002B58E2 /* StringExtensions.swift */; }; - F463DFD7222D83060031AAA5 /* TypedSerialDisposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574521F90BD10006FB3F /* TypedSerialDisposable.swift */; }; - F463DFD9222D83060031AAA5 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA57F0521E7A64200467647 /* GradientView.swift */; }; - F463DFDA222D83060031AAA5 /* ActionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574321F909A60006FB3F /* ActionProtocol.swift */; }; - F463DFDC222D83060031AAA5 /* InputCoalescingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F464775C21FA3FA5005F8B7E /* InputCoalescingAction.swift */; }; - F463DFDD222D83060031AAA5 /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A619541F47263100777BB2 /* CollectionExtensions.swift */; }; - F463DFE0222D83060031AAA5 /* TransferState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574721F9182B0006FB3F /* TransferState.swift */; }; - F463DFE1222D83060031AAA5 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35996E0E1F55C3E1004D6AC0 /* FoundationExtensions.swift */; }; - F463DFE5222D83060031AAA5 /* LoadingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E626081D340F0C0041E512 /* LoadingState.swift */; }; - F463DFE6222D83060031AAA5 /* NSDecimalNumberOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C471D25952A002B58E2 /* NSDecimalNumberOperators.swift */; }; - F463DFE8222D83060031AAA5 /* ReactiveLifetimeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DF8ECA1E6464AB009AB29C /* ReactiveLifetimeProvider.swift */; }; - F463DFE9222D83060031AAA5 /* ReactiveSwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C351D258FD1002B58E2 /* ReactiveSwiftExtensions.swift */; }; - F463DFEA222D83060031AAA5 /* Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C481D25952A002B58E2 /* Regex.swift */; }; - F463DFEC222D83060031AAA5 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C371D258FD1002B58E2 /* SequenceExtensions.swift */; }; - F463DFEF222D83060031AAA5 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C3B1D258FD1002B58E2 /* StringExtensions.swift */; }; - F463DFF0222D83060031AAA5 /* TypedSerialDisposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574521F90BD10006FB3F /* TypedSerialDisposable.swift */; }; - F463DFF3222D83070031AAA5 /* ActionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574321F909A60006FB3F /* ActionProtocol.swift */; }; - F463DFF5222D83070031AAA5 /* InputCoalescingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F464775C21FA3FA5005F8B7E /* InputCoalescingAction.swift */; }; - F463DFF6222D83070031AAA5 /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A619541F47263100777BB2 /* CollectionExtensions.swift */; }; - F463DFF9222D83070031AAA5 /* TransferState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574721F9182B0006FB3F /* TransferState.swift */; }; - F463DFFA222D83070031AAA5 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35996E0E1F55C3E1004D6AC0 /* FoundationExtensions.swift */; }; - F463DFFE222D83070031AAA5 /* LoadingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E626081D340F0C0041E512 /* LoadingState.swift */; }; - F463DFFF222D83070031AAA5 /* NSDecimalNumberOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C471D25952A002B58E2 /* NSDecimalNumberOperators.swift */; }; - F463E002222D83070031AAA5 /* ReactiveSwiftExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C351D258FD1002B58E2 /* ReactiveSwiftExtensions.swift */; }; - F463E003222D83070031AAA5 /* Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C481D25952A002B58E2 /* Regex.swift */; }; - F463E005222D83070031AAA5 /* SequenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C371D258FD1002B58E2 /* SequenceExtensions.swift */; }; - F463E008222D83070031AAA5 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C3B1D258FD1002B58E2 /* StringExtensions.swift */; }; - F463E009222D83070031AAA5 /* TypedSerialDisposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40C574521F90BD10006FB3F /* TypedSerialDisposable.swift */; }; - F463E00D222D836E0031AAA5 /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F463E00C222D836E0031AAA5 /* ReactiveCocoa.framework */; }; - F463E00F222D836E0031AAA5 /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F463E00E222D836E0031AAA5 /* ReactiveSwift.framework */; }; - F463E013222D837D0031AAA5 /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F463E012222D837D0031AAA5 /* ReactiveCocoa.framework */; }; - F463E017222D837D0031AAA5 /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F463E016222D837D0031AAA5 /* ReactiveSwift.framework */; }; - F463E019222D83890031AAA5 /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F463E018222D83890031AAA5 /* ReactiveCocoa.framework */; }; - F463E01B222D83890031AAA5 /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F463E01A222D83890031AAA5 /* ReactiveSwift.framework */; }; - F463E020222D83C40031AAA5 /* UIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE3C3A1D258FD1002B58E2 /* UIExtensions.swift */; }; - F463E022222D84620031AAA5 /* ReactiveLifetimeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DF8ECA1E6464AB009AB29C /* ReactiveLifetimeProvider.swift */; }; - F464775D21FA3FA5005F8B7E /* InputCoalescingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F464775C21FA3FA5005F8B7E /* InputCoalescingAction.swift */; }; - F4A619551F47263100777BB2 /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A619541F47263100777BB2 /* CollectionExtensions.swift */; }; - F4CBDF182200A72400DF24DD /* ReactiveSwiftExtensionsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4CBDF172200A72400DF24DD /* ReactiveSwiftExtensionsSpec.swift */; }; - F4CBDF1E2200A99A00DF24DD /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4CBDF1D2200A99A00DF24DD /* Nimble.framework */; }; - F4CBDF202200A99A00DF24DD /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4CBDF1F2200A99A00DF24DD /* Quick.framework */; }; - F4CBDF222200A99A00DF24DD /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4CBDF212200A99A00DF24DD /* ReactiveCocoa.framework */; }; - F4CBDF242200A99A00DF24DD /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4CBDF232200A99A00DF24DD /* ReactiveSwift.framework */; }; - F4CBDF272200A99A00DF24DD /* FueledUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DE3C1E1D258C79002B58E2 /* FueledUtils.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - 0238E9341E69B53200BF26D4 /* ScrollViewPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollViewPage.swift; sourceTree = ""; }; - 024E88721DA1597900826334 /* ReactiveSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveSwift.framework; path = "Carthage/Checkouts/ReactiveSwift/build/Debug-iphoneos/ReactiveSwift.framework"; sourceTree = ""; }; - 02DE3C1E1D258C79002B58E2 /* FueledUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FueledUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 02DE3C211D258C79002B58E2 /* FueledUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FueledUtils.h; sourceTree = ""; }; - 02DE3C231D258C79002B58E2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 02DE3C291D258E02002B58E2 /* ReactiveCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ReactiveCocoa.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 02DE3C351D258FD1002B58E2 /* ReactiveSwiftExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveSwiftExtensions.swift; sourceTree = ""; }; - 02DE3C371D258FD1002B58E2 /* SequenceExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SequenceExtensions.swift; sourceTree = ""; }; - 02DE3C381D258FD1002B58E2 /* SignalingAlert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalingAlert.swift; sourceTree = ""; }; - 02DE3C3A1D258FD1002B58E2 /* UIExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIExtensions.swift; sourceTree = ""; }; - 02DE3C3B1D258FD1002B58E2 /* StringExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; - 02DE3C431D25928F002B58E2 /* KeyboardInsetHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardInsetHelper.swift; sourceTree = ""; }; - 02DE3C451D25933E002B58E2 /* HairlineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HairlineView.swift; sourceTree = ""; }; - 02DE3C471D25952A002B58E2 /* NSDecimalNumberOperators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDecimalNumberOperators.swift; sourceTree = ""; }; - 02DE3C481D25952A002B58E2 /* Regex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Regex.swift; sourceTree = ""; }; - 02DE3C4B1D259628002B58E2 /* ButtonWithTitleAdjustment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonWithTitleAdjustment.swift; sourceTree = ""; }; - 02DE3C4C1D259628002B58E2 /* LabelWithTitleAdjustment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelWithTitleAdjustment.swift; sourceTree = ""; }; - 02DE3C4F1D259966002B58E2 /* SetRootViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetRootViewController.swift; sourceTree = ""; }; - 02DE3C541D259FA9002B58E2 /* DimmingButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DimmingButton.swift; sourceTree = ""; }; - 02DE3C561D25A24B002B58E2 /* DecoratingTextFieldDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecoratingTextFieldDelegate.swift; sourceTree = ""; }; - 02DF8ECA1E6464AB009AB29C /* ReactiveLifetimeProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveLifetimeProvider.swift; sourceTree = ""; }; - 02E626081D340F0C0041E512 /* LoadingState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingState.swift; sourceTree = ""; }; - 35996E0E1F55C3E1004D6AC0 /* FoundationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoundationExtensions.swift; sourceTree = ""; }; - DFA57F0521E7A64200467647 /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; - F40C574121F909290006FB3F /* ReactiveCocoaExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveCocoaExtensions.swift; sourceTree = ""; }; - F40C574321F909A60006FB3F /* ActionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionProtocol.swift; sourceTree = ""; }; - F40C574521F90BD10006FB3F /* TypedSerialDisposable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedSerialDisposable.swift; sourceTree = ""; }; - F40C574721F9182B0006FB3F /* TransferState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferState.swift; sourceTree = ""; }; - F45268382355E83200DA9B9B /* CoalescingActionSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoalescingActionSpec.swift; sourceTree = ""; }; - F463DF9F222D81D50031AAA5 /* FueledUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FueledUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F463DFAC222D81E10031AAA5 /* FueledUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FueledUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F463DFB9222D81F10031AAA5 /* FueledUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FueledUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F463E00C222D836E0031AAA5 /* ReactiveCocoa.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactiveCocoa.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F463E00E222D836E0031AAA5 /* ReactiveSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactiveSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F463E012222D837D0031AAA5 /* ReactiveCocoa.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactiveCocoa.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F463E014222D837D0031AAA5 /* ReactiveMapKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactiveMapKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F463E016222D837D0031AAA5 /* ReactiveSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactiveSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F463E018222D83890031AAA5 /* ReactiveCocoa.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactiveCocoa.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F463E01A222D83890031AAA5 /* ReactiveSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactiveSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F464775C21FA3FA5005F8B7E /* InputCoalescingAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputCoalescingAction.swift; sourceTree = ""; }; - F4A619541F47263100777BB2 /* CollectionExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; sourceTree = ""; }; - F4CBDF152200A72400DF24DD /* FueledUtilsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FueledUtilsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F4CBDF172200A72400DF24DD /* ReactiveSwiftExtensionsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactiveSwiftExtensionsSpec.swift; sourceTree = ""; }; - F4CBDF192200A72400DF24DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F4CBDF1D2200A99A00DF24DD /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F4CBDF1F2200A99A00DF24DD /* Quick.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Quick.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F4CBDF212200A99A00DF24DD /* ReactiveCocoa.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactiveCocoa.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F4CBDF232200A99A00DF24DD /* ReactiveSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactiveSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 02DE3C1A1D258C79002B58E2 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 02DE3C2A1D258E02002B58E2 /* ReactiveCocoa.framework in Frameworks */, - 024E88731DA1597900826334 /* ReactiveSwift.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DF9C222D81D50031AAA5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F463E019222D83890031AAA5 /* ReactiveCocoa.framework in Frameworks */, - F463E01B222D83890031AAA5 /* ReactiveSwift.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DFA9222D81E10031AAA5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F463E013222D837D0031AAA5 /* ReactiveCocoa.framework in Frameworks */, - F463E017222D837D0031AAA5 /* ReactiveSwift.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DFB6222D81F10031AAA5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F463E00D222D836E0031AAA5 /* ReactiveCocoa.framework in Frameworks */, - F463E00F222D836E0031AAA5 /* ReactiveSwift.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4CBDF122200A72400DF24DD /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F4CBDF272200A99A00DF24DD /* FueledUtils.framework in Frameworks */, - F4CBDF1E2200A99A00DF24DD /* Nimble.framework in Frameworks */, - F4CBDF202200A99A00DF24DD /* Quick.framework in Frameworks */, - F4CBDF222200A99A00DF24DD /* ReactiveCocoa.framework in Frameworks */, - F4CBDF242200A99A00DF24DD /* ReactiveSwift.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 024E88711DA1597800826334 /* Frameworks */ = { - isa = PBXGroup; - children = ( - F463E018222D83890031AAA5 /* ReactiveCocoa.framework */, - F463E01A222D83890031AAA5 /* ReactiveSwift.framework */, - F463E012222D837D0031AAA5 /* ReactiveCocoa.framework */, - F463E014222D837D0031AAA5 /* ReactiveMapKit.framework */, - F463E016222D837D0031AAA5 /* ReactiveSwift.framework */, - F463E00C222D836E0031AAA5 /* ReactiveCocoa.framework */, - F463E00E222D836E0031AAA5 /* ReactiveSwift.framework */, - F4CBDF1D2200A99A00DF24DD /* Nimble.framework */, - F4CBDF1F2200A99A00DF24DD /* Quick.framework */, - F4CBDF212200A99A00DF24DD /* ReactiveCocoa.framework */, - F4CBDF232200A99A00DF24DD /* ReactiveSwift.framework */, - 024E88721DA1597900826334 /* ReactiveSwift.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 02DE3C141D258C79002B58E2 = { - isa = PBXGroup; - children = ( - 02DE3C201D258C79002B58E2 /* FueledUtils */, - F4CBDF162200A72400DF24DD /* FueledUtilsTests */, - 02DE3C1F1D258C79002B58E2 /* Products */, - 02DE3C291D258E02002B58E2 /* ReactiveCocoa.framework */, - 024E88711DA1597800826334 /* Frameworks */, - ); - sourceTree = ""; - }; - 02DE3C1F1D258C79002B58E2 /* Products */ = { - isa = PBXGroup; - children = ( - 02DE3C1E1D258C79002B58E2 /* FueledUtils.framework */, - F4CBDF152200A72400DF24DD /* FueledUtilsTests.xctest */, - F463DF9F222D81D50031AAA5 /* FueledUtils.framework */, - F463DFAC222D81E10031AAA5 /* FueledUtils.framework */, - F463DFB9222D81F10031AAA5 /* FueledUtils.framework */, - ); - name = Products; - sourceTree = ""; - }; - 02DE3C201D258C79002B58E2 /* FueledUtils */ = { - isa = PBXGroup; - children = ( - 02DE3C211D258C79002B58E2 /* FueledUtils.h */, - 02DE3C231D258C79002B58E2 /* Info.plist */, - F40C574321F909A60006FB3F /* ActionProtocol.swift */, - 02DE3C4B1D259628002B58E2 /* ButtonWithTitleAdjustment.swift */, - F464775C21FA3FA5005F8B7E /* InputCoalescingAction.swift */, - F4A619541F47263100777BB2 /* CollectionExtensions.swift */, - 02DE3C561D25A24B002B58E2 /* DecoratingTextFieldDelegate.swift */, - 02DE3C541D259FA9002B58E2 /* DimmingButton.swift */, - F40C574721F9182B0006FB3F /* TransferState.swift */, - 35996E0E1F55C3E1004D6AC0 /* FoundationExtensions.swift */, - 02DE3C451D25933E002B58E2 /* HairlineView.swift */, - 02DE3C431D25928F002B58E2 /* KeyboardInsetHelper.swift */, - 02DE3C4C1D259628002B58E2 /* LabelWithTitleAdjustment.swift */, - 02E626081D340F0C0041E512 /* LoadingState.swift */, - 02DE3C471D25952A002B58E2 /* NSDecimalNumberOperators.swift */, - F40C574121F909290006FB3F /* ReactiveCocoaExtensions.swift */, - 02DF8ECA1E6464AB009AB29C /* ReactiveLifetimeProvider.swift */, - 02DE3C351D258FD1002B58E2 /* ReactiveSwiftExtensions.swift */, - 02DE3C481D25952A002B58E2 /* Regex.swift */, - 0238E9341E69B53200BF26D4 /* ScrollViewPage.swift */, - 02DE3C371D258FD1002B58E2 /* SequenceExtensions.swift */, - 02DE3C4F1D259966002B58E2 /* SetRootViewController.swift */, - 02DE3C381D258FD1002B58E2 /* SignalingAlert.swift */, - 02DE3C3B1D258FD1002B58E2 /* StringExtensions.swift */, - F40C574521F90BD10006FB3F /* TypedSerialDisposable.swift */, - 02DE3C3A1D258FD1002B58E2 /* UIExtensions.swift */, - DFA57F0521E7A64200467647 /* GradientView.swift */, - ); - path = FueledUtils; - sourceTree = ""; - }; - F4CBDF162200A72400DF24DD /* FueledUtilsTests */ = { - isa = PBXGroup; - children = ( - F45268382355E83200DA9B9B /* CoalescingActionSpec.swift */, - F4CBDF172200A72400DF24DD /* ReactiveSwiftExtensionsSpec.swift */, - F4CBDF192200A72400DF24DD /* Info.plist */, - ); - path = FueledUtilsTests; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 02DE3C1B1D258C79002B58E2 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 02DE3C221D258C79002B58E2 /* FueledUtils.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DF9A222D81D50031AAA5 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DFA7222D81E10031AAA5 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DFB4222D81F10031AAA5 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 02DE3C1D1D258C79002B58E2 /* FueledUtils-iOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 02DE3C261D258C79002B58E2 /* Build configuration list for PBXNativeTarget "FueledUtils-iOS" */; - buildPhases = ( - 35A83B012267C86100306B26 /* swiftlint */, - 02DE3C191D258C79002B58E2 /* Sources */, - 02DE3C1A1D258C79002B58E2 /* Frameworks */, - 02DE3C1B1D258C79002B58E2 /* Headers */, - 02DE3C1C1D258C79002B58E2 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "FueledUtils-iOS"; - productName = FueledUtils; - productReference = 02DE3C1E1D258C79002B58E2 /* FueledUtils.framework */; - productType = "com.apple.product-type.framework"; - }; - F463DF9E222D81D50031AAA5 /* FueledUtils-watchOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = F463DFA4222D81D50031AAA5 /* Build configuration list for PBXNativeTarget "FueledUtils-watchOS" */; - buildPhases = ( - 35C495542267E3B000CFC908 /* swiftlint */, - F463DF9A222D81D50031AAA5 /* Headers */, - F463DF9B222D81D50031AAA5 /* Sources */, - F463DF9C222D81D50031AAA5 /* Frameworks */, - F463DF9D222D81D50031AAA5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "FueledUtils-watchOS"; - productName = "FueledUtils-watchOS"; - productReference = F463DF9F222D81D50031AAA5 /* FueledUtils.framework */; - productType = "com.apple.product-type.framework"; - }; - F463DFAB222D81E10031AAA5 /* FueledUtils-tvOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = F463DFB1222D81E10031AAA5 /* Build configuration list for PBXNativeTarget "FueledUtils-tvOS" */; - buildPhases = ( - 35C495532267E39500CFC908 /* swiftlint */, - F463DFA7222D81E10031AAA5 /* Headers */, - F463DFA8222D81E10031AAA5 /* Sources */, - F463DFA9222D81E10031AAA5 /* Frameworks */, - F463DFAA222D81E10031AAA5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "FueledUtils-tvOS"; - productName = "FueledUtils-tvOS"; - productReference = F463DFAC222D81E10031AAA5 /* FueledUtils.framework */; - productType = "com.apple.product-type.framework"; - }; - F463DFB8222D81F10031AAA5 /* FueledUtils-macOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = F463DFBE222D81F10031AAA5 /* Build configuration list for PBXNativeTarget "FueledUtils-macOS" */; - buildPhases = ( - 35C495522267E37800CFC908 /* swiftlint */, - F463DFB4222D81F10031AAA5 /* Headers */, - F463DFB5222D81F10031AAA5 /* Sources */, - F463DFB6222D81F10031AAA5 /* Frameworks */, - F463DFB7222D81F10031AAA5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "FueledUtils-macOS"; - productName = "FueledUtils-macOS"; - productReference = F463DFB9222D81F10031AAA5 /* FueledUtils.framework */; - productType = "com.apple.product-type.framework"; - }; - F4CBDF142200A72400DF24DD /* FueledUtilsTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F4CBDF1C2200A72400DF24DD /* Build configuration list for PBXNativeTarget "FueledUtilsTests" */; - buildPhases = ( - F4CBDF112200A72400DF24DD /* Sources */, - F4CBDF122200A72400DF24DD /* Frameworks */, - F4CBDF132200A72400DF24DD /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = FueledUtilsTests; - productName = FueledUtilsTest; - productReference = F4CBDF152200A72400DF24DD /* FueledUtilsTests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 02DE3C151D258C79002B58E2 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1010; - LastUpgradeCheck = 1110; - ORGANIZATIONNAME = Fueled; - TargetAttributes = { - 02DE3C1D1D258C79002B58E2 = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1020; - }; - F463DF9E222D81D50031AAA5 = { - CreatedOnToolsVersion = 10.1; - ProvisioningStyle = Automatic; - }; - F463DFAB222D81E10031AAA5 = { - CreatedOnToolsVersion = 10.1; - ProvisioningStyle = Automatic; - }; - F463DFB8222D81F10031AAA5 = { - CreatedOnToolsVersion = 10.1; - ProvisioningStyle = Automatic; - }; - F4CBDF142200A72400DF24DD = { - CreatedOnToolsVersion = 10.1; - ProvisioningStyle = Automatic; - }; - }; - }; - buildConfigurationList = 02DE3C181D258C79002B58E2 /* Build configuration list for PBXProject "FueledUtils" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 02DE3C141D258C79002B58E2; - productRefGroup = 02DE3C1F1D258C79002B58E2 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - F463DFB8222D81F10031AAA5 /* FueledUtils-macOS */, - 02DE3C1D1D258C79002B58E2 /* FueledUtils-iOS */, - F463DFAB222D81E10031AAA5 /* FueledUtils-tvOS */, - F463DF9E222D81D50031AAA5 /* FueledUtils-watchOS */, - F4CBDF142200A72400DF24DD /* FueledUtilsTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 02DE3C1C1D258C79002B58E2 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DF9D222D81D50031AAA5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DFAA222D81E10031AAA5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DFB7222D81F10031AAA5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4CBDF132200A72400DF24DD /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 35A83B012267C86100306B26 /* swiftlint */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = swiftlint; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/zsh; - shellScript = "export SWIFTLINT_BIN=`whence swiftlint`\nif [[ -n $SWIFTLINT_BIN ]]\nthen\n $SWIFTLINT_BIN\nfi\n"; - showEnvVarsInLog = 0; - }; - 35C495522267E37800CFC908 /* swiftlint */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = swiftlint; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export SWIFTLINT_BIN=`whence swiftlint`\nif [[ -n $SWIFTLINT_BIN ]]\nthen\n$SWIFTLINT_BIN\nfi\n"; - showEnvVarsInLog = 0; - }; - 35C495532267E39500CFC908 /* swiftlint */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = swiftlint; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export SWIFTLINT_BIN=`whence swiftlint`\nif [[ -n $SWIFTLINT_BIN ]]\nthen\n$SWIFTLINT_BIN\nfi\n"; - showEnvVarsInLog = 0; - }; - 35C495542267E3B000CFC908 /* swiftlint */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = swiftlint; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export SWIFTLINT_BIN=`whence swiftlint`\nif [[ -n $SWIFTLINT_BIN ]]\nthen\n$SWIFTLINT_BIN\nfi\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 02DE3C191D258C79002B58E2 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 02DE3C4A1D25952A002B58E2 /* Regex.swift in Sources */, - 02DE3C4D1D259628002B58E2 /* ButtonWithTitleAdjustment.swift in Sources */, - DFA57F0621E7A64200467647 /* GradientView.swift in Sources */, - 02DE3C421D258FD1002B58E2 /* StringExtensions.swift in Sources */, - 02DE3C501D259966002B58E2 /* SetRootViewController.swift in Sources */, - 02DE3C441D25928F002B58E2 /* KeyboardInsetHelper.swift in Sources */, - 02DE3C3C1D258FD1002B58E2 /* ReactiveSwiftExtensions.swift in Sources */, - 02DF8ECB1E6464AB009AB29C /* ReactiveLifetimeProvider.swift in Sources */, - F463E020222D83C40031AAA5 /* UIExtensions.swift in Sources */, - 0238E9351E69B53200BF26D4 /* ScrollViewPage.swift in Sources */, - 35996E0F1F55C3E1004D6AC0 /* FoundationExtensions.swift in Sources */, - 02DE3C4E1D259628002B58E2 /* LabelWithTitleAdjustment.swift in Sources */, - 02DE3C461D25933E002B58E2 /* HairlineView.swift in Sources */, - 02DE3C571D25A24B002B58E2 /* DecoratingTextFieldDelegate.swift in Sources */, - F40C574221F9092A0006FB3F /* ReactiveCocoaExtensions.swift in Sources */, - 02DE3C491D25952A002B58E2 /* NSDecimalNumberOperators.swift in Sources */, - F4A619551F47263100777BB2 /* CollectionExtensions.swift in Sources */, - F40C574421F909A60006FB3F /* ActionProtocol.swift in Sources */, - 02E626091D340F0C0041E512 /* LoadingState.swift in Sources */, - 02DE3C3E1D258FD1002B58E2 /* SequenceExtensions.swift in Sources */, - 02DE3C3F1D258FD1002B58E2 /* SignalingAlert.swift in Sources */, - F464775D21FA3FA5005F8B7E /* InputCoalescingAction.swift in Sources */, - F40C574621F90BD10006FB3F /* TypedSerialDisposable.swift in Sources */, - 02DE3C551D259FA9002B58E2 /* DimmingButton.swift in Sources */, - F40C574821F9182B0006FB3F /* TransferState.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DF9B222D81D50031AAA5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F463DFE5222D83060031AAA5 /* LoadingState.swift in Sources */, - F463DFDA222D83060031AAA5 /* ActionProtocol.swift in Sources */, - F463DFE0222D83060031AAA5 /* TransferState.swift in Sources */, - F463DFDD222D83060031AAA5 /* CollectionExtensions.swift in Sources */, - F463DFEF222D83060031AAA5 /* StringExtensions.swift in Sources */, - F463DFDC222D83060031AAA5 /* InputCoalescingAction.swift in Sources */, - F463DFEA222D83060031AAA5 /* Regex.swift in Sources */, - F463DFE1222D83060031AAA5 /* FoundationExtensions.swift in Sources */, - F463DFE9222D83060031AAA5 /* ReactiveSwiftExtensions.swift in Sources */, - F463DFE8222D83060031AAA5 /* ReactiveLifetimeProvider.swift in Sources */, - F463DFF0222D83060031AAA5 /* TypedSerialDisposable.swift in Sources */, - F463DFE6222D83060031AAA5 /* NSDecimalNumberOperators.swift in Sources */, - F463DFEC222D83060031AAA5 /* SequenceExtensions.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DFA8222D81E10031AAA5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F463DFCC222D83060031AAA5 /* LoadingState.swift in Sources */, - F463DFC1222D83060031AAA5 /* ActionProtocol.swift in Sources */, - F463DFC7222D83060031AAA5 /* TransferState.swift in Sources */, - 99126766225740DB00D39A08 /* UIExtensions.swift in Sources */, - F463DFC4222D83060031AAA5 /* CollectionExtensions.swift in Sources */, - F463DFCB222D83060031AAA5 /* LabelWithTitleAdjustment.swift in Sources */, - F463DFD4222D83060031AAA5 /* SetRootViewController.swift in Sources */, - F463DFD6222D83060031AAA5 /* StringExtensions.swift in Sources */, - F463DFC3222D83060031AAA5 /* InputCoalescingAction.swift in Sources */, - F463DFD1222D83060031AAA5 /* Regex.swift in Sources */, - F463DFC8222D83060031AAA5 /* FoundationExtensions.swift in Sources */, - F463DFC6222D83060031AAA5 /* DimmingButton.swift in Sources */, - F463DFD0222D83060031AAA5 /* ReactiveSwiftExtensions.swift in Sources */, - F463DFCF222D83060031AAA5 /* ReactiveLifetimeProvider.swift in Sources */, - 991267652257405F00D39A08 /* ReactiveCocoaExtensions.swift in Sources */, - F463DFD7222D83060031AAA5 /* TypedSerialDisposable.swift in Sources */, - F463DFC2222D83060031AAA5 /* ButtonWithTitleAdjustment.swift in Sources */, - F463DFC5222D83060031AAA5 /* DecoratingTextFieldDelegate.swift in Sources */, - F463DFCD222D83060031AAA5 /* NSDecimalNumberOperators.swift in Sources */, - F463DFD3222D83060031AAA5 /* SequenceExtensions.swift in Sources */, - F463DFD5222D83060031AAA5 /* SignalingAlert.swift in Sources */, - F463DFD9222D83060031AAA5 /* GradientView.swift in Sources */, - F463DFD2222D83060031AAA5 /* ScrollViewPage.swift in Sources */, - F463DFC9222D83060031AAA5 /* HairlineView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F463DFB5222D81F10031AAA5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F463DFFE222D83070031AAA5 /* LoadingState.swift in Sources */, - F463DFF3222D83070031AAA5 /* ActionProtocol.swift in Sources */, - F463DFF9222D83070031AAA5 /* TransferState.swift in Sources */, - F463DFF6222D83070031AAA5 /* CollectionExtensions.swift in Sources */, - F463E008222D83070031AAA5 /* StringExtensions.swift in Sources */, - F463DFF5222D83070031AAA5 /* InputCoalescingAction.swift in Sources */, - F463E003222D83070031AAA5 /* Regex.swift in Sources */, - F463DFFA222D83070031AAA5 /* FoundationExtensions.swift in Sources */, - F463E022222D84620031AAA5 /* ReactiveLifetimeProvider.swift in Sources */, - F463E002222D83070031AAA5 /* ReactiveSwiftExtensions.swift in Sources */, - F463E009222D83070031AAA5 /* TypedSerialDisposable.swift in Sources */, - F463DFFF222D83070031AAA5 /* NSDecimalNumberOperators.swift in Sources */, - F463E005222D83070031AAA5 /* SequenceExtensions.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F4CBDF112200A72400DF24DD /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F45268392355E83200DA9B9B /* CoalescingActionSpec.swift in Sources */, - F4CBDF182200A72400DF24DD /* ReactiveSwiftExtensionsSpec.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 02DE3C241D258C79002B58E2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MACOSX_DEPLOYMENT_TARGET = 10.13; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 11.0; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 4.0; - }; - name = Debug; - }; - 02DE3C251D258C79002B58E2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MACOSX_DEPLOYMENT_TARGET = 10.13; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 11.0; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 4.0; - }; - name = Release; - }; - 02DE3C271D258C79002B58E2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_SEARCH_PATHS = ( - "$(DEVELOPER_FRAMEWORKS_DIR)", - "$(inherited)", - ); - INFOPLIST_FILE = FueledUtils/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 2.0.3; - PRODUCT_BUNDLE_IDENTIFIER = com.fueled.FueledUtils; - PRODUCT_NAME = FueledUtils; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 02DE3C281D258C79002B58E2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_SEARCH_PATHS = ( - "$(DEVELOPER_FRAMEWORKS_DIR)", - "$(inherited)", - ); - INFOPLIST_FILE = FueledUtils/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 2.0.3; - PRODUCT_BUNDLE_IDENTIFIER = com.fueled.FueledUtils; - PRODUCT_NAME = FueledUtils; - SKIP_INSTALL = YES; - }; - name = Release; - }; - F463DFA5222D81D50031AAA5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = FueledUtils/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 2.0.3; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.FueledUtils-watchOS"; - PRODUCT_NAME = FueledUtils; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - TARGETED_DEVICE_FAMILY = 4; - }; - name = Debug; - }; - F463DFA6222D81D50031AAA5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = FueledUtils/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 2.0.3; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.FueledUtils-watchOS"; - PRODUCT_NAME = FueledUtils; - SDKROOT = watchos; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = 4; - }; - name = Release; - }; - F463DFB2222D81E10031AAA5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = FueledUtils/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 2.0.3; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.FueledUtils-tvOS"; - PRODUCT_NAME = FueledUtils; - SDKROOT = appletvos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - TARGETED_DEVICE_FAMILY = 3; - }; - name = Debug; - }; - F463DFB3222D81E10031AAA5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = FueledUtils/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 2.0.3; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.FueledUtils-tvOS"; - PRODUCT_NAME = FueledUtils; - SDKROOT = appletvos; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = 3; - }; - name = Release; - }; - F463DFBF222D81F10031AAA5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "-"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = FueledUtils/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 2.0.3; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.FueledUtils-macOS"; - PRODUCT_NAME = FueledUtils; - SDKROOT = macosx; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - }; - name = Debug; - }; - F463DFC0222D81F10031AAA5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "-"; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = FueledUtils/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 2.0.3; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.FueledUtils-macOS"; - PRODUCT_NAME = FueledUtils; - SDKROOT = macosx; - SKIP_INSTALL = YES; - }; - name = Release; - }; - F4CBDF1A2200A72400DF24DD /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = FueledUtilsTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.fueled.FueledUtilsTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - F4CBDF1B2200A72400DF24DD /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = FueledUtilsTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.fueled.FueledUtilsTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 02DE3C181D258C79002B58E2 /* Build configuration list for PBXProject "FueledUtils" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 02DE3C241D258C79002B58E2 /* Debug */, - 02DE3C251D258C79002B58E2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 02DE3C261D258C79002B58E2 /* Build configuration list for PBXNativeTarget "FueledUtils-iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 02DE3C271D258C79002B58E2 /* Debug */, - 02DE3C281D258C79002B58E2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F463DFA4222D81D50031AAA5 /* Build configuration list for PBXNativeTarget "FueledUtils-watchOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F463DFA5222D81D50031AAA5 /* Debug */, - F463DFA6222D81D50031AAA5 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F463DFB1222D81E10031AAA5 /* Build configuration list for PBXNativeTarget "FueledUtils-tvOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F463DFB2222D81E10031AAA5 /* Debug */, - F463DFB3222D81E10031AAA5 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F463DFBE222D81F10031AAA5 /* Build configuration list for PBXNativeTarget "FueledUtils-macOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F463DFBF222D81F10031AAA5 /* Debug */, - F463DFC0222D81F10031AAA5 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - F4CBDF1C2200A72400DF24DD /* Build configuration list for PBXNativeTarget "FueledUtilsTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F4CBDF1A2200A72400DF24DD /* Debug */, - F4CBDF1B2200A72400DF24DD /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 02DE3C151D258C79002B58E2 /* Project object */; -} diff --git a/FueledUtils.xcworkspace/contents.xcworkspacedata b/FueledUtils.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index cd4892b8..00000000 --- a/FueledUtils.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/FueledUtils.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/FueledUtils.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/FueledUtils.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/FueledUtils/Combine/Action.swift b/FueledUtils/Combine/Action.swift new file mode 100644 index 00000000..ed38326c --- /dev/null +++ b/FueledUtils/Combine/Action.swift @@ -0,0 +1,200 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public final class Action { + @Published public private(set) var isExecuting: Bool = false + @Published public private(set) var isEnabled: Bool = false + + public let values: AnyPublisher + public let errors: AnyPublisher + + fileprivate let execute: (Action, Input) -> AnyPublisher> + + fileprivate var cancellables = Set([]) + + public convenience init( + execute: @escaping (Input) -> ExecutePublisher + ) where ExecutePublisher.Output == Output, ExecutePublisher.Failure == Failure { + self.init(enabledIf: Just(true), execute: execute) + } + + public init( + enabledIf isEnabled: EnabledIfPublisher, + execute: @escaping (Input) -> ExecutePublisher + ) where + EnabledIfPublisher.Output == Bool, + EnabledIfPublisher.Failure == Never, + ExecutePublisher.Output == Output, + ExecutePublisher.Failure == Failure + { + let values = PassthroughSubject() + let errors = PassthroughSubject() + + self.values = values.eraseToAnyPublisher() + self.errors = errors.eraseToAnyPublisher() + + let isExecutingLock = Lock() + self.execute = { action, input -> AnyPublisher> in + isExecutingLock.lock() + + if !action.isEnabled || action.isExecuting { + isExecutingLock.unlock() + return Fail(error: .disabled) + .eraseToAnyPublisher() + } + + action.isExecuting = true + isExecutingLock.unlock() + return execute(input) + .handleEvents( + receiveOutput: { value in + values.send(value) + }, + receiveCompletion: { completion in + switch completion { + case .finished: + break + case .failure(let error): + errors.send(error) + } + }, + receiveTermination: { [weak action] in + isExecutingLock.lock() + action?.isExecuting = false + isExecutingLock.unlock() + } + ) + .mapError { .failure($0) } + .eraseToAnyPublisher() + } + + Publishers.CombineLatest( + isEnabled, + self.$isExecuting + ) + .map { $0 && !$1 } + .assign(to: \.isEnabled, withoutRetaining: self) + .store(in: &self.cancellables) + } + + fileprivate init( + enabledIf isEnabled: EnabledIfPublisher, + values: AnyPublisher, + errors: AnyPublisher, + execute: @escaping (Action, Input) -> AnyPublisher> + ) where + EnabledIfPublisher.Output == Bool, + EnabledIfPublisher.Failure == Never + { + self.values = values + self.errors = errors + self.execute = execute + + Publishers.CombineLatest( + isEnabled, + self.$isExecuting + ) + .map { $0 && !$1 } + .assign(to: \.isEnabled, withoutRetaining: self) + .store(in: &self.cancellables) + } + + public func apply(_ input: Input) -> AnyPublisher> { + self.execute(self, input) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher where Failure: ActionErrorProtocol { + public func unwrappingActionError() -> AnyPublisher { + self.catch { actionError -> AnyPublisher in + if let innerError = actionError.innerError { + return Fail(error: innerError).eraseToAnyPublisher() + } + return Empty(completeImmediately: false).eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Action { + public static func constant(_ value: Output) -> Action { + self.constant(inputType: Input.self, value: value) + } + + public static func constant(inputType: Input.Type, value: Output) -> Action { + Action { _ in Just(value).setFailureType(to: Failure.self) } + } + + // Please note that the actions created with the `mapXxx` family are interweaved together - starting one + // will update the other, and vice versa. + // For example, on use case is to type-erase an Action. + @available(*, deprecated, message: "Use `AnyAction` instead") + public func mapInput(_ mapper: @escaping (NewInput) -> Input) -> Action { + self.mapAll( + mapInput: mapper, + map: { $0 }, + mapError: { $0 } + ) + } + + @available(*, deprecated, message: "Use `AnyAction` instead") + public func map(_ mapper: @escaping (Output) -> NewOutput) -> Action { + self.mapAll( + mapInput: { $0 }, + map: mapper, + mapError: { $0 } + ) + } + + @available(*, deprecated, message: "Use `AnyAction` instead") + public func mapError(_ mapper: @escaping (Failure) -> NewFailure) -> Action { + self.mapAll( + mapInput: { $0 }, + map: { $0 }, + mapError: mapper + ) + } + + @available(*, deprecated, message: "Use `AnyAction` instead") + public func mapAll( + mapInput: @escaping (NewInput) -> (Input), + map: @escaping (Output) -> (NewOutput), + mapError: @escaping (Failure) -> (NewFailure) + ) -> Action { + let action = Action( + enabledIf: self.$isEnabled, + values: self.values.map(map).eraseToAnyPublisher(), + errors: self.errors.map(mapError).eraseToAnyPublisher() + ) { (action, input) -> AnyPublisher> in + return self.execute(self, mapInput(input)) + .map(map) + .mapError { $0.map { mapError($0) } } + .eraseToAnyPublisher() + } + + self.$isExecuting + .assign(to: \.isExecuting, withoutRetaining: action) + .store(in: &action.cancellables) + + return action + } +} + +#endif diff --git a/FueledUtils/Combine/ActionError.swift b/FueledUtils/Combine/ActionError.swift new file mode 100644 index 00000000..f1cc075e --- /dev/null +++ b/FueledUtils/Combine/ActionError.swift @@ -0,0 +1,39 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public enum ActionError: Swift.Error { + case disabled + case failure(Error) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension ActionError: ActionErrorProtocol { + public var innerError: Error? { + if case .failure(let error) = self { + return error + } + return nil + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension ActionError { + public func map(_ mapper: (InnerError) -> NewError) -> ActionError { + if let innerError = self.innerError { + return .failure(mapper(innerError)) + } + return .disabled + } +} diff --git a/FueledUtils/Combine/ActionProtocol.swift b/FueledUtils/Combine/ActionProtocol.swift new file mode 100644 index 00000000..9c047963 --- /dev/null +++ b/FueledUtils/Combine/ActionProtocol.swift @@ -0,0 +1,109 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public protocol ActionProtocol { + /// + /// The type of the values used as inputs to the action. + /// + associatedtype Input + /// + /// + /// The type of the values output from the action. + /// + associatedtype Output + /// The type of errors emitted by the action. + /// + associatedtype Failure: Swift.Error + /// + /// The type of errors emitted when applying the action. + /// + associatedtype ApplyFailure + + associatedtype IsExecutingPublisher: Publisher where IsExecutingPublisher.Output == Bool, IsExecutingPublisher.Failure == Never + associatedtype IsEnabledPublisher: Publisher where IsEnabledPublisher.Output == Bool, IsEnabledPublisher.Failure == Never + + associatedtype ValuesPublisher: Publisher where ValuesPublisher.Output == Output, ValuesPublisher.Failure == Never + associatedtype ErrorsPublisher: Publisher where ErrorsPublisher.Output == Failure, ErrorsPublisher.Failure == Never + + associatedtype ApplyPublisher: Publisher where ApplyPublisher.Output == Output, ApplyPublisher.Failure == ApplyFailure + + /// + /// Whether the action is currently executing. + /// + var isExecuting: Bool { get } + /// + /// Whether the action is currently enabled. + /// + var isEnabled: Bool { get } + /// + /// Whether the action is currently executing. + /// + var isExecutingPublisher: IsExecutingPublisher { get } + /// + /// Whether the action is currently enabled. + /// + var isEnabledPublisher: IsEnabledPublisher { get } + + var values: ValuesPublisher { get } + var errors: ErrorsPublisher { get } + + /// + /// Create a `SignalProducer` that would attempt to create and start a unit of work of + /// the `Action`. The `SignalProducer` would forward only events generated by the unit + /// of work it created. + /// + /// - Parameters: + /// - input: A value to be used to create the unit of work. + /// + /// - Returns: A producer that forwards events generated by its started unit of work, + /// or returns an appropriate `ApplyError` indicating the specific error + /// that happened. + /// + func apply(_ input: Input) -> ApplyPublisher +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Action: ActionProtocol { + public typealias ApplyFailure = ActionError + + public var isExecutingPublisher: AnyPublisher { + self.$isExecuting.eraseToAnyPublisher() + } + + public var isEnabledPublisher: AnyPublisher { + self.$isEnabled.eraseToAnyPublisher() + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension ActionProtocol where Input == Void { + /// + /// Create a `SignalProducer` that would attempt to create and start a unit of work of + /// the `Action`. The `SignalProducer` would forward only events generated by the unit + /// of work it created. + /// + /// - Returns: A producer that forwards events generated by its started unit of work, + /// or returns an appropriate `ApplyError` indicating the specific error + /// that happened. + /// + public func apply() -> ApplyPublisher { + return self.apply(()) + } +} + +#endif diff --git a/FueledUtils/Combine/AnyAction.swift b/FueledUtils/Combine/AnyAction.swift new file mode 100644 index 00000000..aaf67490 --- /dev/null +++ b/FueledUtils/Combine/AnyAction.swift @@ -0,0 +1,68 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +/// +/// A type-erased Action that allows to store any `ActionProtocol` +/// (loosing any type information at the same time) +/// +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public final class AnyAction: ActionProtocol { + public typealias Input = Any + public typealias Output = Any + public typealias Failure = Error + public typealias IsExecutingPublisher = AnyPublisher + public typealias IsEnabledPublisher = AnyPublisher + public typealias ValuesPublisher = AnyPublisher + public typealias ErrorsPublisher = AnyPublisher + public typealias ApplyPublisher = AnyPublisher + public typealias ApplyFailure = Error + + private let applyClosure: (Any) -> AnyPublisher + private var cancellables = Set() + + public init(_ action: Action) { + self.isEnabled = action.isEnabled + self.isExecuting = action.isExecuting + self.applyClosure = { action.apply($0 as! Action.Input).map { $0 }.mapError { $0 }.eraseToAnyPublisher() } + self.values = action.values.map { $0 }.eraseToAnyPublisher() + self.errors = action.errors.map { $0 }.eraseToAnyPublisher() + action.isEnabledPublisher.assign(to: \.isEnabled, withoutRetaining: self) + .store(in: &self.cancellables) + action.isExecutingPublisher.assign(to: \.isExecuting, withoutRetaining: self) + .store(in: &self.cancellables) + } + + @Published public private(set) var isEnabled: Bool + @Published public private(set) var isExecuting: Bool + + public var isEnabledPublisher: AnyPublisher { + self.$isEnabled.eraseToAnyPublisher() + } + + public var isExecutingPublisher: AnyPublisher { + self.$isExecuting.eraseToAnyPublisher() + } + + public let values: AnyPublisher + public let errors: AnyPublisher + + public func apply(_ input: Any) -> AnyPublisher { + self.applyClosure(input) + } +} + +#endif diff --git a/FueledUtils/Combine/AnyCurrentValuePublisher.swift b/FueledUtils/Combine/AnyCurrentValuePublisher.swift new file mode 100644 index 00000000..af94361b --- /dev/null +++ b/FueledUtils/Combine/AnyCurrentValuePublisher.swift @@ -0,0 +1,67 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +/// +/// A type-erasing current value publisher. +/// +/// Use an `AnyCurrentValuePublisher` to wrap an existing current value publisher whose details you don’t want to expose. +/// For example, this is useful if you want to use a `CurrentValueSubject` internally, but don't want to expose the setter/its send() method +/// +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public struct AnyCurrentValuePublisher: CurrentValuePublisher { + private let valueGetter: () -> Output + private let receiveSubcriberClosure: (AnySubscriber) -> Void + + public var value: Output { + self.valueGetter() + } + + public init(_ value: Output) { + self.valueGetter = { value } + self.receiveSubcriberClosure = { _ = $0.receive(value) } + } + + public init(_ publisher: CurrentValuePublisher) where CurrentValuePublisher.Output == Output, CurrentValuePublisher.Failure == Failure { + self.valueGetter = { publisher.value } + self.receiveSubcriberClosure = { publisher.receive(subscriber: $0) } + } + + public func receive(subscriber: Subscriber) where Subscriber.Input == Output, Subscriber.Failure == Failure { + self.receiveSubcriberClosure(subscriber.eraseToAnySubscriber()) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension CurrentValuePublisher { + public func eraseToAnyCurrentValuePublisher() -> AnyCurrentValuePublisher { + AnyCurrentValuePublisher(self) + } +} + +/// +/// A publisher that also stores the last value it sent +/// +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public protocol CurrentValuePublisher: Publisher { + var value: Output { get } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension CurrentValueSubject: CurrentValuePublisher { +} + +#endif diff --git a/FueledUtils/Combine/CoalescingAction.swift b/FueledUtils/Combine/CoalescingAction.swift new file mode 100644 index 00000000..3139b2f1 --- /dev/null +++ b/FueledUtils/Combine/CoalescingAction.swift @@ -0,0 +1,153 @@ +// Copyright © 2020 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +/// +/// Similar to `Action`, except if the action is already executing, subsequent `apply()` call will not fail, +/// and will be completed with the same output when the initial executing action completes. +/// Disposing any of the `AnyPublisher` returned by `apply()` will cancel the action. +/// +/// The `Input` is used when sending inputs to the **initial** `apply()` call - for subsequent +/// calls to `apply()` when the action is executing, the inputs will be ignored until +/// the action terminates. +/// +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public class CoalescingAction: ActionProtocol { + public typealias ApplyFailure = Failure + + private let action: Action + private var passthroughSubject: PassthroughSubject? + private var cancellableContainer: CancellableContainer? + + private class CancellableContainer { + private let cancellable: AnyCancellable + private let count = AtomicValue(0) + + init(_ cancellable: AnyCancellable) { + self.cancellable = cancellable + } + + func add(_ passthroughSubject: PassthroughSubject) -> ((Bool) -> Void) -> Void { + self.count.modify { $0 += 1 } + return { isFinal in + let isCancelled = self.count.modify { count -> Bool in + count -= 1 + let isCancelled = count == 0 + if isCancelled { + self.cancellable.cancel() + } + return isCancelled + } + isFinal(isCancelled) + } + } + } + + @Published public private(set) var isExecuting: Bool + @Published public private(set) var isEnabled: Bool + + public var isExecutingPublisher: AnyPublisher { + self.$isExecuting.eraseToAnyPublisher() + } + + public var isEnabledPublisher: AnyPublisher { + self.$isEnabled.eraseToAnyPublisher() + } + + public var values: AnyPublisher { + self.action.values + } + + public var errors: AnyPublisher { + self.action.errors + } + + private var cancellables = Set([]) + + /// + /// Initializes a `CoalescingAction`. + /// + /// When the `Action` is asked to start the execution with an input value, a unit of + /// work — represented by a `Publisher` — would be created by invoking + /// `execute` with the input value. + /// + /// - parameters: + /// - execute: A closure that produces a unit of work, as `Publisher`, to be + /// executed by the `Action`. + /// + public init( + execute: @escaping (Input) -> ExecutePublisher + ) where ExecutePublisher.Output == Output, ExecutePublisher.Failure == Failure { + self.action = Action(execute: execute) + self.isEnabled = self.action.isEnabled + self.isExecuting = self.action.isExecuting + self.action.isEnabledPublisher.assign(to: \.isEnabled, withoutRetaining: self) + .store(in: &self.cancellables) + self.action.isExecutingPublisher.assign(to: \.isExecuting, withoutRetaining: self) + .store(in: &self.cancellables) + } + + /// + /// Create a `AnyPublisher` that would attempt to create and start a unit of work of + /// the `Action`. The `AnyPublisher` would forward only events generated by the unit + /// of work it created. + /// + /// - Warning: Only the first call to `apply()` when the action's `isExecuting`'s `value` is `false` will be using its parameters. + /// Subsequent calls when the action is already executing will ignore the input. + /// + /// - Parameters: + /// - input: The initial input to use for the action. + /// + /// - Returns: A publisher that forwards events generated by its started unit of work. If the action was already executing, it will create a `AnyPublisher` + /// that will forward the events of the initially created `AnyPublisher`. + /// + public func apply(_ input: Input) -> AnyPublisher { + let passthroughSubject = PassthroughSubject() + var cancellable: AnyCancellable! + let cancellableContainer: CancellableContainer + let originalPassthroughSubject: PassthroughSubject + if let existingPassthroughSubject = self.passthroughSubject { + assert(self.isExecuting, "Action must be executing when `passthroughSubject` is non-nil") + originalPassthroughSubject = existingPassthroughSubject + cancellableContainer = self.cancellableContainer! + } else { + originalPassthroughSubject = PassthroughSubject() + self.passthroughSubject = originalPassthroughSubject + cancellable = self.action.apply(input) + .unwrappingActionError() + .subscribe(originalPassthroughSubject) + cancellableContainer = CancellableContainer(cancellable) + self.cancellableContainer = cancellableContainer + } + cancellable = originalPassthroughSubject.subscribe(passthroughSubject) + let onCompletion = cancellableContainer.add(passthroughSubject) + return passthroughSubject + .handleEvents( + receiveTermination: { [weak self] in + onCompletion() { isCancelled in + if isCancelled { + self?.passthroughSubject = nil + self?.cancellableContainer = nil + } + } + cancellable = nil + } + ) + .eraseToAnyPublisher() + } +} + +#endif diff --git a/FueledUtils/Combine/CombineExtensions+Cancellables.swift b/FueledUtils/Combine/CombineExtensions+Cancellables.swift new file mode 100644 index 00000000..0e73cd3c --- /dev/null +++ b/FueledUtils/Combine/CombineExtensions+Cancellables.swift @@ -0,0 +1,53 @@ +// Copyright © 2020 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +private var cancellablesKey: UInt8 = 0 + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension CombineExtensions { + public var cancellables: Set { + get { + self.cancellablesHelper.map { Set($0.map(\.cancellable)) } ?? { + let cancellables = Set() + self.cancellables = cancellables + return cancellables + }() + } + set { + self.cancellablesHelper = newValue.map { CancellableHolder($0) } + } + } + + private final class CancellableHolder: NSObject { + let cancellable: AnyCancellable + + init(_ cancellable: AnyCancellable) { + self.cancellable = cancellable + } + } + + private var cancellablesHelper: [CancellableHolder]? { + get { + objc_getAssociatedObject(self.base, &cancellablesKey) as? [CancellableHolder] + } + set { + objc_setAssociatedObject(self.base, &cancellablesKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY) + } + } +} + +#endif diff --git a/FueledUtils/Combine/CombineExtensions.swift b/FueledUtils/Combine/CombineExtensions.swift new file mode 100644 index 00000000..4bcc183e --- /dev/null +++ b/FueledUtils/Combine/CombineExtensions.swift @@ -0,0 +1,35 @@ +// Copyright © 2020 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public protocol CombineExtensionsProvider { +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public final class CombineExtensions { + public var base: Base + + fileprivate init(_ base: Base) { + self.base = base + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension CombineExtensionsProvider { + public var combineExtensions: CombineExtensions { + return CombineExtensions(self) + } +} diff --git a/FueledUtils/Combine/NSObject+CombineExtensions.swift b/FueledUtils/Combine/NSObject+CombineExtensions.swift new file mode 100644 index 00000000..2ab14c14 --- /dev/null +++ b/FueledUtils/Combine/NSObject+CombineExtensions.swift @@ -0,0 +1,19 @@ +// Copyright © 2020 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension NSObject: CombineExtensionsProvider { +} diff --git a/FueledUtils/Combine/ObservableObjectExtensions.swift b/FueledUtils/Combine/ObservableObjectExtensions.swift new file mode 100644 index 00000000..ad2fe6e0 --- /dev/null +++ b/FueledUtils/Combine/ObservableObjectExtensions.swift @@ -0,0 +1,98 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher { + // Perform a one-way link, where the receiver will listen for changes on the object and automatically trigger its `objectWillChange` publisher + public func link(to object: Object) { + object.objectWillChange.subscribe( + AnySubscriber( + receiveValue: { [weak self] _ in + self?.objectWillChange.send() + return .unlimited + } + ) + ) + } + + public func link(to objects: ObjectCollection) where ObjectCollection.Element: ObservableObject { + objects.forEach(self.link(to:)) + } + + public func link(to publisher: Publisher) where Publisher.Output: ObservableObject { + var cancellable: AnyCancellable? + publisher.subscribe( + AnySubscriber( + receiveValue: { [weak self] object in + _ = cancellable + cancellable = object.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() } + return .unlimited + } + ) + ) + } + + public func link(to publisher: Publisher) where Publisher.Output: OptionalProtocol, Publisher.Output.Wrapped: ObservableObject { + var cancellable: AnyCancellable? + publisher.subscribe( + AnySubscriber( + receiveValue: { [weak self] object in + _ = cancellable + cancellable = object.wrapped?.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() } + return .unlimited + } + ) + ) + } + + public func link(to publisher: Publisher) where Publisher.Output == ObjectCollection, ObjectCollection.Element: ObservableObject { + var cancellables = Set() + publisher.subscribe( + AnySubscriber( + receiveValue: { [weak self] objects in + cancellables = Set() + objects.forEach { object in + object.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() } + .store(in: &cancellables) + } + return .unlimited + } + ) + ) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension ObservableObject { + public var objectDidChange: AnyPublisher { + // The delay of 0.0 allows the will to transform into a Did, by waiting for exactly one run loop cycle + self.objectWillChange.delay(for: 0.0, scheduler: RunLoop.current).eraseToAnyPublisher() + } + + public var publisher: AnyPublisher { + self.objectDidChange.map { _ in self }.prepend(self).eraseToAnyPublisher() + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher where Output: Collection, Failure == Never, Output.Element: ObservableObject { + public func onAnyChanges() -> AnyPublisher<[Output.Element], Never> { + self.flatMap { Publishers.CombineLatestMany($0.map { $0.publisher }) }.eraseToAnyPublisher() + } +} + +#endif diff --git a/FueledUtils/Combine/OverridingAction.swift b/FueledUtils/Combine/OverridingAction.swift new file mode 100644 index 00000000..e6bb0386 --- /dev/null +++ b/FueledUtils/Combine/OverridingAction.swift @@ -0,0 +1,105 @@ +// Copyright © 2020 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +/// +/// Similar to `Action`, except if the action is already executing, subsequent `apply()` call will not fail, +/// and will be interrupt the previous apply(). +/// +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public class OverridingAction: ActionProtocol { + public typealias ApplyFailure = Failure + + private let action: Action + private var passthroughSubject: PassthroughSubject? + private var currentCancellable: AnyCancellable? + + @Published public private(set) var isExecuting: Bool + @Published public private(set) var isEnabled: Bool + + public var isExecutingPublisher: AnyPublisher { + self.$isExecuting.eraseToAnyPublisher() + } + + public var isEnabledPublisher: AnyPublisher { + self.$isEnabled.eraseToAnyPublisher() + } + + public var values: AnyPublisher { + self.action.values + } + + public var errors: AnyPublisher { + self.action.errors + } + + private var cancellables = Set([]) + + /// + /// Initializes a `CoalescingAction`. + /// + /// When the `Action` is asked to start the execution with an input value, a unit of + /// work — represented by a `Publisher` — would be created by invoking + /// `execute` with the input value. + /// + /// - parameters: + /// - execute: A closure that produces a unit of work, as `Publisher`, to be + /// executed by the `Action`. + /// + public init( + execute: @escaping (Input) -> ExecutePublisher + ) where ExecutePublisher.Output == Output, ExecutePublisher.Failure == Failure { + self.action = Action(execute: execute) + self.isEnabled = self.action.isEnabled + self.isExecuting = self.action.isExecuting + self.action.isEnabledPublisher.assign(to: \.isEnabled, withoutRetaining: self) + .store(in: &self.cancellables) + self.action.isExecutingPublisher.assign(to: \.isExecuting, withoutRetaining: self) + .store(in: &self.cancellables) + } + + /// + /// Create a `AnyPublisher` that would attempt to create and start a unit of work of + /// the `Action`. The `AnyPublisher` would forward only events generated by the unit + /// of work it created. + /// + /// - Warning: Only the first call to `apply()` when the action's `isExecuting`'s `value` is `false` will be using its parameters. + /// Subsequent calls when the action is already executing will ignore the input. + /// + /// - Parameters: + /// - input: The initial input to use for the action. + /// + /// - Returns: A publisher that forwards events generated by its started unit of work. If the action was already executing, it will create a `AnyPublisher` + /// that will forward the events of the initially created `AnyPublisher`. + /// + public func apply(_ input: Input) -> AnyPublisher { + let passthroughSubject = PassthroughSubject() + self.currentCancellable = nil + self.currentCancellable = self.action.apply(input) + .unwrappingActionError() + .subscribe(passthroughSubject) + self.passthroughSubject = passthroughSubject + return passthroughSubject + .handleEvents( + receiveTermination: { [weak self] in + self?.currentCancellable = nil + } + ) + .eraseToAnyPublisher() + } +} + +#endif diff --git a/FueledUtils/Combine/Published+PublisherInit.swift b/FueledUtils/Combine/Published+PublisherInit.swift new file mode 100644 index 00000000..1ad428e4 --- /dev/null +++ b/FueledUtils/Combine/Published+PublisherInit.swift @@ -0,0 +1,51 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Published { + /// This method exists as it's currently impossible to use the projectedValue of a `@Published` property without having initialize the whole object, + /// which is obviously not ideal if the projectedValue is required to initialize the whole object (basically a chicken and egg problem) + /// This resolves the issue by initializing the Published object separately from the object, and using the result + /// Non-compiling Example: + /// + /// final class TestPublished { + /// @Published private var foo: Int = 0 + /// let fooChanges: AnyPublisher + /// + /// init() { + /// self.fooChanges = AnyPublisher(self.$foo) // 'self' used in property access '$foo' before all stored properties are initialized + /// } + /// } + /// + /// Compiling Example (using below) + /// final class TestPublished { + /// @Published private var foo: Int + /// let fooChanges: AnyPublisher + /// + /// init() { + /// self.fooChanges = AnyPublisher(Published.initWithPublisher(&self._foo, initialValue: 0)) // Compiles + /// } + /// } + public static func initWithPublisher(_ property: inout Published, initialValue: Value) -> Published.Publisher { + var published = Published(initialValue: initialValue) + let publisher = published.projectedValue + property = published + return publisher + } +} + +#endif diff --git a/FueledUtils/Combine/Publisher+AdditionalHandleEvents.swift b/FueledUtils/Combine/Publisher+AdditionalHandleEvents.swift new file mode 100644 index 00000000..ad55aaf3 --- /dev/null +++ b/FueledUtils/Combine/Publisher+AdditionalHandleEvents.swift @@ -0,0 +1,106 @@ +// +// Publisher+AdditionalHandleEvents.swift +// FueledUtils +// +// Created by Stéphane Copin on 10/14/20. +// + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher { + /// - Parameters: + /// - receiveTermination: Sent when the publisher either a completion event or is cancelled. + /// - receiveResult: Sent when the publisher send values, or an error. + /// - Please refer to the documentation for + /// `Publisher.self.handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:)` + /// for more information about the other parameters. + public func handleEvents( + receiveSubscription: ((Subscription) -> Void)? = nil, + receiveOutput: ((Output) -> Void)? = nil, + receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, + receiveCancel: (() -> Void)? = nil, + receiveTermination: @escaping () -> Void, + receiveResult: ((Result) -> Void)? = nil, + receiveRequest: ((Subscribers.Demand) -> Void)? = nil + ) -> Publishers.HandleEvents { + self.extendedHandleEvents( + receiveSubscription: receiveSubscription, + receiveOutput: receiveOutput, + receiveCompletion: receiveCompletion, + receiveCancel: receiveCancel, + receiveTermination: receiveTermination, + receiveResult: receiveResult, + receiveRequest: receiveRequest + ) + } + + /// - Parameters: + /// - receiveTermination: Sent when the publisher either a completion event or is cancelled. + /// - receiveResult: Sent when the publisher send values, or an error. + /// - Please refer to the documentation for + /// `Publisher.self.handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:)` + /// for more information about the other parameters. + public func handleEvents( + receiveSubscription: ((Subscription) -> Void)? = nil, + receiveOutput: ((Output) -> Void)? = nil, + receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, + receiveCancel: (() -> Void)? = nil, + receiveTermination: (() -> Void)? = nil, + receiveResult: @escaping (Result) -> Void, + receiveRequest: ((Subscribers.Demand) -> Void)? = nil + ) -> Publishers.HandleEvents { + self.extendedHandleEvents( + receiveSubscription: receiveSubscription, + receiveOutput: receiveOutput, + receiveCompletion: receiveCompletion, + receiveCancel: receiveCancel, + receiveTermination: receiveTermination, + receiveResult: receiveResult, + receiveRequest: receiveRequest + ) + } + + private func extendedHandleEvents( + receiveSubscription: ((Subscription) -> Void)? = nil, + receiveOutput: ((Output) -> Void)? = nil, + receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, + receiveCancel: (() -> Void)? = nil, + receiveTermination: (() -> Void)? = nil, + receiveResult: ((Result) -> Void)?, + receiveRequest: ((Subscribers.Demand) -> Void)? = nil + ) -> Publishers.HandleEvents { + var hasTerminated = false + let receiveTermination = receiveTermination.map { receiveTermination in + { + if hasTerminated { + return + } + hasTerminated = true + receiveTermination() + } + } + return self.handleEvents( + receiveSubscription: receiveSubscription, + receiveOutput: { + receiveOutput?($0) + receiveResult?(.success($0)) + }, + receiveCompletion: { + receiveCompletion?($0) + if case .failure(let error) = $0 { + receiveResult?(.failure(error)) + } + receiveTermination?() + }, + receiveCancel: { + receiveCancel?() + receiveTermination?() + }, + receiveRequest: receiveRequest + ) + } +} + +#endif diff --git a/FueledUtils/Combine/Publisher+CombinePrevious.swift b/FueledUtils/Combine/Publisher+CombinePrevious.swift new file mode 100644 index 00000000..f49320c8 --- /dev/null +++ b/FueledUtils/Combine/Publisher+CombinePrevious.swift @@ -0,0 +1,47 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher { + public func combinePrevious() -> AnyPublisher<(previous: Output, current: Output), Failure> { + self.combinePreviousImplementation(nil) + } + + public func combinePrevious(_ initial: Output) -> AnyPublisher<(previous: Output, current: Output), Failure> { + self.combinePreviousImplementation(initial) + } + + private func combinePreviousImplementation(_ initial: Output?) -> AnyPublisher<(previous: Output, current: Output), Failure> { + return self + .scan((Output?.none, initial)) { current, newValue in + (current.1, newValue) + } + .map { previous, current -> AnyPublisher<(previous: Output, current: Output), Failure> in + if let previous = previous { + return Just((previous, current!)) + .setFailureType(to: Failure.self) + .eraseToAnyPublisher() + } else { + return Empty(completeImmediately: false).eraseToAnyPublisher() + } + } + .switchToLatest() + .eraseToAnyPublisher() + } +} + +#endif diff --git a/FueledUtils/Combine/Publisher+IgnoreRepeats.swift b/FueledUtils/Combine/Publisher+IgnoreRepeats.swift new file mode 100644 index 00000000..a20eef1d --- /dev/null +++ b/FueledUtils/Combine/Publisher+IgnoreRepeats.swift @@ -0,0 +1,34 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher { + @available(*, deprecated, renamed: "removeDuplicates(by:)") + public func ignoreRepeats(isEqual: @escaping (Output, Output) -> Bool) -> AnyPublisher { + self.removeDuplicates(by: isEqual).eraseToAnyPublisher() + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher where Output: Equatable { + @available(*, deprecated, renamed: "removeDuplicates(by:)") + public func ignoreRepeats() -> AnyPublisher { + self.removeDuplicates(by: ==).eraseToAnyPublisher() + } +} + +#endif diff --git a/FueledUtils/Combine/Publisher+TransferState.swift b/FueledUtils/Combine/Publisher+TransferState.swift new file mode 100644 index 00000000..fa553bcb --- /dev/null +++ b/FueledUtils/Combine/Publisher+TransferState.swift @@ -0,0 +1,35 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher where Output: TransferStateProtocol { + public func ignoreLoading() -> AnyPublisher { + self.map { transferState -> AnyPublisher in + guard let value = transferState.value else { + return Empty(completeImmediately: true) + .eraseToAnyPublisher() + } + return Just(value) + .setFailureType(to: Failure.self) + .eraseToAnyPublisher() + } + .switchToLatest() + .eraseToAnyPublisher() + } +} + +#endif diff --git a/FueledUtils/Combine/PublisherExtensions.swift b/FueledUtils/Combine/PublisherExtensions.swift new file mode 100644 index 00000000..02e3cfa7 --- /dev/null +++ b/FueledUtils/Combine/PublisherExtensions.swift @@ -0,0 +1,109 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher { + public func ignoreError() -> AnyPublisher { + self.catch { _ in Empty() }.eraseToAnyPublisher() + } + + public func promoteOptional() -> AnyPublisher { + self.map { Optional.some($0) }.eraseToAnyPublisher() + } + + public func sink() -> AnyCancellable { + self.sink(receiveCompletion: { _ in }, receiveValue: { _ in }) + } + + public func then(receiveResult: @escaping ((Result) -> Void)) -> AnyCancellable { + self.sink( + receiveCompletion: { completion in + if case .failure(let error) = completion { + receiveResult(.failure(error)) + } + }, + receiveValue: { value in + receiveResult(.success(value)) + } + ) + } + + public func sinkForLifetimeOf(_ object: Object) { + self.sink() + .store(in: &object.combineExtensions.cancellables) + } + + public func sinkForLifetimeOf(_ object: Object, receiveValue: @escaping ((Self.Output) -> Void)) where Failure == Never { + self.sink(receiveValue: receiveValue) + .store(in: &object.combineExtensions.cancellables) + } + + public func sinkForLifetimeOf(_ object: Object, receiveCompletion: @escaping ((Subscribers.Completion) -> Void), receiveValue: @escaping ((Self.Output) -> Void)) { + self.sink(receiveCompletion: receiveCompletion, receiveValue: receiveValue) + .store(in: &object.combineExtensions.cancellables) + } + + public func thenForLifetimeOf(_ object: Object, receiveResult: @escaping ((Result) -> Void)) { + self.then(receiveResult: receiveResult) + .store(in: &object.combineExtensions.cancellables) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher { + public func performDuringLifetimeOf(_ object: Object, action: @escaping (Object, Output) -> Void) { + self + .ignoreError() + .sinkForLifetimeOf(object) { [weak object] value in + guard let object = object else { + return + } + action(object, value) + } + } + + public func performDuringLifetimeOf(_ object: Object, action: @escaping (Object) -> (Output) -> Void) { + self.performDuringLifetimeOf(object) { object, output in + action(object)(output) + } + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher where Failure == Never { + public func assign(to keyPath: ReferenceWritableKeyPath, withoutRetaining object: Object) -> AnyCancellable { + self.sink { [weak object] in + object?[keyPath: keyPath] = $0 + } + } + + public func assign(to keyPath: ReferenceWritableKeyPath, forLifetimeOf object: Object) -> Void { + self.sink { [weak object] in + object?[keyPath: keyPath] = $0 + } + .store(in: &object.combineExtensions.cancellables) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher where Output: OptionalProtocol { + public func ignoreNil() -> AnyPublisher { + self.flatMap { ($0.wrapped.map { Just($0).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher()).setFailureType(to: Failure.self) }.eraseToAnyPublisher() + } +} + +#endif diff --git a/FueledUtils/Combine/PublishersExtensions.swift b/FueledUtils/Combine/PublishersExtensions.swift new file mode 100644 index 00000000..3367e143 --- /dev/null +++ b/FueledUtils/Combine/PublishersExtensions.swift @@ -0,0 +1,178 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publishers { + public struct CombineLatestMany: Publisher where PublisherCollection.Element: Combine.Publisher { + public typealias Output = [PublisherCollection.Element.Output] + + public typealias Failure = PublisherCollection.Element.Failure + + public let publishers: PublisherCollection + + public init(_ publishers: PublisherCollection) { + self.publishers = publishers + } + + public func receive(subscriber: Subscriber) where PublisherCollection.Element.Failure == Subscriber.Failure, Subscriber.Input == Output { + let subscription = CombineLatestManySubscription(subscriber: subscriber, publishers: self.publishers) + subscription.startReceiving() + } + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +private final class CombineLatestManySubscription< + Subscriber: Combine.Subscriber, + PublisherCollection: Swift.Collection +>: Subscription where + PublisherCollection.Element: Combine.Publisher, + PublisherCollection.Element.Failure == Subscriber.Failure, + Subscriber.Input == [PublisherCollection.Element.Output] +{ + @Atomic private var demandsState = DemandsState() + private let subscriber: Subscriber + private let publishers: PublisherCollection + + private struct DemandsState { + var currentDemand: Subscribers.Demand! + var pendingValuesBuffer: [Subscriber.Input] = [] + var cancellables: [AnyCancellable] = [] + + mutating func cancel() { + self.currentDemand = Subscribers.Demand.none + self.pendingValuesBuffer = [] + self.cancellables.forEach { $0.cancel() } + } + } + + init(subscriber: Subscriber, publishers: PublisherCollection) { + self.subscriber = subscriber + self.publishers = publishers + } + + func startReceiving() { + self.subscriber.receive(subscription: self) + } + + func request(_ demand: Subscribers.Demand) { + if self.publishers.isEmpty { + if demand > 0 { + _ = self.subscriber.receive([]) + } + self.subscriber.receive(completion: .finished) + return + } + + func sendPendingValuesIfPossible(demand: Subscribers.Demand, demandsState: inout DemandsState) -> Int? { + if demandsState.currentDemand != nil { + demandsState.currentDemand += demand + var valuesSent = 0 + while let firstValue = demandsState.pendingValuesBuffer.first, demandsState.currentDemand > 0 { + demandsState.pendingValuesBuffer.removeFirst() + sendValueIfPossible(firstValue, demandsState: &demandsState) + valuesSent += 1 + } + return valuesSent + } + return nil + } + + func sendValueIfPossible(_ value: Subscriber.Input, demandsState: inout DemandsState) { + if demandsState.currentDemand == 0 { + demandsState.pendingValuesBuffer.append(value) + return + } + + let demandedValues = self.subscriber.receive(value) + let sentValues = sendPendingValuesIfPossible( + demand: demandedValues, + demandsState: &demandsState + ) ?? 0 + let remainingDemand = demandedValues - sentValues + demandsState.currentDemand += remainingDemand + demandsState.currentDemand -= 1 + } + + let shouldReturn = self.$demandsState.modify { demandsState -> Bool in + let sentValues = sendPendingValuesIfPossible(demand: demand, demandsState: &demandsState) + demandsState.currentDemand = demand + return sentValues != nil + } + + if shouldReturn { + return + } + + let publishers = Array(self.publishers) + let cancellables = AtomicValue( + [ + ( + cancellable: AnyCancellable?, + latestValue: PublisherCollection.Element.Output?, + hasCompleted: Bool + ), + ](repeating: (nil, nil, false), count: self.publishers.count) + ) + publishers.enumerated().forEach { index, publisher in + let cancellable = publisher.sink( + receiveCompletion: { completion in + cancellables.modify { + switch completion { + case .failure(let error): + self.subscriber.receive(completion: .failure(error)) + for i in $0.indices { + $0[i].cancellable = nil + } + case .finished: + $0[index].cancellable = nil + $0[index].hasCompleted = true + if $0.allSatisfy({ $0.hasCompleted }) { + self.subscriber.receive(completion: .finished) + } + } + } + }, + receiveValue: { value in + cancellables.modify { + $0[index].latestValue = value + let allLatestValues = $0.compactMap { $0.latestValue } + if allLatestValues.count == publishers.count { + self.$demandsState.modify { + sendValueIfPossible(allLatestValues, demandsState: &$0) + } + } + } + } + ) + cancellables.modify { cancellables in + if !cancellables[index].hasCompleted { + cancellables[index].cancellable = cancellable + } + self.$demandsState.modify { + $0.cancellables = cancellables.compactMap { $0.cancellable } + } + } + } + } + + func cancel() { + self.$demandsState.modify { $0.cancel() } + } +} + +#endif diff --git a/FueledUtils/Combine/Subject+SendResult.swift b/FueledUtils/Combine/Subject+SendResult.swift new file mode 100644 index 00000000..cc90b186 --- /dev/null +++ b/FueledUtils/Combine/Subject+SendResult.swift @@ -0,0 +1,31 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Subject { + public func send(result: Result) { + switch result { + case .failure(let error): + self.send(completion: .failure(error)) + case .success(let value): + self.send(value) + self.send(completion: .finished) + } + } +} + +#endif diff --git a/FueledUtils/Combine/Subscriber+EraseToAnySubscriber.swift b/FueledUtils/Combine/Subscriber+EraseToAnySubscriber.swift new file mode 100644 index 00000000..f37b5098 --- /dev/null +++ b/FueledUtils/Combine/Subscriber+EraseToAnySubscriber.swift @@ -0,0 +1,25 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Subscriber { + public func eraseToAnySubscriber() -> AnySubscriber { + AnySubscriber(self) + } +} + +#endif diff --git a/FueledUtils/CombineOperators/Combine+Operators.swift b/FueledUtils/CombineOperators/Combine+Operators.swift new file mode 100644 index 00000000..6f03c618 --- /dev/null +++ b/FueledUtils/CombineOperators/Combine+Operators.swift @@ -0,0 +1,133 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +// swiftlint:disable generic_type_name + +#if !canImport(ReactiveSwift) +precedencegroup BindingPrecedence { + associativity: right + + higherThan: AssignmentPrecedence +} + +infix operator <~: BindingPrecedence +#else +import ReactiveSwift +#endif + +precedencegroup AccessPrecedence { + associativity: right + + higherThan: BindingPrecedence +} + +infix operator ~: AccessPrecedence + +precedencegroup InsertCancellablePrecedence { + associativity: left + + lowerThan: AssignmentPrecedence +} + +infix operator >>>: InsertCancellablePrecedence + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public struct ObjectKeyPathReference { + public let object: Root + public let keyPath: ReferenceWritableKeyPath +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func ~ (lhs: Object, rhs: ReferenceWritableKeyPath) -> ObjectKeyPathReference { + ObjectKeyPathReference(object: lhs, keyPath: rhs) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func <~ ( + lhs: ObjectKeyPathReference, + rhs: Publisher +) -> AnyCancellable where Publisher.Output == Value, Publisher.Failure == Never { + rhs.assign(to: lhs.keyPath, withoutRetaining: lhs.object) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func <~ ( + lhs: ObservingObject, + rhs: ObservedObject +) where ObservingObject.ObjectWillChangePublisher == ObservableObjectPublisher { + lhs.link(to: rhs) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func <~ ( + lhs: ObservingObject, + rhs: ObservedObjectCollection +) where ObservingObject.ObjectWillChangePublisher == ObservableObjectPublisher, ObservedObjectCollection.Element: ObservableObject { + lhs.link(to: rhs) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func <~ ( + lhs: ObservingObject, + rhs: Publisher +) where ObservingObject.ObjectWillChangePublisher == ObservableObjectPublisher, Publisher.Output: ObservableObject { + lhs.link(to: rhs) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func <~ ( + lhs: ObservingObject, + rhs: Publisher +) where ObservingObject.ObjectWillChangePublisher == ObservableObjectPublisher, Publisher.Output: OptionalProtocol, Publisher.Output.Wrapped: ObservableObject { + lhs.link(to: rhs) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func <~ ( + lhs: ObservingObject, + rhs: Publisher +) where ObservingObject.ObjectWillChangePublisher == ObservableObjectPublisher, Publisher.Output: Collection, Publisher.Output.Element: ObservableObject { + lhs.link(to: rhs) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func <~ ( + lhs: ObservingObject, + rhs: ReferenceWritableKeyPath +) where ObservingObject.ObjectWillChangePublisher == ObservableObjectPublisher { + lhs.link(to: lhs[keyPath: rhs]) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func <~ ( + lhs: ObservingObject, + rhs: ReferenceWritableKeyPath +) where ObservingObject.ObjectWillChangePublisher == ObservableObjectPublisher, ObservedObjectCollection.Element: ObservableObject { + lhs.link(to: lhs[keyPath: rhs]) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func >>> (lhs: AnyCancellable, rhs: inout CancellableCollection) where CancellableCollection.Element == AnyCancellable { + lhs.store(in: &rhs) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func >>> (lhs: AnyCancellable, rhs: inout Set) { + lhs.store(in: &rhs) +} + +#endif diff --git a/FueledUtils/CombineOperators/CombineOperators+Optional.swift b/FueledUtils/CombineOperators/CombineOperators+Optional.swift new file mode 100644 index 00000000..29888812 --- /dev/null +++ b/FueledUtils/CombineOperators/CombineOperators+Optional.swift @@ -0,0 +1,54 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func >>> (lhs: AnyCancellable?, rhs: inout CancellableCollection) where CancellableCollection.Element == AnyCancellable { + lhs?.store(in: &rhs) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func >>> (lhs: AnyCancellable?, rhs: inout Set) { + lhs?.store(in: &rhs) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func >>> (lhs: AnyCancellable, rhs: inout CancellableCollection?) where CancellableCollection.Element == AnyCancellable { + rhs?.append(lhs) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func >>> (lhs: AnyCancellable, rhs: inout Set?) { + rhs?.insert(lhs) +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func >>> (lhs: AnyCancellable?, rhs: inout CancellableCollection?) where CancellableCollection.Element == AnyCancellable { + guard let lhs = lhs else { + return + } + lhs >>> rhs +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public func >>> (lhs: AnyCancellable?, rhs: inout Set?) { + guard let lhs = lhs else { + return + } + lhs >>> rhs +} + +#endif diff --git a/FueledUtils/CombineUIKit/ControlProtocol+Tapped.swift b/FueledUtils/CombineUIKit/ControlProtocol+Tapped.swift new file mode 100644 index 00000000..80c5b9c1 --- /dev/null +++ b/FueledUtils/CombineUIKit/ControlProtocol+Tapped.swift @@ -0,0 +1,73 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(UIKit) && !os(watchOS) && canImport(Combine) +import Combine + +private var tapActionStorage: UInt8 = 0 +private var tapActionKey: UInt8 = 0 + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension ControlProtocol { + /// + /// The action to be triggered when the button is tapped. + /// This mirrors the `pressed` property native in `ReactiveCocoa`, but uses a + /// protocol to represents the button rather than hardcode it to classes, + /// allowing for any `UIControl` to use this method. + /// + public var tapped: TapAction? { + get { + self.tapActionStorage?.tapAction + } + set { + self.tapActionStorage = nil + + if let newValue = newValue { + let tapActionStorage = TapActionStorage(newValue) + newValue.$isEnabled.assign(to: \.isEnabled, withoutRetaining: self) + .store(in: &tapActionStorage.cancellables) + if let self = self as? ControlLoadingProtocol { + newValue.$isExecuting.sink { [weak self] in + self?.isLoading = $0 + } + .store(in: &tapActionStorage.cancellables) + } + self.removeTarget(newValue, action: TapAction.selector, for: .primaryActionTriggered) + self.addTarget(newValue, action: TapAction.selector, for: .primaryActionTriggered) + self.tapActionStorage = tapActionStorage + } + } + } + + private var tapActionStorage: TapActionStorage? { + get { + objc_getAssociatedObject(self, &tapActionKey) as? TapActionStorage + } + set { + objc_setAssociatedObject(self, &tapActionKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +private final class TapActionStorage { + let tapAction: TapAction + var cancellables = Set() + + init(_ tapAction: TapAction) { + self.tapAction = tapAction + } +} + +#endif diff --git a/FueledUtils/CombineUIKit/TapAction.swift b/FueledUtils/CombineUIKit/TapAction.swift new file mode 100644 index 00000000..c36e8d2e --- /dev/null +++ b/FueledUtils/CombineUIKit/TapAction.swift @@ -0,0 +1,71 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(UIKit) && !os(watchOS) +#if canImport(Combine) +import Combine +#endif + +/// +/// `TapAction` wraps a `ActionProtocol` for use by any `ControlProtocol`. +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public final class TapAction: NSObject { + @objc static var selector: Selector { + #selector(userDidTapControl(_:)) + } + + @Published private(set) var isExecuting: Bool = false + @Published private(set) var isEnabled: Bool = false + + // FIXME: (Stéphane) To be retested for the next version of Swift (after 5.3) + // Any initializers below create a segfault when compiling with optimizations. + private let inputTransform: ((Control) -> Any) + private let confirmAction: ((@escaping () -> Void) -> Void)? + private let action: AnyAction + private var cancellables = Set() + + public convenience init(_ action: Action, confirmAction: ((@escaping () -> Void) -> Void)? = nil) where Action.Input == Void { + self.init(action, input: (), confirmAction: confirmAction) + } + + public convenience init(_ action: Action, input: Action.Input, confirmAction: ((@escaping () -> Void) -> Void)? = nil) { + self.init(action, confirmAction: confirmAction, inputTransform: { _ in input }) + } + + public init(_ action: Action, confirmAction: (((@escaping () -> Void) -> Void))? = nil, inputTransform: @escaping (Control) -> Action.Input) { + self.isEnabled = action.isEnabled + self.isExecuting = action.isExecuting + self.inputTransform = { inputTransform($0) } + self.confirmAction = confirmAction + self.action = AnyAction(action) + super.init() + self.action.isEnabledPublisher.assign(to: \.isEnabled, withoutRetaining: self) + .store(in: &self.cancellables) + self.action.isExecutingPublisher.assign(to: \.isExecuting, withoutRetaining: self) + .store(in: &self.cancellables) + } + + @objc private func userDidTapControl(_ button: Any) { + let confirmAction = self.confirmAction ?? { $0() } + confirmAction { [weak self] in + guard let self = self else { + return + } + + self.action.apply(self.inputTransform(button as! Control)).sink() + .store(in: &self.cancellables) + } + } +} +#endif diff --git a/FueledUtils/CombineUIKit/UIControl+ControlEventsPublisher.swift b/FueledUtils/CombineUIKit/UIControl+ControlEventsPublisher.swift new file mode 100644 index 00000000..c6a5a998 --- /dev/null +++ b/FueledUtils/CombineUIKit/UIControl+ControlEventsPublisher.swift @@ -0,0 +1,94 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(UIKit) && !os(watchOS) && canImport(Combine) +import Combine +import UIKit + +private var publisherControlEventsProcessorsHolderKey: UInt8 = 0 + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension ControlProtocol { + public func publisherForControlEvents(_ controlEvents: UIControl.Event) -> AnyPublisher { + let passthroughSubject = PassthroughSubject() + var cancellable: AnyCancellable! = self.publisherControlEventsProcessorsHolder.addProcessor( + for: controlEvents, + in: self, + passthroughSubject: passthroughSubject + ) + _ = cancellable + return passthroughSubject + .map { + $0 as! Self + } + .handleEvents( + receiveCancel: { + cancellable = nil + } + ) + .eraseToAnyPublisher() + } + + private var publisherControlEventsProcessorsHolder: PublisherControlEventsProcessorsHolder { + get { + objc_getAssociatedObject(self, &publisherControlEventsProcessorsHolderKey) as? PublisherControlEventsProcessorsHolder ?? { + let publisherControlEventsProcessorsHolder = PublisherControlEventsProcessorsHolder() + self.publisherControlEventsProcessorsHolder = publisherControlEventsProcessorsHolder + return publisherControlEventsProcessorsHolder + }() + } + set { + objc_setAssociatedObject(self, &publisherControlEventsProcessorsHolderKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +private final class PublisherControlEventsProcessorsHolder { + private final class PublisherControlEventsProcessor: NSObject { + weak var passthroughSubject: PassthroughSubject! + + init(control: ControlProtocol, controlEvents: UIControl.Event, passthroughSubject: PassthroughSubject) { + self.passthroughSubject = passthroughSubject + super.init() + control.addTarget(self, action: #selector(PublisherControlEventsProcessor.handleControlEvents(_:)), for: controlEvents) + } + + @objc func handleControlEvents(_ sender: Any) { + self.passthroughSubject.send(sender) + } + } + + private var processors: [PublisherControlEventsProcessor] = [] + + init() { + } + + func addProcessor( + for controlEvents: UIControl.Event, + in control: ControlProtocol, + passthroughSubject: PassthroughSubject + ) -> AnyCancellable { + let processor = PublisherControlEventsProcessor( + control: control, + controlEvents: controlEvents, + passthroughSubject: passthroughSubject + ) + return AnyCancellable { + self.processors.removeAll { $0 === processor } + } + } +} + +#endif diff --git a/FueledUtils/CombineUIKit/UITextInput+Combine.swift b/FueledUtils/CombineUIKit/UITextInput+Combine.swift new file mode 100644 index 00000000..aef9a4dd --- /dev/null +++ b/FueledUtils/CombineUIKit/UITextInput+Combine.swift @@ -0,0 +1,43 @@ +// Copyright © 2020 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(UIKit) && !os(watchOS) && canImport(Combine) +import Combine +import UIKit + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension CombineExtensions where Base: UITextInput, Base: ControlProtocol { + public var textValues: AnyPublisher { + self.textPublisherForControlEvents([.editingDidEnd, .editingDidEndOnExit]) + } + + public var continuousTextValues: AnyPublisher { + self.textPublisherForControlEvents(.allEditingEvents) + } + + private func textPublisherForControlEvents(_ controlEvents: UIControl.Event) -> AnyPublisher { + self.base.publisherForControlEvents(controlEvents) + .map { textInput in + textInput.textRange( + from: textInput.beginningOfDocument, + to: textInput.endOfDocument + ) + .flatMap { textInput.text(in: $0) } + ?? "" + } + .eraseToAnyPublisher() + } +} + +#endif diff --git a/FueledUtils/Core/AnyIdentifiable.swift b/FueledUtils/Core/AnyIdentifiable.swift new file mode 100644 index 00000000..f36516b1 --- /dev/null +++ b/FueledUtils/Core/AnyIdentifiable.swift @@ -0,0 +1,29 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// +/// A type-erased `Identifiable` object. +/// +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +struct AnyIdentifiable: Identifiable { + private let hashValueClosure: () -> AnyHashable + + init(_ identifiable: Identifiable) { + self.hashValueClosure = { AnyHashable(identifiable.id) } + } + + var id: AnyHashable { + self.hashValueClosure() + } +} diff --git a/FueledUtils/Core/Atomic.swift b/FueledUtils/Core/Atomic.swift new file mode 100644 index 00000000..18ebb986 --- /dev/null +++ b/FueledUtils/Core/Atomic.swift @@ -0,0 +1,120 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +infix operator .=: AssignmentPrecedence + +public final class AtomicValue { + private var valueStorage: Value + + /// If modifying the `value`, consider that the operation might not be atomic. Consider the following examples: + /// ``` + /// let atomicValue = AtomicValue(0) + /// atomicValue.value = 1 + /// ``` + /// This is an atomic operation as the original value is overriden. + /// ``` + /// let atomicValue = AtomicValue(0) + /// atomicValue.value += 1 + /// ``` + /// This is _not_ an atomic operation, as this will be translated as: + /// atomicValue.value = atomicValue.value + 1 + /// Resulting in the internal lock being acquired twice, resulting in potential undesired behavior. + /// If modifying the value, `modify()` is recommended to avoid such behavior. + public var value: Value { + get { + self.lock.lock() + defer { + self.lock.unlock() + } + return self.valueStorage + } + set { + self.lock.lock() + defer { + self.lock.unlock() + } + self.valueStorage = newValue + } + } + private let lock = Lock() + + public init(_ value: Value) { + self.valueStorage = value + } + + public static func .= (atomicValue: AtomicValue, value: Value) { + atomicValue.modify { $0 = value } + } + + public func modify(_ modify: (inout Value) -> Return) -> Return { + self.lock.lock() + defer { + self.lock.unlock() + } + return modify(&self.valueStorage) + } + + public func withValue(_ getter: (Value) -> Return) -> Return { + self.lock.lock() + defer { + self.lock.unlock() + } + return getter(self.valueStorage) + } +} + +@propertyWrapper +public struct Atomic { + private let atomicValue: AtomicValue + + public init(_ value: Value) { + self.atomicValue = AtomicValue(value) + } + + public init(wrappedValue value: Value) { + self.init(value) + } + + public var wrappedValue: Value { + get { + self.atomicValue.value + } + set { + self.atomicValue.value = newValue + } + } + + public var projectedValue: Atomic { + get { + self + } + set { + self = newValue + } + } + + public mutating func modify(_ modify: (inout Value) -> Return) -> Return { + self.atomicValue.modify(modify) + } + + public mutating func withValue(_ getter: (Value) -> Return) -> Return { + self.atomicValue.withValue(getter) + } + + public static func .= (atomicValue: inout Atomic, value: Value) { + atomicValue.atomicValue .= value + } +} diff --git a/FueledUtils/CollectionExtensions.swift b/FueledUtils/Core/CollectionExtensions.swift similarity index 64% rename from FueledUtils/CollectionExtensions.swift rename to FueledUtils/Core/CollectionExtensions.swift index d38f4148..cc1018de 100644 --- a/FueledUtils/CollectionExtensions.swift +++ b/FueledUtils/Core/CollectionExtensions.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ extension Collection { /// /// **Unavailable**: Please use `getSafely(at:)` instead. diff --git a/FueledUtils/Core/Lock.swift b/FueledUtils/Core/Lock.swift new file mode 100644 index 00000000..1c1de4b4 --- /dev/null +++ b/FueledUtils/Core/Lock.swift @@ -0,0 +1,100 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +private protocol LockImplementation { + mutating func lock() + mutating func `try`() -> Bool + mutating func unlock() +} + +@available(iOS 10.0, tvOS 10.0, watchOS 3.0, *) +private struct UnfairLock: LockImplementation { + private var unfairLock = os_unfair_lock_s() + + mutating func lock() { + os_unfair_lock_lock(&self.unfairLock) + } + + mutating func `try`() -> Bool { + os_unfair_lock_trylock(&self.unfairLock) + } + + mutating func unlock() { + os_unfair_lock_unlock(&self.unfairLock) + } +} + +private struct PThreadMutexLock: LockImplementation { + private var mutex = pthread_mutex_t() + + init?() { + if pthread_mutex_init(&self.mutex, nil) != 0 { + return nil + } + } + + mutating func lock() { + pthread_mutex_lock(&self.mutex) + } + + mutating func `try`() -> Bool { + pthread_mutex_trylock(&self.mutex) == 0 + } + + mutating func unlock() { + pthread_mutex_unlock(&self.mutex) + } +} + +private struct CocoaLock: LockImplementation { + private let lockImplementation = NSLock() + + mutating func lock() { + self.lockImplementation.lock() + } + + mutating func `try`() -> Bool { + self.lockImplementation.try() + } + + mutating func unlock() { + self.lockImplementation.unlock() + } +} + +public final class Lock { + private var lockImplementation: LockImplementation + + public init() { + if #available(iOS 10.0, tvOS 10.0, watchOS 3.0, *) { + self.lockImplementation = UnfairLock() + } else { + self.lockImplementation = PThreadMutexLock() ?? CocoaLock() + } + } + + public func lock() { + self.lockImplementation.lock() + } + + public func `try`() -> Bool { + self.lockImplementation.try() + } + + public func unlock() { + self.lockImplementation.unlock() + } +} diff --git a/FueledUtils/NSDecimalNumberOperators.swift b/FueledUtils/Core/NSDecimalNumberOperators.swift similarity index 72% rename from FueledUtils/NSDecimalNumberOperators.swift rename to FueledUtils/Core/NSDecimalNumberOperators.swift index db975ed6..5c892b9d 100644 --- a/FueledUtils/NSDecimalNumberOperators.swift +++ b/FueledUtils/Core/NSDecimalNumberOperators.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation extension NSDecimalNumber: Comparable { diff --git a/FueledUtils/Core/OptionalProtocol.swift b/FueledUtils/Core/OptionalProtocol.swift new file mode 100644 index 00000000..3341073f --- /dev/null +++ b/FueledUtils/Core/OptionalProtocol.swift @@ -0,0 +1,25 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +public protocol OptionalProtocol { + associatedtype Wrapped + + var wrapped: Wrapped? { get } +} + +extension Optional: OptionalProtocol { + public var wrapped: Wrapped? { + self + } +} diff --git a/FueledUtils/Core/OrderedSet.swift b/FueledUtils/Core/OrderedSet.swift new file mode 100644 index 00000000..d754bf0b --- /dev/null +++ b/FueledUtils/Core/OrderedSet.swift @@ -0,0 +1,236 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +// From https://github.com/apple/swift-package-manager/blob/4f69f1931b5a28bcac7a41bdb1eaddcb1223eeec/TSC/Sources/TSCBasic/OrderedSet.swift +/// An ordered set is an ordered collection of instances of `Element` in which +/// uniqueness of the objects is guaranteed. +public struct OrderedSet: Equatable, RangeReplaceableCollection, SetAlgebra { + public typealias Element = E + public typealias Index = Int + public typealias Indices = Range + + private var array: [Element] + private var set: Set + + /// Creates an empty ordered set. + public init() { + self.array = [] + self.set = Set() + } + + /// Creates an ordered set with the contents of `sequence`. + /// + /// If an element occurs more than once in `sequence`, only the first one + /// will be included. + public init(_ sequence: Sequence) where Sequence.Element == Self.Element { + self.init() + for element in sequence { + self.append(element) + } + } + + // MARK: Working with an ordered set + + /// The number of elements the ordered set stores. + public var count: Int { + self.array.count + } + + /// Returns `true` if the set is empty. + public var isEmpty: Bool { + self.array.isEmpty + } + + /// Returns the contents of the set as an array. + public var contents: [Element] { + self.array + } + + /// Returns `true` if the ordered set contains `member`. + public func contains(_ member: Element) -> Bool { + self.set.contains(member) + } + + /// Adds an element to the ordered set. + /// + /// If it already contains the element, then the set is unchanged. + /// + /// - returns: True if the item was inserted. + @discardableResult + public mutating func append(_ newElement: Element) -> Bool { + let inserted = self.set.insert(newElement).inserted + if inserted { + self.array.append(newElement) + } + return inserted + } + + public mutating func replaceSubrange< + Collection: Swift.Collection, + Range: RangeExpression + >( + _ subrange: Range, + with newElements: Collection + ) where + Collection.Element == Element, + Range.Bound == Index + { + let t = newElements.filter { newElement in + !zip(self.array.indices, self.array).contains { index, element in + if subrange.contains(index) { + return false + } + return element == newElement + } + } + self.array.replaceSubrange( + subrange, + with: t + ) + self.set.formUnion(newElements) + } + + public mutating func insert(_ newMember: E) -> (inserted: Bool, memberAfterInsert: E) { + let result = self.set.insert(newMember) + if result.inserted { + self.array.append(newMember) + } + return result + } + + /// Remove and return the element at the beginning of the ordered set. + public mutating func removeFirst() -> Element { + let firstElement = self.array.removeFirst() + self.set.remove(firstElement) + return firstElement + } + + /// Remove and return the element at the end of the ordered set. + public mutating func removeLast() -> Element { + let lastElement = self.array.removeLast() + self.set.remove(lastElement) + return lastElement + } + + /// Remove all elements. + public mutating func removeAll(keepingCapacity keepCapacity: Bool) { + self.array.removeAll(keepingCapacity: keepCapacity) + self.set.removeAll(keepingCapacity: keepCapacity) + } + + public mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows { + try self.array.removeAll(where: shouldBeRemoved) + self.set = Set(self.array) + } + + @discardableResult + public mutating func remove(_ member: Element) -> Element? { + self.array.removeAll { $0 == member } + return self.set.remove(member) + } + + public static func == (lhs: OrderedSet, rhs: OrderedSet) -> Bool { + lhs.contents == rhs.contents + } + + public func union(_ other: OrderedSet) -> OrderedSet { + var this = self + this.formUnion(other) + return this + } + + public func intersection(_ other: OrderedSet) -> OrderedSet { + var this = self + this.formIntersection(other) + return this + } + + public func symmetricDifference(_ other: OrderedSet) -> OrderedSet { + var this = self + this.formSymmetricDifference(other) + return this + } + + public mutating func update(with newMember: E) -> E? { + if let index = self.array.firstIndex(where: { $0 == newMember }) { + self.array[index] = newMember + } else { + self.array.append(newMember) + } + return self.set.update(with: newMember) + } + + public mutating func formUnion(_ other: OrderedSet) { + self.array += other.filter { !self.set.contains($0) } + self.set.formUnion(other) + } + + public mutating func formIntersection(_ other: OrderedSet) { + self.set.formIntersection(other) + self.array.removeAll { !self.set.contains($0) } + } + + public mutating func formSymmetricDifference(_ other: OrderedSet) { + self.array += other.filter { !self.set.contains($0) } + self.set.formSymmetricDifference(other) + self.array.removeAll { !self.set.contains($0) } + } +} + +extension OrderedSet: ExpressibleByArrayLiteral { + /// Create an instance initialized with `elements`. + /// + /// If an element occurs more than once in `element`, only the first one + /// will be included. + public init(arrayLiteral elements: Element...) { + self.init(elements) + } +} + +extension OrderedSet: RandomAccessCollection { + public var startIndex: Int { + self.contents.startIndex + } + + public var endIndex: Int { + self.contents.endIndex + } + + public subscript(index: Int) -> Element { + self.contents[index] + } +} + +extension OrderedSet: Hashable where Element: Hashable { +} + +extension OrderedSet: Decodable where Element: Decodable { + public init(from decoder: Decoder) throws { + try self.init([Element](from: decoder)) + } +} + +extension OrderedSet: Encodable where Element: Encodable { + public func encode(to encoder: Encoder) throws { + try self.array.encode(to: encoder) + } +} + +extension OrderedSet: CustomStringConvertible { + public var description: String { + self.array.description + } +} diff --git a/FueledUtils/Regex.swift b/FueledUtils/Core/Regex.swift similarity index 54% rename from FueledUtils/Regex.swift rename to FueledUtils/Core/Regex.swift index fe5528ff..6e1952bb 100644 --- a/FueledUtils/Regex.swift +++ b/FueledUtils/Core/Regex.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation /// @@ -60,6 +59,25 @@ public struct Regex { public func match(_ string: String, options: NSRegularExpression.MatchingOptions = []) -> Bool { return implementation.numberOfMatches(in: string, options: options, range: string.nsRange) != 0 } + + /// Match all the captured groups if any. + /// + /// - Parameters: + /// - pattern: The string to match the regex against. + /// - options: The options to use when matching the regular expression + /// against the given string. + /// - Returns: The captured groups. + /// + /// - Note: By default, NSRegularExpression exposes the matching text (not the + /// group) as the first (index 0) element of the NSTextCheckingResult. This + /// is ignored in the returned value, as it is not a captured group. + public func groups(in string: String, options: NSRegularExpression.MatchingOptions = []) -> [[String]] { + let matches = implementation.matches(in: string, options: options, range: string.nsRange) + return matches + .map { match in (1.. Bool) -> Iterator.Element? { - return self.first(where: predicate) - } } diff --git a/FueledUtils/StringExtensions.swift b/FueledUtils/Core/StringExtensions.swift similarity index 83% rename from FueledUtils/StringExtensions.swift rename to FueledUtils/Core/StringExtensions.swift index 9b1572b0..46447fdd 100644 --- a/FueledUtils/StringExtensions.swift +++ b/FueledUtils/Core/StringExtensions.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation extension StringProtocol { @@ -25,16 +24,6 @@ extension StringProtocol { return (String(self) as NSString).length } - /// - /// **Unavailable**: Please use `nsRange` instead. - /// - /// Refer to the documentation for `nsRange` for more info. - /// - @available(*, unavailable, renamed: "nsRange") - public var fullRange: NSRange { - return NSRange(location: 0, length: nsLength) - } - /// /// Returns `NSRange(location: 0, length: nsLength)` for usage with Objective-C APIs. /// diff --git a/FueledUtils/Core/SwiftExtensions.swift b/FueledUtils/Core/SwiftExtensions.swift new file mode 100644 index 00000000..723bfdc6 --- /dev/null +++ b/FueledUtils/Core/SwiftExtensions.swift @@ -0,0 +1,31 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +extension FloatingPoint { + func rounded(decimalPlaces: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Self { + var this = self + this.round(decimalPlaces: decimalPlaces, rule: rule) + return this + } + + mutating func round(decimalPlaces: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) { + var offset = Self(1) + for _ in (0.. = TransferState +/// ``` +/// or +/// ```swift +/// typealias DownloadState = TransferState<(uploadedBytes: UInt64, totalBytes: UInt64)?, Value> +/// ``` +/// Nothing prevents you to also include the `Value` type in the `typealias`, if possible for your use case. +/// +public enum TransferState: TransferStateProtocol { + /// + /// Represents a `loading` state. + /// + case loading(Progress) + /// + /// Represents a `finished` state. + /// + case finished(Value) + + public var progress: Progress? { + if case .loading(let progress) = self { + return progress + } + return nil + } + + public var value: Value? { + if case .finished(let value) = self { + return value + } + return nil + } +} + +extension TransferStateProtocol { + /// + /// Map a `TransferState` finishing with one `Value` into another, mapping it with the given closure. + /// + /// - Parameters: + /// - mapper: Mapper closure how to map the initial value to the mapped value. + /// - Returns: A `TransferState` with the mapped value as mapped by the given closure. + /// + public func map(_ mapper: (Value) throws -> Mapped) rethrows -> TransferState { + if let progress = self.progress { + return .loading(progress) + } + return .finished(try mapper(self.value!)) + } +} + +extension TransferStateProtocol where Progress: Numeric { + public func interpolated(min: Progress, max: Progress) -> TransferState { + if let progress = self.progress { + return .loading(progress * (max - min) + min) + } + return .finished(self.value!) + } +} diff --git a/FueledUtils/FoundationExtensions.swift b/FueledUtils/FoundationExtensions.swift deleted file mode 100644 index efa8db91..00000000 --- a/FueledUtils/FoundationExtensions.swift +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -import Foundation - -extension Bool { - /// - /// **Unavailable**: Please use `toggle()` instead. - /// - /// Refer to the documentation for `toggle()` for more info. - /// - @available(*, unavailable, renamed: "toggle()") - public mutating func flip() { - fatalError() - } -} diff --git a/FueledUtils/ReactiveCombineBridge/Combine+ReactiveSwift.swift b/FueledUtils/ReactiveCombineBridge/Combine+ReactiveSwift.swift new file mode 100644 index 00000000..6a532a07 --- /dev/null +++ b/FueledUtils/ReactiveCombineBridge/Combine+ReactiveSwift.swift @@ -0,0 +1,68 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine +import ReactiveSwift + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publisher { + public var producer: SignalProducer { + SignalProducer { observer, lifetime in + lifetime += self.sink( + receiveCompletion: { completion in + switch completion { + case .finished: + observer.sendCompleted() + case .failure(let error): + observer.send(error: error) + } + }, + receiveValue: { value in + observer.send(value: value) + } + ) + } + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Lifetime { + @discardableResult + public static func += (lhs: Lifetime, rhs: Cancellable?) -> Disposable? { + rhs.flatMap { lhs.observeEnded($0.cancel) } + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Disposable { + public var cancellable: some Cancellable { + DisposableCancellable(self) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +private struct DisposableCancellable: Cancellable { + private let disposable: Disposable + + init(_ disposable: Disposable) { + self.disposable = disposable + } + + func cancel() { + self.disposable.dispose() + } +} + +#endif diff --git a/FueledUtils/ReactiveCombineBridge/ReactiveSwift+Combine.swift b/FueledUtils/ReactiveCombineBridge/ReactiveSwift+Combine.swift new file mode 100644 index 00000000..15624199 --- /dev/null +++ b/FueledUtils/ReactiveCombineBridge/ReactiveSwift+Combine.swift @@ -0,0 +1,220 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine +import ReactiveSwift + +// From https://github.com/ReactiveCocoa/ReactiveSwift/pull/776/files#diff-d8195adf8a5f3283e072483fd9699c90 +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension SignalProducerConvertible { + public func eraseToAnyPublisher() -> AnyPublisher { + self.publisher.eraseToAnyPublisher() + } + + public var publisher: Publishers.SignalProducerPublisher { + Publishers.SignalProducerPublisher(self.producer) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Publishers { + public struct SignalProducerPublisher: Publisher { + public let base: SignalProducer + + public init(_ base: SignalProducer) { + self.base = base + } + + public func receive(subscriber: Subscriber) where Subscriber.Input == Output, Subscriber.Failure == Failure { + let subscription = SignalProducerSubscription(subscriber: subscriber, base: base) + subscription.bootstrap() + } + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +private final class SignalProducerSubscription: Combine.Subscription { + typealias Output = Subscriber.Input + typealias Failure = Subscriber.Failure + + let subscriber: Subscriber + let base: SignalProducer + private let state: ReactiveSwift.Atomic + + init(subscriber: Subscriber, base: SignalProducer) { + self.subscriber = subscriber + self.base = base + self.state = ReactiveSwift.Atomic(State()) + } + + func bootstrap() { + subscriber.receive(subscription: self) + } + + func request(_ incoming: Subscribers.Demand) { + let response: DemandResponse = state.modify { state in + guard state.hasCancelled == false else { + return .noAction + } + + guard state.hasStarted else { + state.hasStarted = true + state.requested = incoming + return .startUpstream + } + + state.requested = state.requested + incoming + let unsatified = state.requested - state.satisfied + + if let max = unsatified.max { + let dequeueCount = Swift.min(state.buffer.count, max) + state.satisfied += dequeueCount + + defer { state.buffer.removeFirst(dequeueCount) } + return .satisfyDemand(Array(state.buffer.prefix(dequeueCount))) + } else { + defer { state.buffer = [] } + return .satisfyDemand(state.buffer) + } + } + + switch response { + case let .satisfyDemand(output): + var demand: Subscribers.Demand = .none + + for output in output { + demand += subscriber.receive(output) + } + + if demand != .none { + request(demand) + } + + case .startUpstream: + let disposable = base.start { [weak self] event in + guard let self = self else { return } + + switch event { + case let .value(output): + let (shouldSendImmediately, isDemandUnlimited): (Bool, Bool) = self.state.modify { state in + guard state.hasCancelled == false else { return (false, false) } + + let unsatified = state.requested - state.satisfied + + if let count = unsatified.max, count >= 1 { + assert(state.buffer.count == 0) + state.satisfied += 1 + return (true, false) + } else if unsatified == .unlimited { + assert(state.buffer.isEmpty) + return (true, true) + } else { + assert(state.requested == state.satisfied) + state.buffer.append(output) + return (false, false) + } + } + + if shouldSendImmediately { + let demand = self.subscriber.receive(output) + + if isDemandUnlimited == false && demand != .none { + self.request(demand) + } + } + + case .completed, .interrupted: + self.cancel() + self.subscriber.receive(completion: .finished) + + case let .failed(error): + self.cancel() + self.subscriber.receive(completion: .failure(error)) + } + } + + let shouldDispose: Bool = state.modify { state in + guard state.hasCancelled == false else { return true } + state.producerSubscription = disposable + return false + } + + if shouldDispose { + disposable.dispose() + } + + case .noAction: + break + } + } + + func cancel() { + let disposable = state.modify { $0.cancel() } + disposable?.dispose() + } + + struct State { + var requested: Subscribers.Demand = .none + var satisfied: Subscribers.Demand = .none + + var buffer: [Output] = [] + + var producerSubscription: Disposable? + var hasStarted = false + var hasCancelled = false + + init() { + producerSubscription = nil + hasStarted = false + hasCancelled = false + } + + mutating func cancel() -> Disposable? { + hasCancelled = true + defer { producerSubscription = nil } + return producerSubscription + } + } + + enum DemandResponse { + case startUpstream + case satisfyDemand([Output]) + case noAction + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Cancellable { + var disposable: some Disposable { + CancellableDisposable(self) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +private final class CancellableDisposable: Disposable { + private let cancellable: Cancellable + private(set) var isDisposed: Bool = false + + init(_ cancellable: Cancellable) { + self.cancellable = cancellable + } + + func dispose() { + self.cancellable.cancel() + self.isDisposed = true + } +} + +#endif diff --git a/FueledUtils/ReactiveCommon/ActionErrorProtocol.swift b/FueledUtils/ReactiveCommon/ActionErrorProtocol.swift new file mode 100644 index 00000000..9c4c3768 --- /dev/null +++ b/FueledUtils/ReactiveCommon/ActionErrorProtocol.swift @@ -0,0 +1,39 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// +/// A protocol for `ActionError` for use in type constraints +/// +public protocol ActionErrorProtocol: Swift.Error { + /// + /// Type of the error associated with the action error. + /// + associatedtype InnerError: Swift.Error + + /// + /// Whether the receiver is currently in the disabled state or not. + /// + var isDisabled: Bool { get } + + /// + /// The error the action protocol currently have. If `nil`, the action error is considered `disabled`. + /// + var innerError: InnerError? { get } +} + +extension ActionErrorProtocol { + public var isDisabled: Bool { + self.innerError == nil + } +} diff --git a/FueledUtils/ReactiveLifetimeProvider.swift b/FueledUtils/ReactiveLifetimeProvider.swift deleted file mode 100644 index 5fa490d3..00000000 --- a/FueledUtils/ReactiveLifetimeProvider.swift +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -import ReactiveCocoa -import ReactiveSwift - -/// -/// **Unavailable**: Please just make your type conform to `ReactiveExtensionsProvider`. -/// The goal of this protocol is to add RAC-style `reactive` proxy to Swift objects. -/// -@available(*, unavailable, renamed: "ReactiveExtensionsProvider") -public protocol ReactiveLifetimeProvider: ReactiveExtensionsProvider { - /// - /// The lifetime token associated with the instance. - /// - var lifetimeToken: Lifetime.Token { get } -} - -/// -/// **Unavailable**: Please just make your type a `class` that conform to `ReactiveExtensionsProvider` instead; there is no need to inherit from this anymore. -/// This base class adds RAC-style `reactive` proxy to Swift objects. -/// -@available(*, unavailable, message: "Make your type a class that conforms to ReactiveLifetimeProvider instead") -open class Lifetimed { -} - -extension Reactive where Base: AnyObject { - /// - /// **Unavailable**: Use `Lifetime.of()` instead - /// Get the lifetime associated with the instance. - /// - @available(*, unavailable, renamed: "Lifetime.of()") - public var lifetime: Lifetime { - return Lifetime.of(self.base) - } -} diff --git a/FueledUtils/ReactiveSwift/ActionError+ActionErrorProtocol.swift b/FueledUtils/ReactiveSwift/ActionError+ActionErrorProtocol.swift new file mode 100644 index 00000000..27e7ee47 --- /dev/null +++ b/FueledUtils/ReactiveSwift/ActionError+ActionErrorProtocol.swift @@ -0,0 +1,27 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import ReactiveSwift + +extension ReactiveSwift.ActionError: ActionErrorProtocol { + /// + /// The error the action protocol currently have. If `nil`, the action error is considered `disabled`. + /// + public var innerError: Error? { + if case .producerFailed(let error) = self { + return error + } + return nil + } +} diff --git a/FueledUtils/LoadingState.swift b/FueledUtils/ReactiveSwift/LoadingState.swift similarity index 54% rename from FueledUtils/LoadingState.swift rename to FueledUtils/ReactiveSwift/LoadingState.swift index 7f3793c8..564868fc 100644 --- a/FueledUtils/LoadingState.swift +++ b/FueledUtils/ReactiveSwift/LoadingState.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation import ReactiveSwift @@ -44,16 +43,6 @@ public enum LoadingState { } } - /// - /// **Unavailable**: Please use `isLoading` instead. - /// - /// Refer to the documentation for `isLoading` for more info. - /// - @available(*, unavailable, renamed: "isLoading") - public var loading: Bool { - return self.isLoading - } - /// /// If the current state is `.loading`, returns `true`. If not, returns `false` /// @@ -66,19 +55,7 @@ public enum LoadingState { } } -extension ActionProtocol { - /// - /// **Unavailable**: Please use `getSafely(at:)` instead. - /// - /// Refer to the documentation for `getSafely(at:)` for more info. - /// - @available(*, unavailable, renamed: "loadingState") - // The unused parameter allows to bypass the compiler error "Invalid redeclaration of 'loadingState'", - // while retaining backward compatibility - public func loadingState(_ unused: Void = ()) -> SignalProducer, Never> { - fatalError() - } - +extension ReactiveActionProtocol { /// /// Returns a `SignalProducer` whose events corresponds to the current loading state of the action. /// Please refer to `LoadingState` for more info. diff --git a/FueledUtils/ActionProtocol.swift b/FueledUtils/ReactiveSwift/ReactiveActionProtocol.swift similarity index 68% rename from FueledUtils/ActionProtocol.swift rename to FueledUtils/ReactiveSwift/ReactiveActionProtocol.swift index e32eb7fa..aa53008a 100644 --- a/FueledUtils/ActionProtocol.swift +++ b/FueledUtils/ReactiveSwift/ReactiveActionProtocol.swift @@ -1,57 +1,24 @@ +// Copyright © 2020, Fueled Digital Media, LLC // -// ActionProtocol.swift -// FueledUtils +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Stéphane Copin on 1/23/19. -// Copyright © 2019 Fueled. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. import Foundation import ReactiveSwift -/// -/// An optional protocol for `ActionError` for use in type constraints -/// -public protocol ActionErrorProtocol: Swift.Error { - /// - /// Type of the error associated with the action error. - /// - associatedtype SubError: Swift.Error - - /// - /// Whether the receiver is currently in the disabled state or not. - /// - var isDisabled: Bool { get } - - /// - /// The error the action protocol currently have. If `nil`, the action error is considered `disabled`. - /// - var error: SubError? { get } -} - -extension ActionError: ActionErrorProtocol { - /// - /// Whether the receiver is currently in the disabled state or not. - /// - public var isDisabled: Bool { - return self.error == nil - } - - /// - /// The error the action protocol currently have. If `nil`, the action error is considered `disabled`. - /// - public var error: Error? { - if case .producerFailed(let error) = self { - return error - } - return nil - } -} - /// /// A protocol for `Action`s for generic constraints and code reuse. /// -public protocol ActionProtocol { +public protocol ReactiveActionProtocol { /// /// The type of the values output from the action. /// @@ -118,10 +85,10 @@ public protocol ActionProtocol { func apply(_ input: Input) -> SignalProducer } -extension Action: ActionProtocol { +extension ReactiveSwift.Action: ReactiveActionProtocol { } -extension ActionProtocol where Input == Void { +extension ReactiveActionProtocol where Input == Void { /// /// Create a `SignalProducer` that would attempt to create and start a unit of work of /// the `Action`. The `SignalProducer` would forward only events generated by the unit diff --git a/FueledUtils/ReactiveSwift/ReactiveAnyAction.swift b/FueledUtils/ReactiveSwift/ReactiveAnyAction.swift new file mode 100644 index 00000000..075fb28d --- /dev/null +++ b/FueledUtils/ReactiveSwift/ReactiveAnyAction.swift @@ -0,0 +1,47 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import ReactiveSwift + +/// +/// A type-erased Action that allows to store any `ReactiveActionProtocol` +/// (loosing any type information at the same time) +/// +final class ReactiveAnyAction: ReactiveActionProtocol { + private let applyClosure: (Input) -> SignalProducer + private let deinitToken: Lifetime.Token + + init(_ action: Action) { + self.isEnabled = action.isEnabled + self.isExecuting = action.isExecuting + self.applyClosure = { action.apply($0 as! Action.Input).map { $0 }.mapError { $0 } } + self.events = action.events.map { $0.map { $0 }.mapError { $0 } } + self.values = action.values.map { $0 } + self.errors = action.errors.map { $0 } + (self.lifetime, self.deinitToken) = Lifetime.make() + } + + let isEnabled: Property + let isExecuting: Property + + let events: Signal.Event, Never> + let values: Signal + let errors: Signal + + let lifetime: Lifetime + + func apply(_ input: Any) -> SignalProducer { + self.applyClosure(input) + } +} diff --git a/FueledUtils/InputCoalescingAction.swift b/FueledUtils/ReactiveSwift/ReactiveCoalescingAction.swift similarity index 82% rename from FueledUtils/InputCoalescingAction.swift rename to FueledUtils/ReactiveSwift/ReactiveCoalescingAction.swift index 364eb0e4..be08060c 100644 --- a/FueledUtils/InputCoalescingAction.swift +++ b/FueledUtils/ReactiveSwift/ReactiveCoalescingAction.swift @@ -1,20 +1,19 @@ +// Copyright © 2020, Fueled Digital Media, LLC // -// InputCoalescingAction.swift -// FueledUtils +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Stéphane Copin on 4/22/16. -// Copyright © 2016 Fueled. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. import ReactiveSwift -/// -/// Similar to `Action`, except if the action is already executing, subsequent `apply()` call will not fail, -/// and will be completed with the same output when the initial executing action completes. -/// Disposing any of the `SignalProducer` returned by 'apply()` will cancel the action. -/// -public typealias CoalescingAction = InputCoalescingAction - /// /// Similar to `Action`, except if the action is already executing, subsequent `apply()` call will not fail, /// and will be completed with the same output when the initial executing action completes. @@ -24,20 +23,20 @@ public typealias CoalescingAction = InputCoalescingA /// calls to `apply()` when the action is executing, the inputs will be ignored until /// the action terminates. /// -public class InputCoalescingAction: ActionProtocol { - private let action: Action +public class ReactiveCoalescingAction: ReactiveActionProtocol { + private let action: ReactiveSwift.Action private var observer: Signal.Observer? private class DisposableContainer { private let disposable: Disposable - private let count = Atomic(0) + private let count = AtomicValue(0) init(_ disposable: Disposable) { self.disposable = disposable } func add(_ lifetime: Lifetime) { - self.count.value += 1 + self.count.modify { $0 += 1 } lifetime.observeEnded { self.count.modify { $0 -= 1 @@ -111,7 +110,7 @@ public class InputCoalescingAction: ActionPro /// executed by the `Action`. /// public init(execute: @escaping (Input) -> SignalProducer) { - self.action = Action(execute: execute) + self.action = ReactiveSwift.Action(execute: execute) } /// diff --git a/FueledUtils/ReactiveCocoaExtensions.swift b/FueledUtils/ReactiveSwift/ReactiveCocoaExtensions.swift similarity index 76% rename from FueledUtils/ReactiveCocoaExtensions.swift rename to FueledUtils/ReactiveSwift/ReactiveCocoaExtensions.swift index 27a6f25c..6998d928 100644 --- a/FueledUtils/ReactiveCocoaExtensions.swift +++ b/FueledUtils/ReactiveSwift/ReactiveCocoaExtensions.swift @@ -1,27 +1,30 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation import ReactiveCocoa import ReactiveSwift +#if canImport(UIKit) import UIKit +#elseif canImport(AppKit) +#endif /// /// Use with `SignalProtocol.observe(context:)` or `SignalProducerProtocol.observe(context:)` below to animate /// all changes made by observers of the signal returned from `observe(context:)`. /// +#if os(iOS) public func animatingContext( _ duration: TimeInterval, delay: TimeInterval = 0, @@ -65,7 +68,9 @@ public func transitionContext( completion: completion) } } +#endif +#if !os(watchOS) extension Reactive where Base: NSLayoutConstraint { /// /// Set whether the constant is active or not in its hierarchy. @@ -74,7 +79,9 @@ extension Reactive where Base: NSLayoutConstraint { return makeBindingTarget { $0.isActive = $1 } } } +#endif +#if os(iOS) extension Reactive where Base: UIView { /// /// Update the `alpha` property of the view with an animation. @@ -99,23 +106,6 @@ extension Reactive where Base: UIView { } extension Reactive where Base: UILabel { - /// - /// Update the `text` property of the label with an animation. - /// - public var animatedText: BindingTarget { - return makeBindingTarget { label, text in - label.setText(text, animated: true) - } - } - /// - /// Update the `attributedText` property of the label with an animation. - /// - public var animatedAttributedText: BindingTarget { - return makeBindingTarget { label, text in - label.setAttributedText(text, animated: true) - } - } - /// /// Update the `textAlignment` property of the label with an animation. /// @@ -139,19 +129,6 @@ extension Reactive where Base: UIViewController { } } -@available(iOS 9.0, *) -extension Reactive where Base: UIStackView { - /// - /// **Unavailable**: Use `subview.reactive.isHidden <~ ` instead. - /// Add/remove/modify the order of the arranged subviews by specified the subview. - /// - @available(*, unavailable, message: "Use `subview.reactive.isHidden <~ ` instead") - public func isArranged(_ subview: UIView, at index: Int) -> BindingTarget { - fatalError() - } -} - -#if os(iOS) extension Reactive where Base: UINavigationItem { /// /// Show/hide the back button, optionally with an animation. diff --git a/FueledUtils/ReactiveSwift/ReactiveOverridingAction.swift b/FueledUtils/ReactiveSwift/ReactiveOverridingAction.swift new file mode 100644 index 00000000..e8a81c3d --- /dev/null +++ b/FueledUtils/ReactiveSwift/ReactiveOverridingAction.swift @@ -0,0 +1,146 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import ReactiveSwift + +/// +/// Similar to `Action`, except if the action is already executing, subsequent `apply()` call will not fail, +/// and will be interrupt the previous apply(). +/// +public final class ReactiveOverridingAction: ReactiveActionProtocol { + private let action: ReactiveSwift.Action + private var observer: Signal.Observer? + + private let currentDisposable = SerialDisposable(nil) + + /// + /// Whether the action is currently executing. + /// + public var isExecuting: Property { + self.action.isExecuting + } + + /// + /// Whether the action is enabled. + /// + public var isEnabled: Property { + self.action.isEnabled + } + + /// + /// A signal of all events generated from all units of work of the `Action`. + /// + /// In other words, this sends every `Event` from every unit of work that the `Action` + /// executes. + /// + public var events: Signal.Event, Never> { + self.action.events + } + + /// + /// A signal of all values generated from all units of work of the `Action`. + /// + /// In other words, this sends every value from every unit of work that the `Action` + /// executes. + /// + public var values: Signal { + self.action.values + } + + /// + /// A signal of all errors generated from all units of work of the `Action`. + /// + /// In other words, this sends every error from every unit of work that the `Action` + /// executes. + /// + public var errors: Signal { + self.action.errors + } + + /// + /// The lifetime of the `Action`. + /// + public var lifetime: Lifetime { + self.action.lifetime + } + + /// + /// Initializes an `OverridingAction`. + /// + /// When the `Action` is asked to start the execution with an input value, a unit of + /// work — represented by a `SignalProducer` — would be created by invoking + /// `execute` with the input value. + /// + /// - parameters: + /// - execute: A closure that produces a unit of work, as `SignalProducer`, to be + /// executed by the `Action`. + /// + public convenience init(execute: @escaping (Input) -> SignalProducer) { + self.init(enabledIf: Property(value: true), execute: execute) + } + + /// + /// Initializes an `OverridingAction`. + /// + /// When the `Action` is asked to start the execution with an input value, a unit of + /// work — represented by a `SignalProducer` — would be created by invoking + /// `execute` with the input value. + /// + /// - parameters: + /// - isEnabled: A property which determines the availability of the `Action`. + /// - execute: A closure that produces a unit of work, as `SignalProducer`, to be + /// executed by the `Action`. + /// + public init( + enabledIf isEnabled: Property, + execute: @escaping (Input) -> SignalProducer + ) + where Property.Value == Bool + { + self.action = ReactiveSwift.Action(enabledIf: isEnabled, execute: execute) + } + + /// + /// Create a `SignalProducer` that would attempt to create and start a unit of work of + /// the `Action`. The `SignalProducer` would forward only events generated by the unit + /// of work it created. + /// + /// - Warning: Only the first call to `apply()` when the action's `isExecuting`'s `value` is `false` will be using its parameters. + /// Subsequent calls when the action is already executing will ignore the input. + /// + /// - Parameters: + /// - input: The initial input to use for the action. + /// + /// - Returns: A producer that forwards events generated by its started unit of work. If the action was already executing, it will create a `SignalProducer` + /// that will forward the events of the initially created `SignalProducer`. + /// + public func apply(_ input: Input) -> SignalProducer { + SignalProducer { observer, lifetime in + self.currentDisposable.inner = nil + + self.observer = observer + self.currentDisposable.inner = self.action.apply(input) + .flatMapError { error in + guard case .producerFailed(let innerError) = error else { + return SignalProducer.empty + } + + return SignalProducer(error: innerError) + } + .start(observer) + + lifetime += self.currentDisposable.inner + } + } +} diff --git a/FueledUtils/ReactiveSwiftExtensions.swift b/FueledUtils/ReactiveSwift/ReactiveSwiftExtensions.swift similarity index 97% rename from FueledUtils/ReactiveSwiftExtensions.swift rename to FueledUtils/ReactiveSwift/ReactiveSwiftExtensions.swift index a5aea499..99645a42 100644 --- a/FueledUtils/ReactiveSwiftExtensions.swift +++ b/FueledUtils/ReactiveSwift/ReactiveSwiftExtensions.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation import ReactiveSwift @@ -530,7 +529,7 @@ infix operator <~> : AssignmentPrecedence return disposable } -extension ActionProtocol { +extension ReactiveActionProtocol { /// /// A signal of all values or errors generated from all units of work of the `Action`. /// diff --git a/FueledUtils/TransferState.swift b/FueledUtils/ReactiveSwift/TransferState+Reactive.swift similarity index 64% rename from FueledUtils/TransferState.swift rename to FueledUtils/ReactiveSwift/TransferState+Reactive.swift index d1482dc9..91be0e0f 100644 --- a/FueledUtils/TransferState.swift +++ b/FueledUtils/ReactiveSwift/TransferState+Reactive.swift @@ -1,55 +1,19 @@ +// Copyright © 2020, Fueled Digital Media, LLC // -// LoadingStatus.swift -// AlterraCommon +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Stéphane Copin on 10/18/17. -// Copyright © 2017 Fueled. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -import Foundation import ReactiveSwift -/// -/// Represents a transfer state, either loading or finished. -/// This is useful to represent a transfer state in percentage or bytes uploaded for example. -/// -/// It is recommended to define a `typealias` corresponding to your use case that specify at least the `Progress` type, for example: -/// ```swift -/// typealias DownloadState = TransferState -/// ``` -/// or -/// ```swift -/// typealias DownloadState = TransferState<(uploadedBytes: UInt64, totalBytes: UInt64)?, Value> -/// ``` -/// Nothing prevents you to also include the `Value` type in the `typealias`, if possible for your use case. -/// -public enum TransferState { - /// - /// Represents a `loading` state. - /// - case loading(Progress) - /// - /// Represents a `finished` state. - /// - case finished(Value) - - /// - /// Map a `TransferState` finishing with one `Value` into another, mapping it with the given closure. - /// - /// - Parameters: - /// - mapper: Mapper closure how to map the initial value to the mapped value. - /// - Returns: A `TransferState` with the mapped value as mapped by the given closure. - /// - public func map(_ mapper: (Value) -> Mapped) -> TransferState { - switch self { - case .loading(let progress): - return .loading(progress) - case .finished(let result): - return .finished(mapper(result)) - } - } -} - extension SignalProtocol { /// /// Converts a `Signal` that has a `TransferState` value into a `Signal` only returning a value when `finished()` is received. @@ -57,7 +21,7 @@ extension SignalProtocol { public func ignoreLoading() -> Signal where Self.Value == TransferState { - return self.signal.filterMap { status in + return self.signal.compactMap { status in switch status { case .loading: return nil diff --git a/FueledUtils/TypedSerialDisposable.swift b/FueledUtils/ReactiveSwift/TypedSerialDisposable.swift similarity index 86% rename from FueledUtils/TypedSerialDisposable.swift rename to FueledUtils/ReactiveSwift/TypedSerialDisposable.swift index cc9ab83a..57282be4 100644 --- a/FueledUtils/TypedSerialDisposable.swift +++ b/FueledUtils/ReactiveSwift/TypedSerialDisposable.swift @@ -1,10 +1,16 @@ +// Copyright © 2020, Fueled Digital Media, LLC // -// TypedSerialDisposable.swift -// FueledUtils +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Stéphane Copin on 11/7/18. -// Copyright © 2018 Fueled. All rights reserved. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. import ReactiveSwift diff --git a/FueledUtils/ReactiveSwiftUIKit/ReactiveControlProtocol+Tapped.swift b/FueledUtils/ReactiveSwiftUIKit/ReactiveControlProtocol+Tapped.swift new file mode 100644 index 00000000..7c9912d5 --- /dev/null +++ b/FueledUtils/ReactiveSwiftUIKit/ReactiveControlProtocol+Tapped.swift @@ -0,0 +1,71 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(UIKit) && !os(watchOS) +import ReactiveCocoa +import ReactiveSwift + +private var tapActionStorage: UInt8 = 0 +private var tapActionKey: UInt8 = 0 + +extension Reactive where Base: ControlProtocol { + /// + /// The action to be triggered when the button is tapped. + /// This mirrors the `pressed` property native in `ReactiveCocoa`, but uses a + /// protocol to represents the button rather than hardcode it to classes, + /// allowing for any `UIControl` to use this method. + /// + public var tapped: ReactiveTapAction? { + get { + self.tapActionStorage?.tapAction + } + nonmutating set { + self.tapActionStorage = nil + + if let newValue = newValue { + let tapActionStorage = TapActionStorage(newValue) + tapActionStorage.disposable += self.makeBindingTarget { control, isEnabled in + control.isEnabled = isEnabled + } <~ newValue.isEnabled + if self.base is ControlLoadingProtocol { + tapActionStorage.disposable += self.makeBindingTarget { control, isExecuting in + (control as! ControlLoadingProtocol).isLoading = isExecuting + } <~ newValue.isExecuting + } + self.base.removeTarget(newValue, action: ReactiveTapAction.selector, for: .primaryActionTriggered) + self.base.addTarget(newValue, action: ReactiveTapAction.selector, for: .primaryActionTriggered) + self.tapActionStorage = tapActionStorage + } + } + } + + private var tapActionStorage: TapActionStorage? { + get { + objc_getAssociatedObject(self.base, &tapActionKey) as? TapActionStorage + } + nonmutating set { + objc_setAssociatedObject(self.base, &tapActionKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } +} + +private final class TapActionStorage { + let tapAction: ReactiveTapAction + let disposable = ScopedDisposable(CompositeDisposable()) + + init(_ tapAction: ReactiveTapAction) { + self.tapAction = tapAction + } +} +#endif diff --git a/FueledUtils/ReactiveSwiftUIKit/ReactiveTapAction.swift b/FueledUtils/ReactiveSwiftUIKit/ReactiveTapAction.swift new file mode 100644 index 00000000..705bfda1 --- /dev/null +++ b/FueledUtils/ReactiveSwiftUIKit/ReactiveTapAction.swift @@ -0,0 +1,60 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(UIKit) && !os(watchOS) +import ReactiveSwift + +/// +/// `ReactiveTapAction` wraps a `ReactiveActionProtocol` for use by any `ButtonProtocol` +/// This is a mirror of `CococaAction` in `ReactiveCocoa`, allowing to use a +/// `ButtonProtocol` and assigin +/// +public final class ReactiveTapAction: NSObject { + @objc static var selector: Selector { + #selector(userDidTapControl(_:)) + } + + public let isExecuting: Property + public let isEnabled: Property + + private let executeClosure: (Control) -> Void + + public convenience init(_ action: Action) where Action.Input == Void { + self.init(action, input: ()) + } + + public convenience init(_ action: Action, input: Action.Input) { + self.init(action) { _ in input } + } + + public init(_ action: Action, inputTransform: @escaping (Control) -> Action.Input) { + self.executeClosure = { + action.apply(inputTransform($0)).start() + } + + self.isEnabled = Property( + initial: action.isEnabled.value, + then: action.isEnabled.producer.observe(on: UIScheduler()) + ) + self.isExecuting = Property( + initial: action.isExecuting.value, + then: action.isExecuting.producer.observe(on: UIScheduler()) + ) + } + + @objc private func userDidTapControl(_ button: Any) { + self.executeClosure(button as! Control) + } +} +#endif diff --git a/FueledUtils/SignalingAlert.swift b/FueledUtils/ReactiveSwiftUIKit/SignalingAlert.swift similarity index 85% rename from FueledUtils/SignalingAlert.swift rename to FueledUtils/ReactiveSwiftUIKit/SignalingAlert.swift index 404d3e4b..f15cb756 100644 --- a/FueledUtils/SignalingAlert.swift +++ b/FueledUtils/ReactiveSwiftUIKit/SignalingAlert.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation import ReactiveCocoa import ReactiveSwift diff --git a/FueledUtils/ReactiveSwiftUIKit/UIReactiveExtensions.swift b/FueledUtils/ReactiveSwiftUIKit/UIReactiveExtensions.swift new file mode 100644 index 00000000..4ecc9b23 --- /dev/null +++ b/FueledUtils/ReactiveSwiftUIKit/UIReactiveExtensions.swift @@ -0,0 +1,36 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import ReactiveSwift +import UIKit + +extension Reactive where Base: UILabel { + /// + /// Update the `text` property of the label with an animation. + /// + public var animatedText: BindingTarget { + return makeBindingTarget { label, text in + label.setText(text, animated: true) + } + } + /// + /// Update the `attributedText` property of the label with an animation. + /// + public var animatedAttributedText: BindingTarget { + return makeBindingTarget { label, text in + label.setAttributedText(text, animated: true) + } + } +} diff --git a/FueledUtils/SwiftUI/BackgroundBlur.swift b/FueledUtils/SwiftUI/BackgroundBlur.swift new file mode 100644 index 00000000..379daeb1 --- /dev/null +++ b/FueledUtils/SwiftUI/BackgroundBlur.swift @@ -0,0 +1,55 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension View { + #if canImport(UIKit) && !os(watchOS) + public func backgroundBlur(style: UIBlurEffect.Style, color: Color? = nil) -> some View { + ZStack { + color + BlurView(style: style) + self + .eraseToAnyView() + } + } + #elseif canImport(AppKit) + @available(macOS 10.15, *) + public func backgroundBlur( + material: NSVisualEffectView.Material = .appearanceBased, + blendingMode: NSVisualEffectView.BlendingMode = .behindWindow, + state: NSVisualEffectView.State = .followsWindowActiveState, + maskImage: NSImage? = nil, + isEmphasized: Bool = false, + color: Color? = nil + ) -> some View { + ZStack { + color + BlurView( + material: material, + blendingMode: blendingMode, + state: state, + maskImage: maskImage, + isEmphasized: isEmphasized + ) + self + .eraseToAnyView() + } + } + #endif +} + +#endif diff --git a/FueledUtils/SwiftUI/Binding+KeyPath.swift b/FueledUtils/SwiftUI/Binding+KeyPath.swift new file mode 100644 index 00000000..66fe50e3 --- /dev/null +++ b/FueledUtils/SwiftUI/Binding+KeyPath.swift @@ -0,0 +1,32 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension Binding { + public init(_ object: Type, to keyPath: ReferenceWritableKeyPath) { + self.init( + get: { + object[keyPath: keyPath] + }, + set: { + object[keyPath: keyPath] = $0 + } + ) + } +} + +#endif diff --git a/FueledUtils/SwiftUI/BlurView.swift b/FueledUtils/SwiftUI/BlurView.swift new file mode 100644 index 00000000..6cbcdc7a --- /dev/null +++ b/FueledUtils/SwiftUI/BlurView.swift @@ -0,0 +1,78 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(SwiftUI) +import SwiftUI +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + +#if canImport(UIKit) && !os(watchOS) +@available(iOS 13.0, tvOS 13.0, *) +public struct BlurView: UIViewRepresentable { + public let style: UIBlurEffect.Style + + public init(style: UIBlurEffect.Style) { + self.style = style + } + + public func makeUIView(context: Context) -> UIVisualEffectView { + UIVisualEffectView(effect: UIBlurEffect(style: self.style)) + } + + public func updateUIView(_ visualEffectView: UIVisualEffectView, context: Context) { + visualEffectView.effect = UIBlurEffect(style: self.style) + } +} +#elseif canImport(AppKit) +@available(macOS 10.15, *) +public struct BlurView: NSViewRepresentable { + public let material: NSVisualEffectView.Material + public let blendingMode: NSVisualEffectView.BlendingMode + public let state: NSVisualEffectView.State + public let maskImage: NSImage? + public let isEmphasized: Bool + + public init( + material: NSVisualEffectView.Material = .appearanceBased, + blendingMode: NSVisualEffectView.BlendingMode = .behindWindow, + state: NSVisualEffectView.State = .followsWindowActiveState, + maskImage: NSImage? = nil, + isEmphasized: Bool = false + ) { + self.material = material + self.blendingMode = blendingMode + self.state = state + self.maskImage = maskImage + self.isEmphasized = isEmphasized + } + + public func makeNSView(context: Context) -> NSVisualEffectView { + NSVisualEffectView() + } + + public func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) { + visualEffectView.material = self.material + visualEffectView.blendingMode = self.blendingMode + visualEffectView.state = self.state + visualEffectView.maskImage = self.maskImage + visualEffectView.isEmphasized = self.isEmphasized + } +} + +#endif + +#endif diff --git a/FueledUtils/SwiftUI/EdgeInsets+Helpers.swift b/FueledUtils/SwiftUI/EdgeInsets+Helpers.swift new file mode 100644 index 00000000..3352912d --- /dev/null +++ b/FueledUtils/SwiftUI/EdgeInsets+Helpers.swift @@ -0,0 +1,38 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension EdgeInsets { + public static var zero: EdgeInsets { + EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 0.0) + } + + public init(_ length: CGFloat) { + self.init(top: length, leading: length, bottom: length, trailing: length) + } + + public init(_ edges: Edge.Set, _ length: CGFloat) { + self.init( + top: edges.contains(.top) ? length : 0.0, + leading: edges.contains(.leading) ? length : 0.0, + bottom: edges.contains(.bottom) ? length : 0.0, + trailing: edges.contains(.trailing) ? length : 0.0 + ) + } +} + +#endif diff --git a/FueledUtils/SwiftUI/ForEachWithIndex.swift b/FueledUtils/SwiftUI/ForEachWithIndex.swift new file mode 100644 index 00000000..9e2e8767 --- /dev/null +++ b/FueledUtils/SwiftUI/ForEachWithIndex.swift @@ -0,0 +1,76 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public struct ForEachWithIndex: View { + public var data: Data + public var content: (_ index: Data.Index, _ element: Data.Element) -> Content + var id: KeyPath + + public init(_ data: Data, id: KeyPath, content: @escaping (_ index: Data.Index, _ element: Data.Element) -> Content) { + self.data = data + self.id = id + self.content = content + } + + public var body: some View { + ForEach( + zip(self.data.indices, self.data).map { index, element in + IndexInfo( + index: index, + id: self.id, + element: element + ) + }, + id: \.elementID + ) { indexInfo in + self.content(indexInfo.index, indexInfo.element) + } + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension ForEachWithIndex where ID == Data.Element.ID, Content: View, Data.Element: Identifiable { + public init(_ data: Data, @ViewBuilder content: @escaping (_ index: Data.Index, _ element: Data.Element) -> Content) { + self.init(data, id: \.id, content: content) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension ForEachWithIndex: DynamicViewContent where Content: View { +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +private struct IndexInfo: Hashable { + let index: Index + let id: KeyPath + let element: Element + + var elementID: ID { + self.element[keyPath: self.id] + } + + static func == (_ lhs: IndexInfo, _ rhs: IndexInfo) -> Bool { + lhs.elementID == rhs.elementID + } + + func hash(into hasher: inout Hasher) { + self.elementID.hash(into: &hasher) + } +} + +#endif diff --git a/FueledUtils/SwiftUI/FramePreferenceKey.swift b/FueledUtils/SwiftUI/FramePreferenceKey.swift new file mode 100644 index 00000000..812564b9 --- /dev/null +++ b/FueledUtils/SwiftUI/FramePreferenceKey.swift @@ -0,0 +1,33 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(SwiftUI) +import SwiftUI + +/// +/// Used to retrieve the frame of a view through a preference key. +/// `TagType` is used to uniquely identify the view using the preference key. +/// +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public struct FramePreferenceKey: PreferenceKey { + public static var defaultValue: CGRect { + .zero + } + + public static func reduce(value: inout CGRect, nextValue: () -> CGRect) { + value = nextValue() + } +} + +#endif diff --git a/FueledUtils/SwiftUI/View+AnyView.swift b/FueledUtils/SwiftUI/View+AnyView.swift new file mode 100644 index 00000000..49ecd3f4 --- /dev/null +++ b/FueledUtils/SwiftUI/View+AnyView.swift @@ -0,0 +1,25 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension View { + public func eraseToAnyView() -> AnyView { + AnyView(self) + } +} + +#endif diff --git a/FueledUtils/ButtonWithTitleAdjustment.swift b/FueledUtils/UIKit/ButtonWithTitleAdjustment.swift similarity index 83% rename from FueledUtils/ButtonWithTitleAdjustment.swift rename to FueledUtils/UIKit/ButtonWithTitleAdjustment.swift index 9453c9ec..4bd7fc97 100644 --- a/FueledUtils/ButtonWithTitleAdjustment.swift +++ b/FueledUtils/UIKit/ButtonWithTitleAdjustment.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import UIKit /// diff --git a/FueledUtils/UIKit/ControlProtocol.swift b/FueledUtils/UIKit/ControlProtocol.swift new file mode 100644 index 00000000..e9f869ef --- /dev/null +++ b/FueledUtils/UIKit/ControlProtocol.swift @@ -0,0 +1,34 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(UIKit) && !os(watchOS) +import UIKit + +/// +/// A protocol that represents a control, which must be a `UIControl`. +/// +public protocol ControlProtocol: UIControl { +} + +/// +/// A protocol that represents a control with a `isLoading` property. +/// +public protocol ControlLoadingProtocol: ControlProtocol { + var isLoading: Bool { get set } +} + +// Make all `UIControl` a `ControlProtocol` by default. +extension UIControl: ControlProtocol { +} +#endif diff --git a/FueledUtils/DecoratingTextFieldDelegate.swift b/FueledUtils/UIKit/DecoratingTextFieldDelegate.swift similarity index 92% rename from FueledUtils/DecoratingTextFieldDelegate.swift rename to FueledUtils/UIKit/DecoratingTextFieldDelegate.swift index e69c1790..348a0b5e 100644 --- a/FueledUtils/DecoratingTextFieldDelegate.swift +++ b/FueledUtils/UIKit/DecoratingTextFieldDelegate.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import UIKit import Foundation diff --git a/FueledUtils/DimmingButton.swift b/FueledUtils/UIKit/DimmingButton.swift similarity index 63% rename from FueledUtils/DimmingButton.swift rename to FueledUtils/UIKit/DimmingButton.swift index 0153e0e8..08faf380 100644 --- a/FueledUtils/DimmingButton.swift +++ b/FueledUtils/UIKit/DimmingButton.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation import UIKit diff --git a/FueledUtils/GradientView.swift b/FueledUtils/UIKit/GradientView.swift similarity index 93% rename from FueledUtils/GradientView.swift rename to FueledUtils/UIKit/GradientView.swift index 0ff78ce4..baae13d8 100644 --- a/FueledUtils/GradientView.swift +++ b/FueledUtils/UIKit/GradientView.swift @@ -1,3 +1,17 @@ +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import UIKit /// diff --git a/FueledUtils/HairlineView.swift b/FueledUtils/UIKit/HairlineView.swift similarity index 53% rename from FueledUtils/HairlineView.swift rename to FueledUtils/UIKit/HairlineView.swift index 934d0f66..3af3fdfd 100644 --- a/FueledUtils/HairlineView.swift +++ b/FueledUtils/UIKit/HairlineView.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation import UIKit diff --git a/FueledUtils/KeyboardInsetHelper.swift b/FueledUtils/UIKit/KeyboardInsetHelper.swift similarity index 86% rename from FueledUtils/KeyboardInsetHelper.swift rename to FueledUtils/UIKit/KeyboardInsetHelper.swift index d209752b..e6ada319 100644 --- a/FueledUtils/KeyboardInsetHelper.swift +++ b/FueledUtils/UIKit/KeyboardInsetHelper.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation import UIKit diff --git a/FueledUtils/LabelWithTitleAdjustment.swift b/FueledUtils/UIKit/LabelWithTitleAdjustment.swift similarity index 81% rename from FueledUtils/LabelWithTitleAdjustment.swift rename to FueledUtils/UIKit/LabelWithTitleAdjustment.swift index e125c45b..cae83a7a 100644 --- a/FueledUtils/LabelWithTitleAdjustment.swift +++ b/FueledUtils/UIKit/LabelWithTitleAdjustment.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import UIKit import Foundation diff --git a/FueledUtils/ScrollViewPage.swift b/FueledUtils/UIKit/ScrollViewPage.swift similarity index 79% rename from FueledUtils/ScrollViewPage.swift rename to FueledUtils/UIKit/ScrollViewPage.swift index a32ca4aa..f9ad5033 100644 --- a/FueledUtils/ScrollViewPage.swift +++ b/FueledUtils/UIKit/ScrollViewPage.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import UIKit extension UIScrollView { diff --git a/FueledUtils/SetRootViewController.swift b/FueledUtils/UIKit/SetRootViewController.swift similarity index 65% rename from FueledUtils/SetRootViewController.swift rename to FueledUtils/UIKit/SetRootViewController.swift index 2f37ef41..84401c4a 100644 --- a/FueledUtils/SetRootViewController.swift +++ b/FueledUtils/UIKit/SetRootViewController.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation import UIKit diff --git a/FueledUtils/UIExtensions.swift b/FueledUtils/UIKit/UIExtensions.swift similarity index 91% rename from FueledUtils/UIExtensions.swift rename to FueledUtils/UIKit/UIExtensions.swift index a0d45a2a..d7f896aa 100644 --- a/FueledUtils/UIExtensions.swift +++ b/FueledUtils/UIKit/UIExtensions.swift @@ -1,18 +1,17 @@ -/* -Copyright © 2019 Fueled Digital Media, LLC +// Copyright © 2020, Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ import Foundation import UIKit @@ -34,7 +33,7 @@ extension CGSize { /// In all other cases, the receiver is returned and `size` is ignored. /// - Returns: The scaled size as specified by the parameters. /// - func scaled(to size: CGSize, contentMode: UIView.ContentMode = .scaleToFill) -> CGSize { + public func scaled(to size: CGSize, contentMode: UIView.ContentMode = .scaleToFill) -> CGSize { switch contentMode { case .redraw, .scaleToFill: @@ -236,14 +235,24 @@ extension UITextField { extension UIView { /// /// Adds the given subview into the receiver, and adds constraint so that its top, bottom, left and right's edges are bounds to its superview's edges. + /// - Parameters: + /// - insets: Optionally apply an offset when adding the view. + /// - Returns: The image if it could be generated. If it couldn't, for example if the `UIView`'s width or height is 0, a crash will happen at runtime. + /// + /// - Note: The returns is an implicitely unwrapped optional for backward-compatibility purpose, and will be made an optional in a future release (as well as not crash) /// - public func addAndFitSubview(_ view: UIView) { + public func addAndFitSubview(_ view: UIView, insets: UIEdgeInsets = .zero) { view.translatesAutoresizingMaskIntoConstraints = false - view.frame = self.bounds + view.frame = self.bounds.inset(by: insets) self.addSubview(view) - let views = ["view": view] - self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|", options: [], metrics: nil, views: views)) - self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|", options: [], metrics: nil, views: views)) + self.addConstraints( + [ + NSLayoutConstraint(item: view, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1.0, constant: insets.left), + NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1.0, constant: -insets.right), + NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: insets.top), + NSLayoutConstraint(item: view, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -insets.bottom), + ] + ) } /// @@ -300,7 +309,7 @@ extension UIView { /// /// - Note: It is safe to call this method multiple times without calling `removeSketchShadow` between each calls. /// - func applySketchShadow( + public func applySketchShadow( color: UIColor = .black, alpha: Float = 0.5, xAxis: CGFloat = 0.0, @@ -328,7 +337,7 @@ extension UIView { /// Remove a shadow as set by `applySketchShadow` /// This method will reset any shadows sets on the backing layer, _even it wasn't applied by `applySketchShadow` /// - func removeSketchShadow() { + public func removeSketchShadow() { self.layer.shadowColor = nil self.layer.shadowOpacity = 0.0 self.layer.shadowOffset = .zero @@ -346,8 +355,9 @@ extension UIStackView { /// - Parameters: /// - removeFromHierachy: If `true`, each views is also removed from the receiver using `removeFromSuperview()`. /// If `false`, `removeFromSuperview()` is not called. + /// This parameters defaults to `true`. /// - func removeArrangedSubviews(removeFromHierachy: Bool) { + public func removeArrangedSubviews(removeFromHierachy: Bool = true) { let arrangedSubviews = self.arrangedSubviews arrangedSubviews.forEach { self.removeArrangedSubview($0, removeFromHierachy: removeFromHierachy) } } @@ -361,7 +371,7 @@ extension UIStackView { /// - removeFromHierachy: If `true`, the view is also removed from the receiver using `removeFromSuperview()`. /// If `false`, `removeFromSuperview()` is not called. /// - func removeArrangedSubview(_ view: UIView, removeFromHierachy: Bool) { + public func removeArrangedSubview(_ view: UIView, removeFromHierachy: Bool) { if removeFromHierachy { view.removeFromSuperview() } else { diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..cb45b4ed --- /dev/null +++ b/Gemfile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +gem 'danger' + diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..42b17db2 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,61 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + claide (1.0.3) + claide-plugins (0.9.2) + cork + nap + open4 (~> 1.3) + colored2 (3.1.2) + cork (0.3.0) + colored2 (~> 3.1) + danger (8.0.6) + claide (~> 1.0) + claide-plugins (>= 0.9.2) + colored2 (~> 3.1) + cork (~> 0.1) + faraday (>= 0.9.0, < 2.0) + faraday-http-cache (~> 2.0) + git (~> 1.7) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.0) + no_proxy_fix + octokit (~> 4.7) + terminal-table (~> 1) + faraday (1.0.1) + multipart-post (>= 1.2, < 3) + faraday-http-cache (2.2.0) + faraday (>= 0.8) + git (1.11.0) + rchardet (~> 1.8) + kramdown (2.3.1) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + multipart-post (2.1.1) + nap (1.1.0) + no_proxy_fix (0.1.2) + octokit (4.18.0) + faraday (>= 0.9) + sawyer (~> 0.8.0, >= 0.5.3) + open4 (1.3.4) + public_suffix (4.0.6) + rchardet (1.8.0) + rexml (3.2.5) + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + unicode-display_width (1.7.0) + +PLATFORMS + ruby + +DEPENDENCIES + danger + +BUNDLED WITH + 2.1.4 diff --git a/LICENSE b/LICENSE index 0c8a8002..b3950782 100644 --- a/LICENSE +++ b/LICENSE @@ -1,53 +1,201 @@ -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of this License; and -You must cause any modified files to carry prominent notices stating that You changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - -You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020, Fueled Digital Media, LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Tests/FueledUtils.xcodeproj/project.pbxproj b/Tests/FueledUtils.xcodeproj/project.pbxproj new file mode 100644 index 00000000..d7d2f3df --- /dev/null +++ b/Tests/FueledUtils.xcodeproj/project.pbxproj @@ -0,0 +1,445 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + EE9B3A498587AC2D3461D310 /* Pods_Common_FueledUtilsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F46CDEA885028D8DB189B3CB /* Pods_Common_FueledUtilsTests.framework */; }; + F453AA7B2538DE55008F045B /* CombineLatestManySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453AA7A2538DE55008F045B /* CombineLatestManySpec.swift */; }; + F453AA7F2538E05E008F045B /* ReactiveSwiftExtensionsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F463C73B241835DD000A0B29 /* ReactiveSwiftExtensionsSpec.swift */; }; + F453AA802538E05E008F045B /* OrderedSetSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46D2DA5252E4E4A00B6987A /* OrderedSetSpec.swift */; }; + F453AA812538E05E008F045B /* CoalescingActionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F499631F2537459200E2D4B5 /* CoalescingActionSpec.swift */; }; + F453AA822538E05E008F045B /* OverridingActionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F429742D25378425004BFA85 /* OverridingActionSpec.swift */; }; + F453AA832538E05E008F045B /* ReactiveCoalescingActionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F463C73A241835DD000A0B29 /* ReactiveCoalescingActionSpec.swift */; }; + F453AA842538E05E008F045B /* ReactiveOverridingActionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F429742925378348004BFA85 /* ReactiveOverridingActionSpec.swift */; }; + F453AA892538EF16008F045B /* SinkForLifetimeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F453AA882538EF16008F045B /* SinkForLifetimeSpec.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 00B6C3B54CADD6729AF81F36 /* Pods-Common-FueledUtilsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Common-FueledUtilsTests.release.xcconfig"; path = "Target Support Files/Pods-Common-FueledUtilsTests/Pods-Common-FueledUtilsTests.release.xcconfig"; sourceTree = ""; }; + 1A3B52FAD952809D407DDA1E /* Pods_Common_asd_WatchKit_Extension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Common_asd_WatchKit_Extension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3297A6B2D4B923F8F27CD6A5 /* Pods-Common-FueledUtilsTests-SwiftUI.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Common-FueledUtilsTests-SwiftUI.release.xcconfig"; path = "Target Support Files/Pods-Common-FueledUtilsTests-SwiftUI/Pods-Common-FueledUtilsTests-SwiftUI.release.xcconfig"; sourceTree = ""; }; + 39EF9AE56A510B26CCC488B7 /* Pods_Common_FueledUtilsTests_SwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Common_FueledUtilsTests_SwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 48DE7B611288E2A6C99EEDAE /* Pods_Common_asd_WatchKit_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Common_asd_WatchKit_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4A32E8FB94B9CE2642794723 /* Pods-Common-asd WatchKit Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Common-asd WatchKit Extension.release.xcconfig"; path = "Target Support Files/Pods-Common-asd WatchKit Extension/Pods-Common-asd WatchKit Extension.release.xcconfig"; sourceTree = ""; }; + 607FACE51AFB9204008FA782 /* FueledUtilsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FueledUtilsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 65B56BF0A7147538E12F737F /* Pods-Common-FueledUtilsTests-SwiftUI.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Common-FueledUtilsTests-SwiftUI.debug.xcconfig"; path = "Target Support Files/Pods-Common-FueledUtilsTests-SwiftUI/Pods-Common-FueledUtilsTests-SwiftUI.debug.xcconfig"; sourceTree = ""; }; + AB25EAABC0CDD547AE307879 /* Pods-Common-asd WatchKit App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Common-asd WatchKit App.release.xcconfig"; path = "Target Support Files/Pods-Common-asd WatchKit App/Pods-Common-asd WatchKit App.release.xcconfig"; sourceTree = ""; }; + ACF665E90E66C2B35C6C5C05 /* Pods-Common-FueledUtilsTests-ReactiveSwift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Common-FueledUtilsTests-ReactiveSwift.release.xcconfig"; path = "Target Support Files/Pods-Common-FueledUtilsTests-ReactiveSwift/Pods-Common-FueledUtilsTests-ReactiveSwift.release.xcconfig"; sourceTree = ""; }; + C783847C92189B0968E12A16 /* Pods-Common-asd WatchKit Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Common-asd WatchKit Extension.debug.xcconfig"; path = "Target Support Files/Pods-Common-asd WatchKit Extension/Pods-Common-asd WatchKit Extension.debug.xcconfig"; sourceTree = ""; }; + CF40A2CC4151F8E9B373D243 /* FueledUtils.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = FueledUtils.podspec; path = ../FueledUtils.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + D43FD9799A872E88963939C1 /* Pods-Common-FueledUtilsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Common-FueledUtilsTests.debug.xcconfig"; path = "Target Support Files/Pods-Common-FueledUtilsTests/Pods-Common-FueledUtilsTests.debug.xcconfig"; sourceTree = ""; }; + D498AEE5BC8A0A514416E6DA /* Pods-Common-FueledUtilsTests-ReactiveSwift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Common-FueledUtilsTests-ReactiveSwift.debug.xcconfig"; path = "Target Support Files/Pods-Common-FueledUtilsTests-ReactiveSwift/Pods-Common-FueledUtilsTests-ReactiveSwift.debug.xcconfig"; sourceTree = ""; }; + E7AC5BA00D7F6054AC66E468 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; + E82CDABCC774F7F31D52705D /* Pods-Common-asd WatchKit App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Common-asd WatchKit App.debug.xcconfig"; path = "Target Support Files/Pods-Common-asd WatchKit App/Pods-Common-asd WatchKit App.debug.xcconfig"; sourceTree = ""; }; + F429742925378348004BFA85 /* ReactiveOverridingActionSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveOverridingActionSpec.swift; sourceTree = ""; }; + F429742D25378425004BFA85 /* OverridingActionSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverridingActionSpec.swift; sourceTree = ""; }; + F453AA7A2538DE55008F045B /* CombineLatestManySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineLatestManySpec.swift; sourceTree = ""; }; + F453AA882538EF16008F045B /* SinkForLifetimeSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SinkForLifetimeSpec.swift; sourceTree = ""; }; + F463C73A241835DD000A0B29 /* ReactiveCoalescingActionSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveCoalescingActionSpec.swift; sourceTree = ""; }; + F463C73B241835DD000A0B29 /* ReactiveSwiftExtensionsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveSwiftExtensionsSpec.swift; sourceTree = ""; }; + F463C73F241835EA000A0B29 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F46CDEA885028D8DB189B3CB /* Pods_Common_FueledUtilsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Common_FueledUtilsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F46D2DA5252E4E4A00B6987A /* OrderedSetSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedSetSpec.swift; sourceTree = ""; }; + F499631F2537459200E2D4B5 /* CoalescingActionSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoalescingActionSpec.swift; sourceTree = ""; }; + F7F10FE9C8384333882C2368 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 607FACE21AFB9204008FA782 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EE9B3A498587AC2D3461D310 /* Pods_Common_FueledUtilsTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 29F7E676D0DA9F23C4893ECA /* Pods */ = { + isa = PBXGroup; + children = ( + D498AEE5BC8A0A514416E6DA /* Pods-Common-FueledUtilsTests-ReactiveSwift.debug.xcconfig */, + ACF665E90E66C2B35C6C5C05 /* Pods-Common-FueledUtilsTests-ReactiveSwift.release.xcconfig */, + 65B56BF0A7147538E12F737F /* Pods-Common-FueledUtilsTests-SwiftUI.debug.xcconfig */, + 3297A6B2D4B923F8F27CD6A5 /* Pods-Common-FueledUtilsTests-SwiftUI.release.xcconfig */, + D43FD9799A872E88963939C1 /* Pods-Common-FueledUtilsTests.debug.xcconfig */, + 00B6C3B54CADD6729AF81F36 /* Pods-Common-FueledUtilsTests.release.xcconfig */, + C783847C92189B0968E12A16 /* Pods-Common-asd WatchKit Extension.debug.xcconfig */, + 4A32E8FB94B9CE2642794723 /* Pods-Common-asd WatchKit Extension.release.xcconfig */, + E82CDABCC774F7F31D52705D /* Pods-Common-asd WatchKit App.debug.xcconfig */, + AB25EAABC0CDD547AE307879 /* Pods-Common-asd WatchKit App.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 607FACC71AFB9204008FA782 = { + isa = PBXGroup; + children = ( + 607FACF51AFB993E008FA782 /* Podspec Metadata */, + F463C739241835DD000A0B29 /* Tests */, + F463C73E241835EA000A0B29 /* Supporting Files */, + 607FACD11AFB9204008FA782 /* Products */, + 29F7E676D0DA9F23C4893ECA /* Pods */, + 8CF3F11B25F743ED68711CBC /* Frameworks */, + ); + sourceTree = ""; + }; + 607FACD11AFB9204008FA782 /* Products */ = { + isa = PBXGroup; + children = ( + 607FACE51AFB9204008FA782 /* FueledUtilsTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { + isa = PBXGroup; + children = ( + CF40A2CC4151F8E9B373D243 /* FueledUtils.podspec */, + E7AC5BA00D7F6054AC66E468 /* README.md */, + F7F10FE9C8384333882C2368 /* LICENSE */, + ); + name = "Podspec Metadata"; + sourceTree = ""; + }; + 8CF3F11B25F743ED68711CBC /* Frameworks */ = { + isa = PBXGroup; + children = ( + 39EF9AE56A510B26CCC488B7 /* Pods_Common_FueledUtilsTests_SwiftUI.framework */, + F46CDEA885028D8DB189B3CB /* Pods_Common_FueledUtilsTests.framework */, + 1A3B52FAD952809D407DDA1E /* Pods_Common_asd_WatchKit_Extension.framework */, + 48DE7B611288E2A6C99EEDAE /* Pods_Common_asd_WatchKit_App.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + F463C739241835DD000A0B29 /* Tests */ = { + isa = PBXGroup; + children = ( + F499631F2537459200E2D4B5 /* CoalescingActionSpec.swift */, + F46D2DA5252E4E4A00B6987A /* OrderedSetSpec.swift */, + F429742D25378425004BFA85 /* OverridingActionSpec.swift */, + F463C73A241835DD000A0B29 /* ReactiveCoalescingActionSpec.swift */, + F463C73B241835DD000A0B29 /* ReactiveSwiftExtensionsSpec.swift */, + F429742925378348004BFA85 /* ReactiveOverridingActionSpec.swift */, + F453AA7A2538DE55008F045B /* CombineLatestManySpec.swift */, + F453AA882538EF16008F045B /* SinkForLifetimeSpec.swift */, + ); + path = Tests; + sourceTree = ""; + }; + F463C73E241835EA000A0B29 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + F463C73F241835EA000A0B29 /* Info.plist */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 607FACE41AFB9204008FA782 /* FueledUtilsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "FueledUtilsTests" */; + buildPhases = ( + 7A704080E54E9F7FB6D2806D /* [CP] Check Pods Manifest.lock */, + 607FACE11AFB9204008FA782 /* Sources */, + 607FACE21AFB9204008FA782 /* Frameworks */, + 607FACE31AFB9204008FA782 /* Resources */, + 41CB9636F71B84882C2E6A45 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FueledUtilsTests; + productName = Tests; + productReference = 607FACE51AFB9204008FA782 /* FueledUtilsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 607FACC81AFB9204008FA782 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1200; + LastUpgradeCheck = 1140; + ORGANIZATIONNAME = CocoaPods; + TargetAttributes = { + 607FACE41AFB9204008FA782 = { + CreatedOnToolsVersion = 6.3.1; + LastSwiftMigration = 0900; + }; + }; + }; + buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "FueledUtils" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 607FACC71AFB9204008FA782; + productRefGroup = 607FACD11AFB9204008FA782 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 607FACE41AFB9204008FA782 /* FueledUtilsTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 607FACE31AFB9204008FA782 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 41CB9636F71B84882C2E6A45 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Common-FueledUtilsTests/Pods-Common-FueledUtilsTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/FueledUtils/FueledUtils.framework", + "${BUILT_PRODUCTS_DIR}/Nimble/Nimble.framework", + "${BUILT_PRODUCTS_DIR}/Quick/Quick.framework", + "${BUILT_PRODUCTS_DIR}/ReactiveCocoa/ReactiveCocoa.framework", + "${BUILT_PRODUCTS_DIR}/ReactiveSwift/ReactiveSwift.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FueledUtils.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nimble.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Quick.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactiveCocoa.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactiveSwift.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Common-FueledUtilsTests/Pods-Common-FueledUtilsTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7A704080E54E9F7FB6D2806D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Common-FueledUtilsTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 607FACE11AFB9204008FA782 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F453AA7B2538DE55008F045B /* CombineLatestManySpec.swift in Sources */, + F453AA802538E05E008F045B /* OrderedSetSpec.swift in Sources */, + F453AA892538EF16008F045B /* SinkForLifetimeSpec.swift in Sources */, + F453AA822538E05E008F045B /* OverridingActionSpec.swift in Sources */, + F453AA832538E05E008F045B /* ReactiveCoalescingActionSpec.swift in Sources */, + F453AA812538E05E008F045B /* CoalescingActionSpec.swift in Sources */, + F453AA7F2538E05E008F045B /* ReactiveSwiftExtensionsSpec.swift in Sources */, + F453AA842538E05E008F045B /* ReactiveOverridingActionSpec.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 607FACED1AFB9204008FA782 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 607FACEE1AFB9204008FA782 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 607FACF31AFB9204008FA782 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D43FD9799A872E88963939C1 /* Pods-Common-FueledUtilsTests.debug.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "Supporting Files/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 607FACF41AFB9204008FA782 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 00B6C3B54CADD6729AF81F36 /* Pods-Common-FueledUtilsTests.release.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + "$(inherited)", + ); + INFOPLIST_FILE = "Supporting Files/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.fueled.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "FueledUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 607FACED1AFB9204008FA782 /* Debug */, + 607FACEE1AFB9204008FA782 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "FueledUtilsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 607FACF31AFB9204008FA782 /* Debug */, + 607FACF41AFB9204008FA782 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 607FACC81AFB9204008FA782 /* Project object */; +} diff --git a/Tests/FueledUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Tests/FueledUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..8fe1237a --- /dev/null +++ b/Tests/FueledUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtils.xcscheme b/Tests/FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtils/ReactiveSwift.xcscheme similarity index 59% rename from FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtils.xcscheme rename to Tests/FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtils/ReactiveSwift.xcscheme index bd9e3879..cc6ad7ff 100644 --- a/FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtils.xcscheme +++ b/Tests/FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtils/ReactiveSwift.xcscheme @@ -1,6 +1,6 @@ + + + + @@ -26,36 +40,24 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - codeCoverageEnabled = "YES" - onlyGenerateCoverageForSpecifiedTargets = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + skipped = "NO"> @@ -71,15 +73,16 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - + - + - + - + diff --git a/FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtils-watchOS.xcscheme b/Tests/FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtilsTests.xcscheme similarity index 58% rename from FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtils-watchOS.xcscheme rename to Tests/FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtilsTests.xcscheme index f6ee4297..f2a5dc09 100644 --- a/FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtils-watchOS.xcscheme +++ b/Tests/FueledUtils.xcodeproj/xcshareddata/xcschemes/FueledUtilsTests.xcscheme @@ -1,33 +1,38 @@ - - - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + - - - - - - - - diff --git a/Tests/FueledUtils.xcworkspace/contents.xcworkspacedata b/Tests/FueledUtils.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..514f9518 --- /dev/null +++ b/Tests/FueledUtils.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/FueledUtils.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Tests/FueledUtils.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from FueledUtils.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Tests/FueledUtils.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Tests/Podfile b/Tests/Podfile new file mode 100644 index 00000000..100fe2ae --- /dev/null +++ b/Tests/Podfile @@ -0,0 +1,17 @@ +use_frameworks! + +abstract_target 'Common' do + target 'FueledUtilsTests' do + platform :ios, '13.0' + + pod 'Quick', '~> 2.0' + pod 'Nimble', '~> 8.0' + + pod 'FueledUtils/ReactiveCombineBridge', path: '../' + pod 'FueledUtils/ReactiveSwift', path: '../' + pod 'FueledUtils/ReactiveSwiftUIKit', path: '../' + pod 'FueledUtils/SwiftUI', path: '../' + pod 'FueledUtils/CombineOperators', path: '../' + pod 'FueledUtils/CombineUIKit', path: '../' + end +end diff --git a/Tests/Podfile.lock b/Tests/Podfile.lock new file mode 100644 index 00000000..d67c157c --- /dev/null +++ b/Tests/Podfile.lock @@ -0,0 +1,63 @@ +PODS: + - FueledUtils/Combine (3.0-alpha1): + - FueledUtils/ReactiveCommon + - FueledUtils/CombineOperators (3.0-alpha1): + - FueledUtils/Combine + - FueledUtils/CombineUIKit (3.0-alpha1): + - FueledUtils/Combine + - FueledUtils/UIKit + - FueledUtils/Core (3.0-alpha1) + - FueledUtils/ReactiveCombineBridge (3.0-alpha1): + - FueledUtils/Combine + - FueledUtils/ReactiveSwift + - FueledUtils/ReactiveCommon (3.0-alpha1): + - FueledUtils/Core + - FueledUtils/ReactiveSwift (3.0-alpha1): + - FueledUtils/ReactiveCommon + - ReactiveCocoa (~> 10.0) + - ReactiveSwift (~> 6.0) + - FueledUtils/ReactiveSwiftUIKit (3.0-alpha1): + - FueledUtils/ReactiveSwift + - FueledUtils/UIKit + - FueledUtils/SwiftUI (3.0-alpha1): + - FueledUtils/Combine + - FueledUtils/Core + - FueledUtils/UIKit (3.0-alpha1): + - FueledUtils/Core + - Nimble (8.0.5) + - Quick (2.2.0) + - ReactiveCocoa (10.3.0): + - ReactiveSwift (~> 6.2) + - ReactiveSwift (6.4.0) + +DEPENDENCIES: + - FueledUtils/CombineOperators (from `../`) + - FueledUtils/CombineUIKit (from `../`) + - FueledUtils/ReactiveCombineBridge (from `../`) + - FueledUtils/ReactiveSwift (from `../`) + - FueledUtils/ReactiveSwiftUIKit (from `../`) + - FueledUtils/SwiftUI (from `../`) + - Nimble (~> 8.0) + - Quick (~> 2.0) + +SPEC REPOS: + trunk: + - Nimble + - Quick + - ReactiveCocoa + - ReactiveSwift + +EXTERNAL SOURCES: + FueledUtils: + :path: "../" + +SPEC CHECKSUMS: + FueledUtils: aa4b2a1e780f4f7e870ad53b64d0d7d16ab78604 + Nimble: 4ab1aeb9b45553c75b9687196b0fa0713170a332 + Quick: 7fb19e13be07b5dfb3b90d4f9824c855a11af40e + ReactiveCocoa: 083ae559e6f588ce519cab412ea119b431c26a24 + ReactiveSwift: 7555791a608c0679563a3f72546f971b2a06de98 + +PODFILE CHECKSUM: 57c718acafcbfdffd0d4c0cd376a0088c9cc4812 + +COCOAPODS: 1.10.0 diff --git a/FueledUtilsTests/Info.plist b/Tests/Supporting Files/Info.plist similarity index 100% rename from FueledUtilsTests/Info.plist rename to Tests/Supporting Files/Info.plist diff --git a/Tests/Tests/CoalescingActionSpec.swift b/Tests/Tests/CoalescingActionSpec.swift new file mode 100644 index 00000000..f3027130 --- /dev/null +++ b/Tests/Tests/CoalescingActionSpec.swift @@ -0,0 +1,61 @@ +// Copyright © 2020 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine +import FueledUtils +import Quick +import Nimble + +class CoalescingActionSpec: QuickSpec { + override func spec() { + describe("CoalescingAction") { + describe("apply.dispose()") { + it("should dispose of all created signal producers") { + var subscriptionCounter = 0 + var cancelledCounter = 0 + let coalescingAction = CoalescingAction { + Just(2.0) + .delay(for: 1.0, scheduler: DispatchQueue.main) + .handleEvents( + receiveSubscription: { _ in + subscriptionCounter += 1 + }, + receiveCancel: { + cancelledCounter += 1 + } + ) + } + + expect(subscriptionCounter) == 0 + + let publishersCount = 5 + let cancellables = (0..]>([]).sink( + receiveCompletion: { completion in + let isFinished: Bool + switch completion { + case .failure: + isFinished = false + case .finished: + isFinished = true + } + expect(isFinished) == true + completionCount += 1 + }, + receiveValue: { values in + expect(values).to(haveCount(0)) + valueCount += 1 + } + ) + .store(in: &self.cancellables) + + expect(completionCount) == 1 + expect(valueCount) == 1 + } + } + describe("with two elements") { + it("should match the native CombineLatest behavior") { + var completionCount = 0 + var valueCount = 0 + var nativeValues: [Int] = [] + var manyValues: [Int] = [] + + func publisher(_ value: Int) -> AnyPublisher { + Just(value).delay(for: 0.3, scheduler: DispatchQueue.main) + .eraseToAnyPublisher() + } + + Publishers.CombineLatest( + publisher(1).append(publisher(3)), + publisher(2) + ) + .sink( + receiveCompletion: { completion in + let isFinished: Bool + switch completion { + case .failure: + isFinished = false + case .finished: + isFinished = true + } + expect(isFinished) == true + completionCount += 1 + }, + receiveValue: { value1, value2 in + nativeValues = [value1, value2] + valueCount += 1 + } + ) + .store(in: &self.cancellables) + + Publishers.CombineLatestMany( + [ + publisher(1).append(publisher(3)).eraseToAnyPublisher(), + publisher(2).eraseToAnyPublisher() + ] + ) + .sink( + receiveCompletion: { completion in + let isFinished: Bool + switch completion { + case .failure: + isFinished = false + case .finished: + isFinished = true + } + expect(isFinished) == true + completionCount += 1 + }, + receiveValue: { values in + manyValues = values + valueCount += 1 + } + ) + .store(in: &self.cancellables) + + expect(completionCount).toEventually(equal(2)) + expect(valueCount).toEventually(equal(4)) + + expect(nativeValues).toEventually(equal([3, 2])) + expect(manyValues).toEventually(equal([3, 2])) + } + } + describe("with two publishers") { + it("should correctly interrupt the publishers when interrupted") { + var subscriptionCount = 0 + var cancelCount = 0 + var nativeValueCount = 0 + var nativeValues: [Int] = [] + var manyValueCount = 0 + var manyValues: [Int] = [] + + func publisher(_ value: Int) -> AnyPublisher { + Just(1).delay(for: 0.5, scheduler: DispatchQueue.main) + .handleEvents( + receiveSubscription: { _ in + subscriptionCount += 1 + }, + receiveCancel: { + cancelCount += 1 + } + ) + .eraseToAnyPublisher() + } + + var cancellable = Publishers.CombineLatest( + publisher(1), + publisher(2) + ) + .handleEvents( + receiveCancel: { + cancelCount += 1 + } + ) + .sink( + receiveValue: { value1, value2 in + nativeValues = [value1, value2] + nativeValueCount += 1 + } + ) + + cancellable = Publishers.CombineLatestMany( + [ + publisher(1), + publisher(2) + ] + ) + .handleEvents( + receiveCancel: { + cancelCount += 1 + } + ) + .sink( + receiveValue: { values in + manyValues = values + manyValueCount += 1 + } + ) + + cancellable.cancel() + + expect(subscriptionCount) == 4 + expect(cancelCount).toEventually(equal(6)) + + expect(nativeValueCount).toEventually(equal(0)) + expect(nativeValues).toEventually(haveCount(0)) + + expect(manyValueCount).toEventually(equal(0)) + expect(manyValues).toEventually(haveCount(0)) + } + } + } + } +} + +#endif diff --git a/Tests/Tests/OrderedSetSpec.swift b/Tests/Tests/OrderedSetSpec.swift new file mode 100644 index 00000000..e02f7032 --- /dev/null +++ b/Tests/Tests/OrderedSetSpec.swift @@ -0,0 +1,67 @@ +// Copyright © 2020 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Quick +import Nimble +import FueledUtils +import XCTest + +class OrderedSetSpec: QuickSpec { + override func spec() { + describe("OrderedSet") { + describe("Initialization") { + it("should initialize with no corruption") { + expect(OrderedSet([1])) == [1] + } + it("should initialize with no corruption") { + expect(OrderedSet([1, 1, 2, 2, 4, 3, 1, 2, 3])) == [1, 2, 4, 3] + } + } + describe("Array operations") { + it("should add properly") { + var set = OrderedSet([1, 2, 3]) + set += [2, 2, 3, 3, 4, 4] + expect(set) == [1, 2, 3, 4] + } + it("should remove properly") { + var set = OrderedSet([1, 2, 3]) + set.remove(2) + expect(set) == [1, 3] + } + it("should replace in a subrange properly") { + var set = OrderedSet([1, 2, 3, 4, 5, 6, 7]) + set.replaceSubrange(1..<4, with: [3, 5, 9, 8, 1, 0]) + expect(set) == [1, 3, 9, 8, 1, 0, 5, 6, 7] + } + } + describe("Set algebra") { + it("should substract properly") { + var set = OrderedSet([1, 2, 3]) + set.subtract([2, 3, 4]) + expect(set) == [1] + } + it("should form an union properly") { + var set = OrderedSet([1, 2, 3]) + set.formUnion([3, 2, 4, 1]) + expect(set) == [1, 2, 3, 4] + } + it("should perform a symmetric difference properly") { + var set: Set = [1, 2, 3, 4] + set.formSymmetricDifference([3, 4, 5]) + expect(set) == [1, 2, 5] + } + } + } + } +} diff --git a/Tests/Tests/OverridingActionSpec.swift b/Tests/Tests/OverridingActionSpec.swift new file mode 100644 index 00000000..35c77e6c --- /dev/null +++ b/Tests/Tests/OverridingActionSpec.swift @@ -0,0 +1,67 @@ +// Copyright © 2020 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Combine) +import Combine +import FueledUtils +import Quick +import Nimble + +class OverridingActionSpec: QuickSpec { + private var cancellables: [AnyCancellable]! + + override func spec() { + describe("OverridingAction") { + beforeEach { + self.cancellables = [] + } + describe("apply.sink()") { + it("should cancel the previous work") { + var subscriptionCounter = 0 + var cancelledCounter = 0 + var interruptedCounter = 0 + let coalescingAction = OverridingAction { + Just(2.0) + .delay(for: 1.0, scheduler: DispatchQueue.main) + .handleEvents( + receiveSubscription: { _ in + subscriptionCounter += 1 + }, + receiveCancel: { + cancelledCounter += 1 + }, + receiveTermination: { + interruptedCounter += 1 + } + ) + } + + expect(subscriptionCounter) == 0 + + let publishersCount = 5 + self.cancellables = (0..