├── Demo ├── ReactiveTimelaneDemo │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── ReactiveTimelaneDemo.entitlements │ ├── App │ │ ├── App.swift │ │ ├── SceneDelegate.swift │ │ └── AppDelegate.swift │ ├── Info.plist │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── ViewController.swift └── ReactiveTimelaneDemo.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── project.pbxproj ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── PULL_REQUEST_TEMPLATE.md ├── Tests ├── LinuxMain.swift └── ReactiveTimelaneTests │ ├── XCTestManifests.swift │ ├── LifetimeTests.swift │ ├── SignalProducerTests.swift │ └── SignalTests.swift ├── .github ├── workflows │ └── ci.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── Package.resolved ├── Package.swift ├── LICENSE ├── Sources └── ReactiveTimelane │ ├── Lifetime+Lane.swift │ ├── SignalProducer+Lane.swift │ └── Signal+Lane.swift ├── .gitignore └── README.md /Demo/ReactiveTimelaneDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | 5 | ## Related issues 6 | 7 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ReactiveTimelaneTests 3 | 4 | var tests = [XCTestCaseEntry]() 5 | tests += SignalTests.allTests() 6 | tests += SignalProducerTests.allTests() 7 | tests += LifetimeTests.allTests() 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo/ReactiveTimelaneDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: macOS-latest 6 | steps: 7 | 8 | - name: Checkout 9 | uses: actions/checkout@v1 10 | 11 | - name: Build 12 | run: swift build -v 13 | 14 | - name: Test 15 | run: swift test -v 16 | -------------------------------------------------------------------------------- /Tests/ReactiveTimelaneTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SignalTests.allTests), 7 | testCase(SignalProducerTests.allTests), 8 | testCase(LifetimeTests.allTests), 9 | ] 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo/App/App.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | struct App { 4 | static func show(in window: UIWindow) { 5 | let viewController = ViewController() 6 | let navigationController = UINavigationController(rootViewController: viewController) 7 | navigationController.navigationBar.prefersLargeTitles = true 8 | viewController.title = "ReactiveTimelane" 9 | navigationController.title = "ReactiveTimelane" 10 | window.rootViewController = navigationController 11 | window.makeKeyAndVisible() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @available(iOS 13.0, *) 4 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, 9 | options connectionOptions: UIScene.ConnectionOptions) { 10 | guard let windowScene = (scene as? UIWindowScene) else { return } 11 | let window = UIWindow(windowScene: windowScene) 12 | self.window = window 13 | 14 | App.show(in: window) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? 11 | 12 | 13 | ## Describe the solution you'd like 14 | 15 | 16 | ## Describe alternatives you've considered 17 | 18 | 19 | ## Additional context 20 | 21 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ReactiveSwift", 6 | "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "3f4351d04115fd8797802d9b2d17b812cd761602", 10 | "version": "6.3.0" 11 | } 12 | }, 13 | { 14 | "package": "TimelaneCore", 15 | "repositoryURL": "https://github.com/icanzilb/TimelaneCore", 16 | "state": { 17 | "branch": null, 18 | "revision": "343792b6960f0bec2c7a8a84a4027506eddaf440", 19 | "version": "1.0.10" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected behavior 21 | 22 | 23 | ## Screenshots 24 | 25 | 26 | ## Platform 27 | 28 | - Device: 29 | - OS: 30 | - Version of this library: 31 | 32 | ## Additional context 33 | 34 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ReactiveTimelane", 6 | platforms: [ 7 | .macOS(.v10_10), .iOS(.v9), .tvOS(.v9), .watchOS(.v2) 8 | ], 9 | products: [ 10 | .library(name: "ReactiveTimelane", targets: ["ReactiveTimelane"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/icanzilb/TimelaneCore", from: "1.0.9"), 14 | .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "6.3.0") 15 | ], 16 | targets: [ 17 | .target(name: "ReactiveTimelane", 18 | dependencies: ["ReactiveSwift", "TimelaneCore"]), 19 | .testTarget(name: "ReactiveTimelaneTests", 20 | dependencies: [ 21 | "ReactiveTimelane", 22 | "ReactiveSwift", 23 | .product(name: "TimelaneCoreTestUtils", package: "TimelaneCore")]), 24 | ], 25 | swiftLanguageVersions: [.v5] 26 | ) 27 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | func application(_ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 10 | guard #available(iOS 13.0, *) else { 11 | let window = UIWindow(frame: UIScreen.main.bounds) 12 | self.window = window 13 | App.show(in: window) 14 | return true 15 | } 16 | return true 17 | } 18 | 19 | @available(iOS 13.0, *) 20 | func application(_ application: UIApplication, 21 | configurationForConnecting connectingSceneSession: UISceneSession, 22 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) 24 | configuration.delegateClass = SceneDelegate.self 25 | return configuration 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Niclas Kristek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Tests/ReactiveTimelaneTests/LifetimeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TimelaneCore 3 | import TimelaneCoreTestUtils 4 | import ReactiveSwift 5 | @testable import ReactiveTimelane 6 | 7 | @available(macOS 10.14, iOS 12, tvOS 12, watchOS 5, *) 8 | final class LifetimeTests: XCTestCase { 9 | override func setUp() { 10 | continueAfterFailure = false 11 | super.setUp() 12 | } 13 | 14 | // MARK: - Subscription 15 | 16 | func testSubscription() { 17 | let recorder = TestLog() 18 | Timelane.Subscription.didEmitVersion = true 19 | 20 | let (lifetime, token) = Lifetime.make() 21 | lifetime 22 | .lane("Test Subscription", logger: recorder.log) 23 | 24 | XCTAssertEqual(recorder.logged.count, 1) 25 | XCTAssertEqual(recorder.logged[0].signpostType, "begin") 26 | XCTAssertEqual(recorder.logged[0].subscribe, "Test Subscription") 27 | 28 | token.dispose() 29 | 30 | XCTAssertEqual(recorder.logged[1].signpostType, "end") 31 | } 32 | 33 | // MARK: - All tests 34 | 35 | static var allTests = [ 36 | ("testSubscription", testSubscription), 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ReactiveTimelane/Lifetime+Lane.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | import TimelaneCore 4 | 5 | @available(macOS 10.14, iOS 12, tvOS 12, watchOS 5, *) 6 | public extension Lifetime { 7 | /** 8 | This operator logs the lifetime to the Timelane Instrument. 9 | 10 | - Note: You can download the Timelane Instrument from [timelane.tools](http://timelane.tools). 11 | 12 | - Parameter name: A name for the lane when visualized in Instruments. 13 | 14 | - Parameter file: The name of the current file. 15 | 16 | - Parameter function: The name of the current function. 17 | 18 | - Parameter line: The number of the current line. 19 | 20 | - Parameter logger: A logger which should be used to log the lifetime. 21 | */ 22 | func lane(_ name: String, 23 | file: StaticString = #file, 24 | function: StaticString = #function, 25 | line: UInt = #line, 26 | logger: @escaping Timelane.Logger = Timelane.defaultLogger) { 27 | let fileName = file.description.components(separatedBy: "/").last! 28 | let source = "\(fileName):\(line) - \(function)" 29 | let subscription = Timelane.Subscription(name: name, logger: logger) 30 | subscription.begin(source: source) 31 | self += observeEnded { 32 | subscription.end(state: .completed) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ReactiveTimelane/SignalProducer+Lane.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | import TimelaneCore 4 | 5 | @available(macOS 10.14, iOS 12, tvOS 12, watchOS 5, *) 6 | public extension SignalProducer { 7 | /** 8 | This operator logs the lifetime of the subscription to the `SignalProducer` and its events to the Timelane Instrument. 9 | 10 | - Note: You can download the Timelane Instrument from [timelane.tools](http://timelane.tools). 11 | 12 | - Parameter name: A name for the lane when visualized in Instruments. 13 | 14 | - Parameter filter: Determines which metrics should be logged. 15 | 16 | - Parameter file: The name of the current file. 17 | 18 | - Parameter function: The name of the current function. 19 | 20 | - Parameter line: The number of the current line. 21 | 22 | - Parameter transformValue: An optional closure to format the subscription values for displaying in Instruments. 23 | 24 | - Parameter logger: A logger which should be used to log the specified metrics. 25 | 26 | - Returns: A `SignalProducer` where the specified metrics are logged for the Timelane Instrument. 27 | */ 28 | func lane(_ name: String, 29 | filter: Timelane.LaneTypeOptions = .all, 30 | file: StaticString = #file, 31 | function: StaticString = #function, 32 | line: UInt = #line, 33 | transformValue transform: @escaping (Value) -> String = String.init(describing:), 34 | logger: @escaping Timelane.Logger = Timelane.defaultLogger) -> SignalProducer { 35 | lift { $0.lane(name, filter: filter, transformValue: transform, logger: logger) } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIRequiredDeviceCapabilities 31 | 32 | armv7 33 | 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Nimble", 6 | "repositoryURL": "https://github.com/Quick/Nimble.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "72f5a90d573f7f7d70aa6b8ad84b3e1e02eabb4d", 10 | "version": "8.0.9" 11 | } 12 | }, 13 | { 14 | "package": "Quick", 15 | "repositoryURL": "https://github.com/Quick/Quick.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "33682c2f6230c60614861dfc61df267e11a1602f", 19 | "version": "2.2.0" 20 | } 21 | }, 22 | { 23 | "package": "ReactiveCocoa", 24 | "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveCocoa", 25 | "state": { 26 | "branch": null, 27 | "revision": "8be399d8df3ca96e54f94c2a5091ce07c2cfe93c", 28 | "version": "10.3.0" 29 | } 30 | }, 31 | { 32 | "package": "ReactiveSwift", 33 | "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift", 34 | "state": { 35 | "branch": null, 36 | "revision": "3f4351d04115fd8797802d9b2d17b812cd761602", 37 | "version": "6.3.0" 38 | } 39 | }, 40 | { 41 | "package": "ReactiveTimelane", 42 | "repositoryURL": "https://github.com/nkristek/ReactiveTimelane", 43 | "state": { 44 | "branch": null, 45 | "revision": "3882e77ca7d0a6d0947b25cba02ae6658a5ec7b1", 46 | "version": "1.0.1" 47 | } 48 | }, 49 | { 50 | "package": "TimelaneCore", 51 | "repositoryURL": "https://github.com/icanzilb/TimelaneCore", 52 | "state": { 53 | "branch": null, 54 | "revision": "343792b6960f0bec2c7a8a84a4027506eddaf440", 55 | "version": "1.0.10" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactiveTimelane 2 | [![CI Status](https://github.com/nkristek/ReactiveTimelane/workflows/CI/badge.svg)](https://github.com/nkristek/ReactiveTimelane/actions) 3 | 4 | **ReactiveTimelane** provides operators for `Signal`, `SignalProducer` and `Lifetime` in [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift) for profiling streams and lifetimes with the Timelane Instrument. 5 | 6 | #### Contents: 7 | 8 | - [Usage](#usage) 9 | - [API Reference](#api-reference) 10 | - [Installation](#installation) 11 | - [Contribution](#contribution) 12 | 13 | ## Usage 14 | 15 | > Before making use of ReactiveTimelane, you need to install the Timelane Instrument from http://timelane.tools 16 | 17 | Import the `ReactiveTimelane` framework in your code: 18 | 19 | ```swift 20 | import ReactiveTimelane 21 | ``` 22 | 23 | Use the `lane(_:)` operator to profile a subscription via the TimelaneInstrument. Insert `lane(_:)` at the precise spot in your code you'd like to profile like so: 24 | 25 | ```swift 26 | let producer: SignalProducer = SignalProducer(value: ()) 27 | producer 28 | .lane("Void producer") 29 | .start() 30 | ``` 31 | 32 | Then profile your project by clicking **Product > Profile** in Xcode's main menu. 33 | 34 | For a more detailed walkthrough go to [http://timelane.tools](http://timelane.tools). 35 | 36 | ## API Reference 37 | 38 | ### `lane(_:filter:)` 39 | 40 | Use `lane("Lane name")` to send data to both the subscriptions and events lanes in the Timelane Instrument. 41 | 42 | `lane("Lane name", filter: .subscription)` sends begin/completion events to the Subscriptions lane. Use this syntax if you only want to observe the lifetime of the `Signal` / `SignalProducer`. 43 | 44 | `lane("Lane name", filter: .event)` sends events and values to the Events lane. Use this filter if you are only interested in the values the `Signal` / `SignalProducer` emits. 45 | 46 | Additionally you can transform the values logged in Timelane by using the optional `transformValue` trailing closure: 47 | 48 | ```swift 49 | lane("Lane name", transformValue: { "Value is \($0)" }) 50 | ``` 51 | 52 | ## Installation 53 | 54 | ### Swift Package Manager 55 | 56 | #### Automatically in Xcode: 57 | 58 | - Click **File > Swift Packages > Add Package Dependency...** 59 | - Use the package URL `https://github.com/nkristek/ReactiveTimelane` to add ReactiveTimelane to your project. 60 | 61 | #### Manually in your `Package.swift` file: 62 | 63 | ```swift 64 | .package(url: "https://github.com/nkristek/ReactiveTimelane", from: "1.1.0") 65 | ``` 66 | 67 | ## Contribution 68 | 69 | If you find a bug feel free to open an issue. Contributions are also appreciated. 70 | -------------------------------------------------------------------------------- /Sources/ReactiveTimelane/Signal+Lane.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactiveSwift 3 | import TimelaneCore 4 | 5 | @available(macOS 10.14, iOS 12, tvOS 12, watchOS 5, *) 6 | public extension Signal { 7 | /** 8 | This operator logs the lifetime of the subscription to the `Signal` and its events to the Timelane Instrument. 9 | 10 | - Note: You can download the Timelane Instrument from [timelane.tools](http://timelane.tools). 11 | 12 | - Parameter name: A name for the lane when visualized in Instruments. 13 | 14 | - Parameter filter: Determines which metrics should be logged. 15 | 16 | - Parameter file: The name of the current file. 17 | 18 | - Parameter function: The name of the current function. 19 | 20 | - Parameter line: The number of the current line. 21 | 22 | - Parameter transformValue: An optional closure to format the subscription values for displaying in Instruments. 23 | 24 | - Parameter logger: A logger which should be used to log the specified metrics. 25 | 26 | - Returns: A `Signal` where the specified metrics are logged for the Timelane Instrument. 27 | */ 28 | func lane(_ name: String, 29 | filter: Timelane.LaneTypeOptions = .all, 30 | file: StaticString = #file, 31 | function: StaticString = #function, 32 | line: UInt = #line, 33 | transformValue transform: @escaping (Value) -> String = String.init(describing:), 34 | logger: @escaping Timelane.Logger = Timelane.defaultLogger) -> Signal { 35 | let fileName = file.description.components(separatedBy: "/").last! 36 | let source = "\(fileName):\(line) - \(function)" 37 | let subscription = Timelane.Subscription(name: name, logger: logger) 38 | 39 | if filter.contains(.subscription) { 40 | subscription.begin(source: source) 41 | } 42 | 43 | return on( 44 | failed: { error in 45 | if filter.contains(.subscription) { 46 | subscription.end(state: .error(error.localizedDescription)) 47 | } 48 | 49 | if filter.contains(.event) { 50 | subscription.event(value: .error(error.localizedDescription), source: source) 51 | } 52 | }, completed: { 53 | if filter.contains(.subscription) { 54 | subscription.end(state: .completed) 55 | } 56 | 57 | if filter.contains(.event) { 58 | subscription.event(value: .completion, source: source) 59 | } 60 | }, interrupted: { 61 | if filter.contains(.subscription) { 62 | subscription.end(state: .cancelled) 63 | } 64 | 65 | if filter.contains(.event) { 66 | subscription.event(value: .cancelled, source: source) 67 | } 68 | }, value: { value in 69 | if filter.contains(.event) { 70 | subscription.event(value: .value(transform(value)), source: source) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import ReactiveSwift 3 | import ReactiveCocoa 4 | import ReactiveTimelane 5 | 6 | final class ViewController: UIViewController { 7 | 8 | // MARK: - Properties 9 | 10 | private let timedValueProducer: SignalProducer<(), Never> = { 11 | SignalProducer(value: ()) 12 | .delay(5, on: QueueScheduler.main) 13 | .lane("Timed value producer") 14 | }() 15 | 16 | fileprivate enum Errors: Error { 17 | case somethingWentWrong 18 | } 19 | 20 | private let timedErrorProducer: SignalProducer<(), Errors> = { 21 | SignalProducer(value: ()) 22 | .delay(5, on: QueueScheduler.main) 23 | .flatMap(.latest) { SignalProducer(error: .somethingWentWrong).lane("Inner error producer") } 24 | .lane("Timed error producer") 25 | }() 26 | 27 | private var (subscriptionLifetime, subscriptionToken) = Lifetime.make() 28 | 29 | // MARK: - Lifecycle 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | setUpView() 34 | setUpBindings() 35 | } 36 | 37 | private func setUpBindings() { 38 | reactive.lifetime += startValueProducerButton.reactive 39 | .mapControlEvents(.touchUpInside, { _ in () }) 40 | .observeValues { [weak self] in 41 | guard let strongSelf = self else { return } 42 | strongSelf.subscriptionLifetime += strongSelf.timedValueProducer.start() 43 | } 44 | 45 | reactive.lifetime += startErrorProducerButton.reactive 46 | .mapControlEvents(.touchUpInside, { _ in () }) 47 | .observeValues { [weak self] in 48 | guard let strongSelf = self else { return } 49 | strongSelf.subscriptionLifetime += strongSelf.timedErrorProducer.start() 50 | } 51 | 52 | reactive.lifetime += stopButton.reactive 53 | .mapControlEvents(.touchUpInside, { _ in () }) 54 | .observeValues { [weak self] in 55 | guard let strongSelf = self else { return } 56 | (strongSelf.subscriptionLifetime, strongSelf.subscriptionToken) = Lifetime.make() 57 | } 58 | } 59 | 60 | // MARK: - View 61 | 62 | private lazy var buttonStack: UIStackView = { 63 | let stack = UIStackView(arrangedSubviews: [ 64 | startValueProducerButton, 65 | startErrorProducerButton, 66 | stopButton 67 | ]) 68 | stack.axis = .vertical 69 | stack.spacing = 24 70 | return stack 71 | }() 72 | 73 | private let startValueProducerButton: UIButton = { 74 | let button = UIButton(type: .system) 75 | button.setTitle("Start value producer", for: .normal) 76 | return button 77 | }() 78 | 79 | private let startErrorProducerButton: UIButton = { 80 | let button = UIButton(type: .system) 81 | button.setTitle("Start error producer", for: .normal) 82 | return button 83 | }() 84 | 85 | private let stopButton: UIButton = { 86 | let button = UIButton(type: .system) 87 | button.setTitle("Stop all", for: .normal) 88 | return button 89 | }() 90 | 91 | private func setUpView() { 92 | if #available(iOS 13.0, *) { 93 | view.backgroundColor = .systemBackground 94 | } else { 95 | view.backgroundColor = .white 96 | } 97 | view.addSubview(buttonStack) 98 | buttonStack.translatesAutoresizingMaskIntoConstraints = false 99 | NSLayoutConstraint.activate([ 100 | buttonStack.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), 101 | buttonStack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor), 102 | ]) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tests/ReactiveTimelaneTests/SignalProducerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TimelaneCore 3 | import TimelaneCoreTestUtils 4 | import ReactiveSwift 5 | @testable import ReactiveTimelane 6 | 7 | @available(macOS 10.14, iOS 12, tvOS 12, watchOS 5, *) 8 | final class SignalProducerTests: XCTestCase { 9 | override func setUp() { 10 | continueAfterFailure = false 11 | super.setUp() 12 | } 13 | 14 | // MARK: - Events 15 | 16 | func testCompletedEvent() { 17 | let recorder = TestLog() 18 | Timelane.Subscription.didEmitVersion = true 19 | 20 | SignalProducer { observer, _ in observer.sendCompleted() } 21 | .lane("Test Subscription", filter: .event, logger: recorder.log) 22 | .start() 23 | 24 | XCTAssertEqual(recorder.logged.count, 1) 25 | XCTAssertEqual(recorder.logged[0].type, "Completed") 26 | XCTAssertEqual(recorder.logged[0].subscription, "Test Subscription") 27 | } 28 | 29 | func testValueEvents() { 30 | let recorder = TestLog() 31 | Timelane.Subscription.didEmitVersion = true 32 | 33 | SignalProducer(values: 1, 2, 3) 34 | .lane("Test Subscription", filter: .event, logger: recorder.log) 35 | .start() 36 | 37 | XCTAssertEqual(recorder.logged.count, 4) 38 | XCTAssertEqual(recorder.logged[0].outputTldr, "Output, Test Subscription, 1") 39 | XCTAssertEqual(recorder.logged[1].outputTldr, "Output, Test Subscription, 2") 40 | XCTAssertEqual(recorder.logged[2].outputTldr, "Output, Test Subscription, 3") 41 | XCTAssertEqual(recorder.logged[3].type, "Completed") 42 | XCTAssertEqual(recorder.logged[3].subscription, "Test Subscription") 43 | } 44 | 45 | func testFormatting() { 46 | let recorder = TestLog() 47 | Timelane.Subscription.didEmitVersion = true 48 | 49 | SignalProducer(value: 1) 50 | .lane("Test Subscription", 51 | filter: .event, 52 | transformValue: { "TEST \($0)" }, 53 | logger: recorder.log) 54 | .start() 55 | XCTAssertEqual(recorder.logged.count, 2) 56 | XCTAssertEqual(recorder.logged[0].outputTldr, "Output, Test Subscription, TEST 1") 57 | } 58 | 59 | enum TestError: LocalizedError { 60 | case somethingWentWrong 61 | var errorDescription: String? { "Error description" } 62 | } 63 | 64 | func testErrorEvent() { 65 | let recorder = TestLog() 66 | Timelane.Subscription.didEmitVersion = true 67 | 68 | SignalProducer(error: .somethingWentWrong) 69 | .lane("Test Subscription", filter: .event, logger: recorder.log) 70 | .start() 71 | 72 | XCTAssertEqual(recorder.logged.count, 1) 73 | XCTAssertEqual(recorder.logged[0].type, "Error") 74 | XCTAssertEqual(recorder.logged[0].value, TestError.somethingWentWrong.errorDescription) 75 | } 76 | 77 | func testErrorEventAfterValues() { 78 | let recorder = TestLog() 79 | Timelane.Subscription.didEmitVersion = true 80 | 81 | SignalProducer(values: 1, 2, 3) 82 | .concat(error: .somethingWentWrong) 83 | .lane("Test Subscription", filter: .event, logger: recorder.log) 84 | .start() 85 | 86 | XCTAssertEqual(recorder.logged.count, 4) 87 | XCTAssertEqual(recorder.logged[0].outputTldr, "Output, Test Subscription, 1") 88 | XCTAssertEqual(recorder.logged[1].outputTldr, "Output, Test Subscription, 2") 89 | XCTAssertEqual(recorder.logged[2].outputTldr, "Output, Test Subscription, 3") 90 | XCTAssertEqual(recorder.logged[3].type, "Error") 91 | XCTAssertEqual(recorder.logged[3].value, TestError.somethingWentWrong.errorDescription) 92 | } 93 | 94 | func testCancelledEvent() { 95 | let recorder = TestLog() 96 | Timelane.Subscription.didEmitVersion = true 97 | 98 | let property = MutableProperty(0) 99 | let disposable = property.producer 100 | .lane("Test Subscription", filter: .event, logger: recorder.log) 101 | .start() 102 | 103 | XCTAssertEqual(recorder.logged.count, 1) 104 | XCTAssertEqual(recorder.logged[0].outputTldr, "Output, Test Subscription, 0") 105 | 106 | disposable.dispose() 107 | 108 | XCTAssertEqual(recorder.logged.count, 2) 109 | XCTAssertEqual(recorder.logged[1].type, "Cancelled") 110 | } 111 | 112 | // MARK: - Subscription 113 | 114 | func testSubscription() { 115 | let recorder = TestLog() 116 | Timelane.Subscription.didEmitVersion = true 117 | 118 | let property = MutableProperty(0) 119 | let disposable = property.producer 120 | .lane("Test Subscription", filter: .subscription, logger: recorder.log) 121 | .start() 122 | 123 | XCTAssertEqual(recorder.logged.count, 1) 124 | XCTAssertEqual(recorder.logged[0].signpostType, "begin") 125 | XCTAssertEqual(recorder.logged[0].subscribe, "Test Subscription") 126 | 127 | disposable.dispose() 128 | 129 | XCTAssertEqual(recorder.logged[1].signpostType, "end") 130 | } 131 | 132 | // MARK: - All tests 133 | 134 | static var allTests = [ 135 | ("testCompletedEvent", testCompletedEvent), 136 | ("testValueEvents", testValueEvents), 137 | ("testFormatting", testFormatting), 138 | ("testErrorEvent", testErrorEvent), 139 | ("testErrorEventAfterValues", testErrorEventAfterValues), 140 | ("testCancelledEvent", testCancelledEvent), 141 | ("testSubscription", testSubscription), 142 | ] 143 | } 144 | -------------------------------------------------------------------------------- /Tests/ReactiveTimelaneTests/SignalTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TimelaneCore 3 | import TimelaneCoreTestUtils 4 | import ReactiveSwift 5 | @testable import ReactiveTimelane 6 | 7 | @available(macOS 10.14, iOS 12, tvOS 12, watchOS 5, *) 8 | final class SignalTests: XCTestCase { 9 | override func setUp() { 10 | continueAfterFailure = false 11 | super.setUp() 12 | } 13 | 14 | // MARK: - Events 15 | 16 | func testCompletedEvent() { 17 | let recorder = TestLog() 18 | Timelane.Subscription.didEmitVersion = true 19 | 20 | let (signal, observer) = Signal.pipe() 21 | signal 22 | .lane("Test Subscription", filter: .event, logger: recorder.log) 23 | .observe { _ in } 24 | observer.sendCompleted() 25 | 26 | XCTAssertEqual(recorder.logged.count, 1) 27 | XCTAssertEqual(recorder.logged[0].type, "Completed") 28 | XCTAssertEqual(recorder.logged[0].subscription, "Test Subscription") 29 | } 30 | 31 | func testValueEvents() { 32 | let recorder = TestLog() 33 | Timelane.Subscription.didEmitVersion = true 34 | 35 | let (signal, observer) = Signal.pipe() 36 | signal 37 | .lane("Test Subscription", filter: .event, logger: recorder.log) 38 | .observe { _ in } 39 | observer.send(value: 1) 40 | observer.send(value: 2) 41 | observer.send(value: 3) 42 | observer.sendCompleted() 43 | 44 | XCTAssertEqual(recorder.logged.count, 4) 45 | XCTAssertEqual(recorder.logged[0].outputTldr, "Output, Test Subscription, 1") 46 | XCTAssertEqual(recorder.logged[1].outputTldr, "Output, Test Subscription, 2") 47 | XCTAssertEqual(recorder.logged[2].outputTldr, "Output, Test Subscription, 3") 48 | XCTAssertEqual(recorder.logged[3].type, "Completed") 49 | XCTAssertEqual(recorder.logged[3].subscription, "Test Subscription") 50 | } 51 | 52 | func testFormatting() { 53 | let recorder = TestLog() 54 | Timelane.Subscription.didEmitVersion = true 55 | 56 | let (signal, observer) = Signal.pipe() 57 | signal 58 | .lane("Test Subscription", 59 | filter: .event, 60 | transformValue: { "TEST \($0)" }, 61 | logger: recorder.log) 62 | .observe { _ in } 63 | observer.send(value: 1) 64 | observer.sendCompleted() 65 | XCTAssertEqual(recorder.logged.count, 2) 66 | XCTAssertEqual(recorder.logged[0].outputTldr, "Output, Test Subscription, TEST 1") 67 | } 68 | 69 | enum TestError: LocalizedError { 70 | case somethingWentWrong 71 | var errorDescription: String? { "Error description" } 72 | } 73 | 74 | func testErrorEvent() { 75 | let recorder = TestLog() 76 | Timelane.Subscription.didEmitVersion = true 77 | 78 | let (signal, observer) = Signal.pipe() 79 | signal 80 | .lane("Test Subscription", filter: .event, logger: recorder.log) 81 | .observe { _ in } 82 | observer.send(error: .somethingWentWrong) 83 | XCTAssertEqual(recorder.logged.count, 1) 84 | XCTAssertEqual(recorder.logged[0].type, "Error") 85 | XCTAssertEqual(recorder.logged[0].value, TestError.somethingWentWrong.errorDescription) 86 | } 87 | 88 | func testErrorEventAfterValues() { 89 | let recorder = TestLog() 90 | Timelane.Subscription.didEmitVersion = true 91 | 92 | let (signal, observer) = Signal.pipe() 93 | signal 94 | .lane("Test Subscription", filter: .event, logger: recorder.log) 95 | .observe { _ in } 96 | observer.send(value: 1) 97 | observer.send(value: 2) 98 | observer.send(value: 3) 99 | observer.send(error: .somethingWentWrong) 100 | 101 | XCTAssertEqual(recorder.logged.count, 4) 102 | XCTAssertEqual(recorder.logged[0].outputTldr, "Output, Test Subscription, 1") 103 | XCTAssertEqual(recorder.logged[1].outputTldr, "Output, Test Subscription, 2") 104 | XCTAssertEqual(recorder.logged[2].outputTldr, "Output, Test Subscription, 3") 105 | XCTAssertEqual(recorder.logged[3].type, "Error") 106 | XCTAssertEqual(recorder.logged[3].value, TestError.somethingWentWrong.errorDescription) 107 | } 108 | 109 | func testCancelledEvent() { 110 | let recorder = TestLog() 111 | Timelane.Subscription.didEmitVersion = true 112 | 113 | let (signal, observer) = Signal.pipe() 114 | signal 115 | .lane("Test Subscription", filter: .event, logger: recorder.log) 116 | .observe { _ in } 117 | 118 | observer.send(value: 0) 119 | 120 | XCTAssertEqual(recorder.logged.count, 1) 121 | XCTAssertEqual(recorder.logged[0].outputTldr, "Output, Test Subscription, 0") 122 | 123 | observer.sendInterrupted() 124 | 125 | XCTAssertEqual(recorder.logged.count, 2) 126 | XCTAssertEqual(recorder.logged[1].type, "Cancelled") 127 | } 128 | 129 | // MARK: - Subscription 130 | 131 | func testSubscription() { 132 | let recorder = TestLog() 133 | Timelane.Subscription.didEmitVersion = true 134 | 135 | let (signal, observer) = Signal.pipe() 136 | signal 137 | .lane("Test Subscription", filter: .subscription, logger: recorder.log) 138 | .observe { _ in } 139 | 140 | XCTAssertEqual(recorder.logged.count, 1) 141 | XCTAssertEqual(recorder.logged[0].signpostType, "begin") 142 | XCTAssertEqual(recorder.logged[0].subscribe, "Test Subscription") 143 | 144 | observer.sendCompleted() 145 | 146 | XCTAssertEqual(recorder.logged[1].signpostType, "end") 147 | } 148 | 149 | // MARK: - All tests 150 | 151 | static var allTests = [ 152 | ("testCompletedEvent", testCompletedEvent), 153 | ("testValueEvents", testValueEvents), 154 | ("testFormatting", testFormatting), 155 | ("testErrorEvent", testErrorEvent), 156 | ("testErrorEventAfterValues", testErrorEventAfterValues), 157 | ("testCancelledEvent", testCancelledEvent), 158 | ("testSubscription", testSubscription), 159 | ] 160 | } 161 | -------------------------------------------------------------------------------- /Demo/ReactiveTimelaneDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5A076A49247E57000002B523 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A076A48247E57000002B523 /* AppDelegate.swift */; }; 11 | 5A076A4B247E57000002B523 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A076A4A247E57000002B523 /* SceneDelegate.swift */; }; 12 | 5A076A4F247E57020002B523 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5A076A4E247E57020002B523 /* Assets.xcassets */; }; 13 | 5A076A55247E57020002B523 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5A076A53247E57020002B523 /* LaunchScreen.storyboard */; }; 14 | 5A076A5E247E58000002B523 /* ReactiveTimelane in Frameworks */ = {isa = PBXBuildFile; productRef = 5A076A5D247E58000002B523 /* ReactiveTimelane */; }; 15 | 5A076A60247E581A0002B523 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A076A5F247E581A0002B523 /* ViewController.swift */; }; 16 | 5A076A63247E5BE40002B523 /* ReactiveCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 5A076A62247E5BE40002B523 /* ReactiveCocoa */; }; 17 | 5A076A65247E5C5C0002B523 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A076A64247E5C5C0002B523 /* App.swift */; }; 18 | 5A076A6C247E5E8E0002B523 /* ReactiveSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5A076A6B247E5E8E0002B523 /* ReactiveSwift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 5A076A45247E57000002B523 /* ReactiveTimelaneDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactiveTimelaneDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 5A076A48247E57000002B523 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | 5A076A4A247E57000002B523 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 25 | 5A076A4E247E57020002B523 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | 5A076A54247E57020002B523 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 27 | 5A076A56247E57020002B523 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 28 | 5A076A5F247E581A0002B523 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 29 | 5A076A64247E5C5C0002B523 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 30 | 5A076A6D247E6D560002B523 /* ReactiveTimelaneDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ReactiveTimelaneDemo.entitlements; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 5A076A42247E57000002B523 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | 5A076A5E247E58000002B523 /* ReactiveTimelane in Frameworks */, 39 | 5A076A6C247E5E8E0002B523 /* ReactiveSwift in Frameworks */, 40 | 5A076A63247E5BE40002B523 /* ReactiveCocoa in Frameworks */, 41 | ); 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXFrameworksBuildPhase section */ 45 | 46 | /* Begin PBXGroup section */ 47 | 5A076A3C247E57000002B523 = { 48 | isa = PBXGroup; 49 | children = ( 50 | 5A076A47247E57000002B523 /* ReactiveTimelaneDemo */, 51 | 5A076A46247E57000002B523 /* Products */, 52 | ); 53 | sourceTree = ""; 54 | }; 55 | 5A076A46247E57000002B523 /* Products */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | 5A076A45247E57000002B523 /* ReactiveTimelaneDemo.app */, 59 | ); 60 | name = Products; 61 | sourceTree = ""; 62 | }; 63 | 5A076A47247E57000002B523 /* ReactiveTimelaneDemo */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 5A076A6D247E6D560002B523 /* ReactiveTimelaneDemo.entitlements */, 67 | 5A076A66247E5D490002B523 /* App */, 68 | 5A076A69247E5D860002B523 /* Resources */, 69 | 5A076A5F247E581A0002B523 /* ViewController.swift */, 70 | ); 71 | path = ReactiveTimelaneDemo; 72 | sourceTree = ""; 73 | }; 74 | 5A076A66247E5D490002B523 /* App */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 5A076A64247E5C5C0002B523 /* App.swift */, 78 | 5A076A48247E57000002B523 /* AppDelegate.swift */, 79 | 5A076A4A247E57000002B523 /* SceneDelegate.swift */, 80 | ); 81 | path = App; 82 | sourceTree = ""; 83 | }; 84 | 5A076A69247E5D860002B523 /* Resources */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 5A076A4E247E57020002B523 /* Assets.xcassets */, 88 | 5A076A53247E57020002B523 /* LaunchScreen.storyboard */, 89 | 5A076A56247E57020002B523 /* Info.plist */, 90 | ); 91 | name = Resources; 92 | sourceTree = ""; 93 | }; 94 | /* End PBXGroup section */ 95 | 96 | /* Begin PBXNativeTarget section */ 97 | 5A076A44247E57000002B523 /* ReactiveTimelaneDemo */ = { 98 | isa = PBXNativeTarget; 99 | buildConfigurationList = 5A076A59247E57020002B523 /* Build configuration list for PBXNativeTarget "ReactiveTimelaneDemo" */; 100 | buildPhases = ( 101 | 5A076A41247E57000002B523 /* Sources */, 102 | 5A076A42247E57000002B523 /* Frameworks */, 103 | 5A076A43247E57000002B523 /* Resources */, 104 | ); 105 | buildRules = ( 106 | ); 107 | dependencies = ( 108 | ); 109 | name = ReactiveTimelaneDemo; 110 | packageProductDependencies = ( 111 | 5A076A5D247E58000002B523 /* ReactiveTimelane */, 112 | 5A076A62247E5BE40002B523 /* ReactiveCocoa */, 113 | 5A076A6B247E5E8E0002B523 /* ReactiveSwift */, 114 | ); 115 | productName = ReactiveTimelaneDemo; 116 | productReference = 5A076A45247E57000002B523 /* ReactiveTimelaneDemo.app */; 117 | productType = "com.apple.product-type.application"; 118 | }; 119 | /* End PBXNativeTarget section */ 120 | 121 | /* Begin PBXProject section */ 122 | 5A076A3D247E57000002B523 /* Project object */ = { 123 | isa = PBXProject; 124 | attributes = { 125 | LastSwiftUpdateCheck = 1140; 126 | LastUpgradeCheck = 1140; 127 | ORGANIZATIONNAME = "Niclas Kristek"; 128 | TargetAttributes = { 129 | 5A076A44247E57000002B523 = { 130 | CreatedOnToolsVersion = 11.4.1; 131 | }; 132 | }; 133 | }; 134 | buildConfigurationList = 5A076A40247E57000002B523 /* Build configuration list for PBXProject "ReactiveTimelaneDemo" */; 135 | compatibilityVersion = "Xcode 9.3"; 136 | developmentRegion = en; 137 | hasScannedForEncodings = 0; 138 | knownRegions = ( 139 | en, 140 | Base, 141 | ); 142 | mainGroup = 5A076A3C247E57000002B523; 143 | packageReferences = ( 144 | 5A076A5C247E58000002B523 /* XCRemoteSwiftPackageReference "ReactiveTimelane" */, 145 | 5A076A61247E5BE40002B523 /* XCRemoteSwiftPackageReference "ReactiveCocoa" */, 146 | 5A076A6A247E5E8E0002B523 /* XCRemoteSwiftPackageReference "ReactiveSwift" */, 147 | ); 148 | productRefGroup = 5A076A46247E57000002B523 /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 5A076A44247E57000002B523 /* ReactiveTimelaneDemo */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 5A076A43247E57000002B523 /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 5A076A55247E57020002B523 /* LaunchScreen.storyboard in Resources */, 163 | 5A076A4F247E57020002B523 /* Assets.xcassets in Resources */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXResourcesBuildPhase section */ 168 | 169 | /* Begin PBXSourcesBuildPhase section */ 170 | 5A076A41247E57000002B523 /* Sources */ = { 171 | isa = PBXSourcesBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | 5A076A49247E57000002B523 /* AppDelegate.swift in Sources */, 175 | 5A076A65247E5C5C0002B523 /* App.swift in Sources */, 176 | 5A076A4B247E57000002B523 /* SceneDelegate.swift in Sources */, 177 | 5A076A60247E581A0002B523 /* ViewController.swift in Sources */, 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | }; 181 | /* End PBXSourcesBuildPhase section */ 182 | 183 | /* Begin PBXVariantGroup section */ 184 | 5A076A53247E57020002B523 /* LaunchScreen.storyboard */ = { 185 | isa = PBXVariantGroup; 186 | children = ( 187 | 5A076A54247E57020002B523 /* Base */, 188 | ); 189 | name = LaunchScreen.storyboard; 190 | sourceTree = ""; 191 | }; 192 | /* End PBXVariantGroup section */ 193 | 194 | /* Begin XCBuildConfiguration section */ 195 | 5A076A57247E57020002B523 /* Debug */ = { 196 | isa = XCBuildConfiguration; 197 | buildSettings = { 198 | ALWAYS_SEARCH_USER_PATHS = NO; 199 | CLANG_ANALYZER_NONNULL = YES; 200 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 202 | CLANG_CXX_LIBRARY = "libc++"; 203 | CLANG_ENABLE_MODULES = YES; 204 | CLANG_ENABLE_OBJC_ARC = YES; 205 | CLANG_ENABLE_OBJC_WEAK = YES; 206 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 207 | CLANG_WARN_BOOL_CONVERSION = YES; 208 | CLANG_WARN_COMMA = YES; 209 | CLANG_WARN_CONSTANT_CONVERSION = YES; 210 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 211 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 212 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 213 | CLANG_WARN_EMPTY_BODY = YES; 214 | CLANG_WARN_ENUM_CONVERSION = YES; 215 | CLANG_WARN_INFINITE_RECURSION = YES; 216 | CLANG_WARN_INT_CONVERSION = YES; 217 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 219 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 220 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 221 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 222 | CLANG_WARN_STRICT_PROTOTYPES = YES; 223 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 224 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 225 | CLANG_WARN_UNREACHABLE_CODE = YES; 226 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 227 | COPY_PHASE_STRIP = NO; 228 | DEBUG_INFORMATION_FORMAT = dwarf; 229 | ENABLE_STRICT_OBJC_MSGSEND = YES; 230 | ENABLE_TESTABILITY = YES; 231 | GCC_C_LANGUAGE_STANDARD = gnu11; 232 | GCC_DYNAMIC_NO_PIC = NO; 233 | GCC_NO_COMMON_BLOCKS = YES; 234 | GCC_OPTIMIZATION_LEVEL = 0; 235 | GCC_PREPROCESSOR_DEFINITIONS = ( 236 | "DEBUG=1", 237 | "$(inherited)", 238 | ); 239 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 240 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 241 | GCC_WARN_UNDECLARED_SELECTOR = YES; 242 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 243 | GCC_WARN_UNUSED_FUNCTION = YES; 244 | GCC_WARN_UNUSED_VARIABLE = YES; 245 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 246 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 247 | MTL_FAST_MATH = YES; 248 | ONLY_ACTIVE_ARCH = YES; 249 | SDKROOT = iphoneos; 250 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 251 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 252 | }; 253 | name = Debug; 254 | }; 255 | 5A076A58247E57020002B523 /* Release */ = { 256 | isa = XCBuildConfiguration; 257 | buildSettings = { 258 | ALWAYS_SEARCH_USER_PATHS = NO; 259 | CLANG_ANALYZER_NONNULL = YES; 260 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 261 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 262 | CLANG_CXX_LIBRARY = "libc++"; 263 | CLANG_ENABLE_MODULES = YES; 264 | CLANG_ENABLE_OBJC_ARC = YES; 265 | CLANG_ENABLE_OBJC_WEAK = YES; 266 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 267 | CLANG_WARN_BOOL_CONVERSION = YES; 268 | CLANG_WARN_COMMA = YES; 269 | CLANG_WARN_CONSTANT_CONVERSION = YES; 270 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 271 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 272 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 273 | CLANG_WARN_EMPTY_BODY = YES; 274 | CLANG_WARN_ENUM_CONVERSION = YES; 275 | CLANG_WARN_INFINITE_RECURSION = YES; 276 | CLANG_WARN_INT_CONVERSION = YES; 277 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 278 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 279 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 280 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 281 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 282 | CLANG_WARN_STRICT_PROTOTYPES = YES; 283 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 284 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 285 | CLANG_WARN_UNREACHABLE_CODE = YES; 286 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 287 | COPY_PHASE_STRIP = NO; 288 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 289 | ENABLE_NS_ASSERTIONS = NO; 290 | ENABLE_STRICT_OBJC_MSGSEND = YES; 291 | GCC_C_LANGUAGE_STANDARD = gnu11; 292 | GCC_NO_COMMON_BLOCKS = YES; 293 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 294 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 295 | GCC_WARN_UNDECLARED_SELECTOR = YES; 296 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 297 | GCC_WARN_UNUSED_FUNCTION = YES; 298 | GCC_WARN_UNUSED_VARIABLE = YES; 299 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 300 | MTL_ENABLE_DEBUG_INFO = NO; 301 | MTL_FAST_MATH = YES; 302 | SDKROOT = iphoneos; 303 | SWIFT_COMPILATION_MODE = wholemodule; 304 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 305 | VALIDATE_PRODUCT = YES; 306 | }; 307 | name = Release; 308 | }; 309 | 5A076A5A247E57020002B523 /* Debug */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 313 | CODE_SIGN_ENTITLEMENTS = ReactiveTimelaneDemo/ReactiveTimelaneDemo.entitlements; 314 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 315 | CODE_SIGN_STYLE = Automatic; 316 | DEVELOPMENT_TEAM = ""; 317 | "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; 318 | ENABLE_PREVIEWS = YES; 319 | INFOPLIST_FILE = ReactiveTimelaneDemo/Info.plist; 320 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 321 | LD_RUNPATH_SEARCH_PATHS = ( 322 | "$(inherited)", 323 | "@executable_path/Frameworks", 324 | ); 325 | PRODUCT_BUNDLE_IDENTIFIER = com.nkristek.ReactiveTimelaneDemo; 326 | PRODUCT_NAME = "$(TARGET_NAME)"; 327 | SUPPORTS_MACCATALYST = NO; 328 | SWIFT_VERSION = 5.0; 329 | TARGETED_DEVICE_FAMILY = "1,2"; 330 | }; 331 | name = Debug; 332 | }; 333 | 5A076A5B247E57020002B523 /* Release */ = { 334 | isa = XCBuildConfiguration; 335 | buildSettings = { 336 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 337 | CODE_SIGN_ENTITLEMENTS = ReactiveTimelaneDemo/ReactiveTimelaneDemo.entitlements; 338 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 339 | CODE_SIGN_STYLE = Automatic; 340 | DEVELOPMENT_TEAM = ""; 341 | "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; 342 | ENABLE_PREVIEWS = YES; 343 | INFOPLIST_FILE = ReactiveTimelaneDemo/Info.plist; 344 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 345 | LD_RUNPATH_SEARCH_PATHS = ( 346 | "$(inherited)", 347 | "@executable_path/Frameworks", 348 | ); 349 | PRODUCT_BUNDLE_IDENTIFIER = com.nkristek.ReactiveTimelaneDemo; 350 | PRODUCT_NAME = "$(TARGET_NAME)"; 351 | SUPPORTS_MACCATALYST = NO; 352 | SWIFT_VERSION = 5.0; 353 | TARGETED_DEVICE_FAMILY = "1,2"; 354 | }; 355 | name = Release; 356 | }; 357 | /* End XCBuildConfiguration section */ 358 | 359 | /* Begin XCConfigurationList section */ 360 | 5A076A40247E57000002B523 /* Build configuration list for PBXProject "ReactiveTimelaneDemo" */ = { 361 | isa = XCConfigurationList; 362 | buildConfigurations = ( 363 | 5A076A57247E57020002B523 /* Debug */, 364 | 5A076A58247E57020002B523 /* Release */, 365 | ); 366 | defaultConfigurationIsVisible = 0; 367 | defaultConfigurationName = Release; 368 | }; 369 | 5A076A59247E57020002B523 /* Build configuration list for PBXNativeTarget "ReactiveTimelaneDemo" */ = { 370 | isa = XCConfigurationList; 371 | buildConfigurations = ( 372 | 5A076A5A247E57020002B523 /* Debug */, 373 | 5A076A5B247E57020002B523 /* Release */, 374 | ); 375 | defaultConfigurationIsVisible = 0; 376 | defaultConfigurationName = Release; 377 | }; 378 | /* End XCConfigurationList section */ 379 | 380 | /* Begin XCRemoteSwiftPackageReference section */ 381 | 5A076A5C247E58000002B523 /* XCRemoteSwiftPackageReference "ReactiveTimelane" */ = { 382 | isa = XCRemoteSwiftPackageReference; 383 | repositoryURL = "https://github.com/nkristek/ReactiveTimelane"; 384 | requirement = { 385 | kind = upToNextMajorVersion; 386 | minimumVersion = 1.0.1; 387 | }; 388 | }; 389 | 5A076A61247E5BE40002B523 /* XCRemoteSwiftPackageReference "ReactiveCocoa" */ = { 390 | isa = XCRemoteSwiftPackageReference; 391 | repositoryURL = "https://github.com/ReactiveCocoa/ReactiveCocoa"; 392 | requirement = { 393 | kind = upToNextMajorVersion; 394 | minimumVersion = 10.3.0; 395 | }; 396 | }; 397 | 5A076A6A247E5E8E0002B523 /* XCRemoteSwiftPackageReference "ReactiveSwift" */ = { 398 | isa = XCRemoteSwiftPackageReference; 399 | repositoryURL = "https://github.com/ReactiveCocoa/ReactiveSwift"; 400 | requirement = { 401 | kind = upToNextMajorVersion; 402 | minimumVersion = 6.3.0; 403 | }; 404 | }; 405 | /* End XCRemoteSwiftPackageReference section */ 406 | 407 | /* Begin XCSwiftPackageProductDependency section */ 408 | 5A076A5D247E58000002B523 /* ReactiveTimelane */ = { 409 | isa = XCSwiftPackageProductDependency; 410 | package = 5A076A5C247E58000002B523 /* XCRemoteSwiftPackageReference "ReactiveTimelane" */; 411 | productName = ReactiveTimelane; 412 | }; 413 | 5A076A62247E5BE40002B523 /* ReactiveCocoa */ = { 414 | isa = XCSwiftPackageProductDependency; 415 | package = 5A076A61247E5BE40002B523 /* XCRemoteSwiftPackageReference "ReactiveCocoa" */; 416 | productName = ReactiveCocoa; 417 | }; 418 | 5A076A6B247E5E8E0002B523 /* ReactiveSwift */ = { 419 | isa = XCSwiftPackageProductDependency; 420 | package = 5A076A6A247E5E8E0002B523 /* XCRemoteSwiftPackageReference "ReactiveSwift" */; 421 | productName = ReactiveSwift; 422 | }; 423 | /* End XCSwiftPackageProductDependency section */ 424 | }; 425 | rootObject = 5A076A3D247E57000002B523 /* Project object */; 426 | } 427 | --------------------------------------------------------------------------------