├── .swift-version ├── Package.swift ├── .gitignore ├── Tests ├── TestPrinter.swift ├── MetricsTests.swift ├── SessionMetricsTests.swift └── RenderTests.swift ├── Configs ├── TumbleweedTests.plist └── Tumbleweed.plist ├── Tumbleweed.podspec ├── LICENSE ├── Sources ├── SessionMetrics.swift ├── Metric.swift └── Renderer.swift ├── README.md └── Tumbleweed.xcodeproj ├── xcshareddata └── xcschemes │ ├── Tumbleweed-watchOS.xcscheme │ ├── Tumbleweed-iOS.xcscheme │ ├── Tumbleweed-tvOS.xcscheme │ └── Tumbleweed-macOS.xcscheme └── project.pbxproj /.swift-version: -------------------------------------------------------------------------------- 1 | 3.1 2 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "Tumbleweed" 5 | ) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # xcode noise 2 | build/* 3 | dist/* 4 | *.pbxuser 5 | *.mode1v3 6 | *.mode2v3 7 | *.perspectivev3 8 | *.moved-aside 9 | 10 | # osx noise 11 | .DS_Store 12 | profile 13 | 14 | *.xcodeproj/project.xcworkspace/* 15 | */xcuserdata/* 16 | 17 | docs/* 18 | -------------------------------------------------------------------------------- /Tests/TestPrinter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestPrinter.swift 3 | // Tumbleweed 4 | // 5 | // Created by Johan Sørensen on 07/04/2017. 6 | // Copyright © 2017 NRK. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class OutputBufferingPrinter { 12 | private(set) var output = "" 13 | 14 | func print(_ line: String) { 15 | output.append(line) 16 | } 17 | 18 | func reset() { 19 | output = "" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Configs/TumbleweedTests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tumbleweed.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Tumbleweed" 3 | s.version = "0.1" 4 | s.summary = "Logs detailed metrics about URLSession tasks to the console" 5 | s.description = <<-DESC 6 | Logs detailed metrics about URLSession tasks to the console, such as DNS lookup durations, request and response times and more 7 | DESC 8 | s.homepage = "https://github.com/nrkno/Tumbleweed" 9 | s.license = { :type => "MIT", :file => "LICENSE" } 10 | s.author = { "Johan Sørensen" => "johan.sorensen@nrk.no" } 11 | s.ios.deployment_target = "10.0" 12 | s.osx.deployment_target = "10.12" 13 | #s.watchos.deployment_target = "3.0" 14 | s.tvos.deployment_target = "10.0" 15 | s.source = { :git => "https://github.com/nrkno/Tumbleweed.git", :tag => s.version.to_s } 16 | s.source_files = "Sources/**/*" 17 | s.frameworks = "Foundation" 18 | end 19 | -------------------------------------------------------------------------------- /Configs/Tumbleweed.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2017 Johan Sørensen. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 NRK 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 | 23 | -------------------------------------------------------------------------------- /Tests/MetricsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetricsTests.swift 3 | // Tumbleweed 4 | // 5 | // Created by Johan Sørensen on 06/04/2017. 6 | // Copyright © 2017 NRK. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import Tumbleweed 12 | // 13 | //class MetricsTests: XCTestCase { 14 | // 15 | // func testDurations() { 16 | // let mock = MockURLSessionTaskTransactionMetrics(request: request()) 17 | // let 18 | // } 19 | // 20 | // // MARK: - Helpers 21 | // 22 | // private func request(url: URL = URL(string: "http://example.com")!, method: String = "GET") -> URLRequest { 23 | // var request = URLRequest(url: url) 24 | // request.httpMethod = method 25 | // return request 26 | // } 27 | //} 28 | // 29 | //@available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 30 | //private class MockURLSessionTaskTransactionMetrics: Measurable { 31 | // var request: URLRequest 32 | // var networkProtocolName: String? = "HTTP/1.1" 33 | // var isProxyConnection: Bool = false 34 | // var isReusedConnection: Bool = false 35 | // var resourceFetchType: URLSessionTaskMetrics.ResourceFetchType = .networkLoad 36 | // 37 | // init(request: URLRequest) { 38 | // self.request = request 39 | // } 40 | //} 41 | -------------------------------------------------------------------------------- /Sources/SessionMetrics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tumbleweed.swift 3 | // NRK 4 | // 5 | // Created by Johan Sørensen on 06/04/2017. 6 | // Copyright © 2017 NRK. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An object that is capable of collection metrics based on a given set of URLSessionTaskMetrics 12 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 13 | public struct SessionMetrics { 14 | public let task: URLSessionTask 15 | public let metrics: [Metric] 16 | public let redirectCount: Int 17 | public let taskInterval: DateInterval 18 | 19 | public init(source sessionTaskMetrics: URLSessionTaskMetrics, task: URLSessionTask) { 20 | self.task = task 21 | self.redirectCount = sessionTaskMetrics.redirectCount 22 | self.taskInterval = sessionTaskMetrics.taskInterval 23 | self.metrics = sessionTaskMetrics.transactionMetrics.map(Metric.init(transactionMetrics:)) 24 | } 25 | 26 | public func render(with renderer: Renderer) { 27 | renderer.render(with: self) 28 | } 29 | } 30 | 31 | /// Convenience object that can be used as the delegate for a URLSession 32 | /// eg let session = URLSession(configuration: .default, delegate: SessionMetricsLogger(), delegateQueue: nil) 33 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 34 | public final class SessionMetricsLogger: NSObject, URLSessionTaskDelegate { 35 | let renderer = ConsoleRenderer() 36 | var enabled = true 37 | 38 | public func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { 39 | guard enabled else { return } 40 | 41 | let gatherer = SessionMetrics(source: metrics, task: task) 42 | renderer.render(with: gatherer) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/SessionMetricsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TumbleweedTests.swift 3 | // NRK 4 | // 5 | // Created by Johan Sørensen on 06/04/2017. 6 | // Copyright © 2017 NRK. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Tumbleweed 12 | 13 | class SessionMetricsTests: XCTestCase { 14 | let printer = OutputBufferingPrinter() 15 | 16 | override func setUp() { 17 | super.setUp() 18 | printer.reset() 19 | } 20 | 21 | func testOutput() { 22 | let client = Client(printer: printer.print) 23 | 24 | let wait = expectation(description: "async") 25 | let request = URLRequest(url: URL(string: "https://httpbin.org/get")!) 26 | client.perform(request: request) { (data, error) in 27 | XCTAssert(error == nil) 28 | wait.fulfill() 29 | } 30 | 31 | waitForExpectations(timeout: 1.0) { (error) in 32 | let lines = self.printer.output.components(separatedBy: "\n") 33 | XCTAssertEqual(lines.count, 15) 34 | XCTAssertTrue(lines[0].hasPrefix("Task ID: 1 lifetime: ")) 35 | XCTAssertEqual(lines[1], "GET https://httpbin.org/get -> 200 application/json, through local-cache") 36 | XCTAssertEqual(lines[6], "GET https://httpbin.org/get -> 200 application/json, through network-load") 37 | print("") 38 | print(self.printer.output) 39 | print("") 40 | } 41 | } 42 | } 43 | 44 | final class Client { 45 | let session: URLSession 46 | 47 | init(printer: @escaping (String) -> Void) { 48 | let delegate = SessionDelegate(printer: printer) 49 | self.session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) 50 | } 51 | 52 | func perform(request: URLRequest, completion: @escaping (Data?, Error?) -> Void) { 53 | let task = session.dataTask(with: request) { (data, response, error) in 54 | completion(data, error) 55 | } 56 | task.resume() 57 | } 58 | } 59 | 60 | final class SessionDelegate: NSObject, URLSessionDelegate, URLSessionTaskDelegate { 61 | let printer: (String) -> Void 62 | 63 | init(printer: @escaping (String) -> Void) { 64 | self.printer = printer 65 | super.init() 66 | } 67 | 68 | func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { 69 | let stats = SessionMetrics(source: metrics, task: task) 70 | var renderer = ConsoleRenderer() 71 | renderer.printer = self.printer 72 | stats.render(with: renderer) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tumbleweed 2 | 3 | Logs detailed metrics about URLSession tasks to the console, such as DNS lookup durations, request and response times and more. Tumbleweed works by parsing the metrics collected by the `URLSessionTaskMetrics` introduced in iOS 10.0. 4 | 5 | ## Example output 6 | 7 | ``` 8 | Task ID: 1 lifetime: 594.9ms redirects: 0 9 | GET https://github.com/ -> 200 text/html, through network-load 10 | protocol: http/1.1 proxy: false reusedconn: false 11 | domain lookup |# | 3.0ms 12 | connect |############################################### | 330.0ms 13 | secure connection | ################################ | 223.0ms 14 | request | # | 0.2ms 15 | server | ################### | 130.9ms 16 | response | ###############| 105.8ms 17 | total 575.0ms 18 | ``` 19 | 20 | A few notes on the data: 21 | * Task lifetime is the time of the URLSessionTask creation to the response is delivered. 22 | * The "through" key on the second line will tell you whether the response was fetched from cache, delivered by server push or by network load. 23 | * "server" is how long the server spent processing the request 24 | 25 | _Note_ that not all responses will deliver complete metrics, such as local cache fetches and other non-network loaded responses. 26 | 27 | ## Usage 28 | 29 | In your `URLSessionDelegate`: 30 | 31 | ```swift 32 | func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { 33 | let stats = SessionMetrics(source: metrics, task: task) 34 | let renderer = ConsoleRenderer() 35 | renderer.render(with: stats) 36 | } 37 | ``` 38 | 39 | As a convenience Tumbleweed provides the `SessionMetricsLogger` which can be set as the `URLSessionDelegate` on `URLSession`, if you're not using any of the other delegates. 40 | 41 | ```Swift 42 | let sessionDelegate: URLSessionDelegate? 43 | if #available(iOS 10.0, *) { 44 | sessionDelegate = SessionMetricsLogger() 45 | } else { 46 | sessionDelegate = nil 47 | } 48 | let session = URLSession(configuration: .default, delegate: sessionDelegate, delegateQueue: nil) 49 | ``` 50 | 51 | ## Installation 52 | 53 | Tumbleweed is available as a CocoaPod (`pod 'Tumbleweed'`) and the Swift Package Manager. Framework installation is also available by dragging the Tumbleweed.xcodeproj into your project or via Carthage. 54 | 55 | Tumbleweed has no dependencies outside of Foundation. 56 | 57 | ## License 58 | 59 | See the LICENSE file 60 | -------------------------------------------------------------------------------- /Tumbleweed.xcodeproj/xcshareddata/xcschemes/Tumbleweed-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 71 | 72 | 73 | 74 | 76 | 77 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /Tests/RenderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RenderTests.swift 3 | // Tumbleweed 4 | // 5 | // Created by Johan Sørensen on 07/04/2017. 6 | // Copyright © 2017 NRK. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tumbleweed 11 | 12 | class RenderTests: XCTestCase { 13 | let metric = Metric(transactionMetrics: TestMeasurable()) 14 | let printer = OutputBufferingPrinter() 15 | var renderer: ConsoleRenderer! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | printer.reset() 20 | renderer = ConsoleRenderer() 21 | renderer.printer = printer.print 22 | } 23 | 24 | func testHeader() { 25 | let output = renderer.renderHeader(with: metric) 26 | XCTAssertEqual(output, "GET http://example.com -> 200 text/plain, through network-load") 27 | } 28 | 29 | func testMeta() { 30 | let output = renderer.renderMeta(with: metric) 31 | XCTAssertEqual(output, "protocol: HTTP/1.1 proxy: false reusedconn: false") 32 | } 33 | 34 | func testDuration() { 35 | let duration = Metric.Duration(type: .domainLookup, interval: DateInterval(start: seconds(ago: 3), end: seconds(ago: 2))) 36 | let total = DateInterval(start: seconds(ago: 3), end: seconds(ago: 0)) 37 | let output = renderer.renderDuration(line: duration, total: total) 38 | XCTAssertEqual(output, "domain lookup |########################### |1000.0ms") 39 | } 40 | 41 | func testTotalDuration() { 42 | let duration1 = Metric.Duration(type: .request, interval: DateInterval(start: seconds(ago: 3), end: seconds(ago: 2))) 43 | let duration2 = Metric.Duration(type: .response, interval: DateInterval(start: seconds(ago: 2), end: seconds(ago: 1))) 44 | let metric = Metric(transactionMetrics: TestMeasurable(), durations: [duration1, duration2]) 45 | let total = renderer.totalDateInterval(from: metric) 46 | XCTAssert(total != nil) 47 | XCTAssertEqual(total!, DateInterval(start: duration1.interval.start, end: duration2.interval.end)) 48 | } 49 | 50 | func testSummary() { 51 | let interval = DateInterval(start: seconds(ago: 3), end: seconds(ago: 0)) 52 | let output = renderer.renderMetricSummary(for: interval) 53 | XCTAssertEqual(output, " total 3000.0ms") 54 | } 55 | } 56 | 57 | let now = Date() 58 | func seconds(ago: Int, from: Date = now) -> Date { 59 | return Calendar.current.date(byAdding: DateComponents(second: -ago), to: from)! 60 | } 61 | 62 | class TestMeasurable: Measurable { 63 | var request: URLRequest = URLRequest(url: URL(string: "http://example.com")!) 64 | var response: URLResponse? = HTTPURLResponse(url: URL(string: "http://example.com")!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: ["Content-Type": "text/plain"]) 65 | 66 | var networkProtocolName: String? = "HTTP/1.1" 67 | var isProxyConnection: Bool = false 68 | var isReusedConnection: Bool = false 69 | var resourceFetchType: URLSessionTaskMetrics.ResourceFetchType = .networkLoad 70 | 71 | var domainLookupStartDate: Date? = seconds(ago: 10) 72 | var domainLookupEndDate: Date? = seconds(ago: 9) 73 | 74 | var connectStartDate: Date? = seconds(ago: 8) 75 | var connectEndDate: Date? = seconds(ago: 6) 76 | var secureConnectionStartDate: Date? = seconds(ago: 7) 77 | var secureConnectionEndDate: Date? = seconds(ago:6) 78 | 79 | var requestStartDate: Date? = seconds(ago: 4) 80 | var requestEndDate: Date? = seconds(ago: 3) 81 | var responseStartDate: Date? = seconds(ago: 2) 82 | var responseEndDate: Date? = seconds(ago: 1) 83 | } 84 | -------------------------------------------------------------------------------- /Tumbleweed.xcodeproj/xcshareddata/xcschemes/Tumbleweed-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Tumbleweed.xcodeproj/xcshareddata/xcschemes/Tumbleweed-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Tumbleweed.xcodeproj/xcshareddata/xcschemes/Tumbleweed-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Sources/Metric.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Metric.swift 3 | // Tumbleweed 4 | // 5 | // Created by Johan Sørensen on 06/04/2017. 6 | // Copyright © 2017 NRK. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 12 | public protocol Measurable { 13 | var request: URLRequest { get } 14 | var response: URLResponse? { get } 15 | 16 | var networkProtocolName: String? { get } 17 | var isProxyConnection: Bool { get } 18 | var isReusedConnection: Bool { get } 19 | var resourceFetchType: URLSessionTaskMetrics.ResourceFetchType { get } 20 | 21 | var domainLookupStartDate: Date? { get } 22 | var domainLookupEndDate: Date? { get } 23 | 24 | var connectStartDate: Date? { get } 25 | var connectEndDate: Date? { get } 26 | var secureConnectionStartDate: Date? { get } 27 | var secureConnectionEndDate: Date? { get } 28 | 29 | var requestStartDate: Date? { get } 30 | var requestEndDate: Date? { get } 31 | var responseStartDate: Date? { get } 32 | var responseEndDate: Date? { get } 33 | } 34 | 35 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 36 | extension URLSessionTaskTransactionMetrics: Measurable {} 37 | 38 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 39 | extension URLSessionTaskMetrics.ResourceFetchType { 40 | var name: String { 41 | switch self { 42 | case .unknown: 43 | return "unknown" 44 | case .networkLoad: 45 | return "network-load" 46 | case .serverPush: 47 | return "server-push" 48 | case .localCache: 49 | return "local-cache" 50 | } 51 | } 52 | } 53 | 54 | private extension Array where Element == Metric.Duration { 55 | func find(type: Metric.DurationType) -> Element? { 56 | return self.filter({ $0.type == type }).first 57 | } 58 | } 59 | 60 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 61 | public struct Metric { 62 | public let transactionMetrics: Measurable 63 | public let durations: [Duration] 64 | 65 | public init(transactionMetrics metrics: Measurable) { 66 | self.transactionMetrics = metrics 67 | 68 | func check(type: DurationType, _ start: Date?, _ end: Date?) -> Duration? { 69 | guard let start = start, let end = end else { return nil } 70 | return Duration(type: type, interval: DateInterval(start: start, end: end)) 71 | } 72 | 73 | var durations: [Duration] = [] 74 | // domain, {secure}connect is nil f a persistent connection was used or it was retrieved from local cache 75 | if let duration = check(type: .domainLookup, metrics.domainLookupStartDate, metrics.domainLookupEndDate) { 76 | durations.append(duration) 77 | } 78 | if let duration = check(type: .connect, metrics.connectStartDate, metrics.connectEndDate) { 79 | durations.append(duration) 80 | } 81 | if let duration = check(type: .secureConnection, metrics.secureConnectionStartDate, metrics.secureConnectionEndDate) { 82 | durations.append(duration) 83 | } 84 | if let duration = check(type: .request, metrics.requestStartDate, metrics.requestEndDate) { 85 | durations.append(duration) 86 | } 87 | if let duration = check(type: .response, metrics.responseStartDate, metrics.responseEndDate) { 88 | durations.append(duration) 89 | } 90 | if let duration = check(type: .total, metrics.domainLookupStartDate, metrics.responseEndDate) { 91 | durations.append(duration) 92 | } 93 | 94 | // Calculate how long the server spent processing the request 95 | if let request = durations.find(type: .request), 96 | let response = durations.find(type: .response), 97 | let index = durations.index(of: response), 98 | request.interval.duration > 0 { 99 | let interval = DateInterval(start: request.interval.end, end: response.interval.start) 100 | let duration = Duration(type: .server, interval: interval) 101 | durations.insert(duration, at: index) 102 | } 103 | 104 | self.durations = durations 105 | } 106 | 107 | internal init(transactionMetrics metrics: Measurable, durations: [Duration]) { 108 | self.transactionMetrics = metrics 109 | self.durations = durations 110 | } 111 | 112 | public enum DurationType { 113 | case domainLookup 114 | case connect 115 | case secureConnection 116 | case request 117 | case server 118 | case response 119 | case total 120 | 121 | public var name: String { 122 | switch self { 123 | case .domainLookup: 124 | return "domain lookup" 125 | case .connect: 126 | return "connect" 127 | case .secureConnection: 128 | return "secure connection" 129 | case .request: 130 | return "request" 131 | case .server: 132 | return "server" 133 | case .response: 134 | return "response" 135 | case .total: 136 | return "total" 137 | } 138 | } 139 | } 140 | 141 | public struct Duration { 142 | public let type: DurationType 143 | public let interval: DateInterval 144 | } 145 | } 146 | 147 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 148 | extension Metric.Duration: Equatable { 149 | public static func ==(lhs: Metric.Duration, rhs: Metric.Duration) -> Bool { 150 | return rhs.type == lhs.type && rhs.interval == rhs.interval 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/Renderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Renderer.swift 3 | // Tumbleweed 4 | // 5 | // Created by Johan Sørensen on 06/04/2017. 6 | // Copyright © 2017 NRK. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 12 | public protocol Renderer { 13 | func render(with stats: SessionMetrics) 14 | } 15 | 16 | /// Renders given SessionMetrics to the console. 17 | /// 18 | /// Task ID: 1 lifetime: 485.3ms redirects: 0 19 | /// GET https://httpbin.org/get -> 200 application/json, through local-cache 20 | /// protocol: ??? proxy: false reusedconn: true 21 | /// request | | 0.0ms 22 | /// response |################################################################################| 1.0ms 23 | /// total 1.0ms 24 | /// GET https://httpbin.org/get -> 200 application/json, through network-load 25 | /// protocol: http/1.1 proxy: false reusedconn: false 26 | /// domain lookup |###### | 34.0ms 27 | /// connect | ####################################################### | 316.0ms 28 | /// secure connection | ###################################### | 216.0ms 29 | /// request | # | 0.1ms 30 | /// response | #| 0.2ms 31 | /// total 465.5ms 32 | @available(iOS 10.0, macOS 10.12, tvOS 10.0, *) 33 | public struct ConsoleRenderer: Renderer { 34 | public var printer: (String) -> Void = { NSLog($0) } 35 | let columns = (left: 18, middle: 82, right: 8) 36 | 37 | public init() { 38 | 39 | } 40 | 41 | public func render(with stats: SessionMetrics) { 42 | var buffer: [String] = [] 43 | buffer.append("Task ID: \(stats.task.taskIdentifier) lifetime: \(stats.taskInterval.duration.ms) redirects: \(stats.redirectCount)") 44 | for metric in stats.metrics { 45 | buffer.append(renderHeader(with: metric)) 46 | buffer.append(renderMeta(with: metric)) 47 | let total = totalDateInterval(from: metric) 48 | for line in metric.durations.filter({ $0.type != .total }) { 49 | buffer.append(renderDuration(line: line, total: total)) 50 | } 51 | if let total = total { 52 | buffer.append(renderMetricSummary(for: total)) 53 | } 54 | } 55 | 56 | printer(buffer.joined(separator: "\n")) 57 | } 58 | 59 | func totalDateInterval(from metric: Metric) -> DateInterval? { 60 | if let total = metric.durations.filter({ $0.type == .total }).first { 61 | return total.interval 62 | } else if let first = metric.durations.first { 63 | // calculate total from all available Durations 64 | var total = first.interval 65 | total.duration += metric.durations.dropFirst().reduce(TimeInterval(0), { accumulated, duration in 66 | return accumulated + duration.interval.duration 67 | }) 68 | return total 69 | } 70 | return nil 71 | } 72 | 73 | func renderHeader(with metric: Metric) -> String { 74 | let method = metric.transactionMetrics.request.httpMethod ?? "???" 75 | let url = metric.transactionMetrics.request.url?.absoluteString ?? "???" 76 | 77 | let responseLine: String 78 | if let response = metric.transactionMetrics.response as? HTTPURLResponse { 79 | let mime = response.mimeType ?? "" 80 | responseLine = "\(response.statusCode) \(mime)" 81 | } else { 82 | responseLine = "[response error]" 83 | } 84 | return "\(method) \(url) -> \(responseLine), through \(metric.transactionMetrics.resourceFetchType.name)" 85 | } 86 | 87 | func renderDuration(line: Metric.Duration, total: DateInterval?) -> String { 88 | let name = line.type.name.padding(toLength: columns.left, withPad: " ", startingAt: 0) 89 | let plot = total.flatMap({ visualize(interval: line.interval, total: $0, within: self.columns.middle) }) ?? "" 90 | let time = line.interval.duration.ms.leftPadding(toLength: columns.right, withPad: " ") 91 | return "\(name)\(plot)\(time)" 92 | } 93 | 94 | func visualize(interval: DateInterval, total: DateInterval, within: Int = 100) -> String { 95 | precondition(total.intersects(total), "supplied duration does not intersect with the total duration") 96 | let width = within - 2 97 | if interval.duration == 0 { 98 | return "|" + String(repeatElement(" ", count: width)) + "|" 99 | } 100 | 101 | let relativeStart = (interval.start.timeIntervalSince1970 - total.start.timeIntervalSince1970) / total.duration 102 | let relativeEnd = 1.0 - (total.end.timeIntervalSince1970 - interval.end.timeIntervalSince1970) / total.duration 103 | 104 | let factor = 1.0 / Double(width) 105 | let startIndex = Int((relativeStart / factor)) 106 | let endIndex = Int((relativeEnd / factor)) 107 | 108 | let line: [String] = (0..= startIndex && position <= endIndex { 110 | return "#" 111 | } else { 112 | return " " 113 | } 114 | } 115 | return "|\(line.joined())|" 116 | } 117 | 118 | func renderMeta(with metric: Metric) -> String { 119 | let networkProtocolName = metric.transactionMetrics.networkProtocolName ?? "???" 120 | let meta = [ 121 | "protocol: \(networkProtocolName)", 122 | "proxy: \(metric.transactionMetrics.isProxyConnection)", 123 | "reusedconn: \(metric.transactionMetrics.isReusedConnection)", 124 | ] 125 | return meta.joined(separator: " ") 126 | } 127 | 128 | func renderMetricSummary(for interval: DateInterval) -> String { 129 | let width = columns.left + columns.middle + columns.right 130 | return "total \(interval.duration.ms)".leftPadding(toLength: width, withPad: " ") 131 | } 132 | } 133 | 134 | private extension TimeInterval { 135 | var ms: String { 136 | return String(format: "%.1fms", self * 1000) 137 | } 138 | } 139 | 140 | private extension String { 141 | func leftPadding(toLength: Int, withPad character: Character) -> String { 142 | let newLength = self.characters.count 143 | if newLength < toLength { 144 | return String(repeatElement(character, count: toLength - newLength)) + self 145 | } else { 146 | return self.substring(from: index(self.startIndex, offsetBy: newLength - toLength)) 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Tumbleweed.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 47; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 52D6D9871BEFF229002C0205 /* Tumbleweed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* Tumbleweed.framework */; }; 11 | D96042CD1E962B6E002F4A75 /* SessionMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96042CC1E962B6E002F4A75 /* SessionMetrics.swift */; }; 12 | D96042D21E962C0B002F4A75 /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96042D11E962C0B002F4A75 /* Renderer.swift */; }; 13 | D96042D31E962C0B002F4A75 /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96042D11E962C0B002F4A75 /* Renderer.swift */; }; 14 | D96042D41E962C0B002F4A75 /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96042D11E962C0B002F4A75 /* Renderer.swift */; }; 15 | D96042D51E962C0B002F4A75 /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96042D11E962C0B002F4A75 /* Renderer.swift */; }; 16 | D985F9311E9635D300E31C0B /* SessionMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96042CF1E962BC6002F4A75 /* SessionMetricsTests.swift */; }; 17 | D985F9321E9635D400E31C0B /* SessionMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96042CF1E962BC6002F4A75 /* SessionMetricsTests.swift */; }; 18 | D985F9331E9635D400E31C0B /* SessionMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96042CF1E962BC6002F4A75 /* SessionMetricsTests.swift */; }; 19 | D985F9351E963AD000E31C0B /* MetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985F9341E963AD000E31C0B /* MetricsTests.swift */; }; 20 | D985F9361E963AD000E31C0B /* MetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985F9341E963AD000E31C0B /* MetricsTests.swift */; }; 21 | D985F9371E963AD000E31C0B /* MetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985F9341E963AD000E31C0B /* MetricsTests.swift */; }; 22 | D985F9391E964EFC00E31C0B /* Metric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985F9381E964EFC00E31C0B /* Metric.swift */; }; 23 | D985F93A1E964EFC00E31C0B /* Metric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985F9381E964EFC00E31C0B /* Metric.swift */; }; 24 | D985F93B1E964EFC00E31C0B /* Metric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985F9381E964EFC00E31C0B /* Metric.swift */; }; 25 | D985F93C1E964EFC00E31C0B /* Metric.swift in Sources */ = {isa = PBXBuildFile; fileRef = D985F9381E964EFC00E31C0B /* Metric.swift */; }; 26 | D9A296FC1E97888D003638DE /* RenderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9A296FA1E97888D003638DE /* RenderTests.swift */; }; 27 | D9A296FD1E97888D003638DE /* RenderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9A296FA1E97888D003638DE /* RenderTests.swift */; }; 28 | D9A296FE1E97888D003638DE /* RenderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9A296FA1E97888D003638DE /* RenderTests.swift */; }; 29 | D9A296FF1E97888D003638DE /* TestPrinter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9A296FB1E97888D003638DE /* TestPrinter.swift */; }; 30 | D9A297001E97888D003638DE /* TestPrinter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9A296FB1E97888D003638DE /* TestPrinter.swift */; }; 31 | D9A297011E97888D003638DE /* TestPrinter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9A296FB1E97888D003638DE /* TestPrinter.swift */; }; 32 | D9A297021E97B4C9003638DE /* SessionMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96042CC1E962B6E002F4A75 /* SessionMetrics.swift */; }; 33 | D9A297031E97B4CA003638DE /* SessionMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96042CC1E962B6E002F4A75 /* SessionMetrics.swift */; }; 34 | D9A297041E97B4CB003638DE /* SessionMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96042CC1E962B6E002F4A75 /* SessionMetrics.swift */; }; 35 | DD7502881C68FEDE006590AF /* Tumbleweed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6DA0F1BF000BD002C0205 /* Tumbleweed.framework */; }; 36 | DD7502921C690C7A006590AF /* Tumbleweed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D9F01BEFFFBE002C0205 /* Tumbleweed.framework */; }; 37 | /* End PBXBuildFile section */ 38 | 39 | /* Begin PBXContainerItemProxy section */ 40 | 52D6D9881BEFF229002C0205 /* PBXContainerItemProxy */ = { 41 | isa = PBXContainerItemProxy; 42 | containerPortal = 52D6D9731BEFF229002C0205 /* Project object */; 43 | proxyType = 1; 44 | remoteGlobalIDString = 52D6D97B1BEFF229002C0205; 45 | remoteInfo = Tumbleweed; 46 | }; 47 | DD7502801C68FCFC006590AF /* PBXContainerItemProxy */ = { 48 | isa = PBXContainerItemProxy; 49 | containerPortal = 52D6D9731BEFF229002C0205 /* Project object */; 50 | proxyType = 1; 51 | remoteGlobalIDString = 52D6DA0E1BF000BD002C0205; 52 | remoteInfo = "Tumbleweed-macOS"; 53 | }; 54 | DD7502931C690C7A006590AF /* PBXContainerItemProxy */ = { 55 | isa = PBXContainerItemProxy; 56 | containerPortal = 52D6D9731BEFF229002C0205 /* Project object */; 57 | proxyType = 1; 58 | remoteGlobalIDString = 52D6D9EF1BEFFFBE002C0205; 59 | remoteInfo = "Tumbleweed-tvOS"; 60 | }; 61 | /* End PBXContainerItemProxy section */ 62 | 63 | /* Begin PBXFileReference section */ 64 | 52D6D97C1BEFF229002C0205 /* Tumbleweed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Tumbleweed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 52D6D9861BEFF229002C0205 /* Tumbleweed-iOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tumbleweed-iOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | 52D6D9E21BEFFF6E002C0205 /* Tumbleweed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Tumbleweed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 67 | 52D6D9F01BEFFFBE002C0205 /* Tumbleweed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Tumbleweed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | 52D6DA0F1BF000BD002C0205 /* Tumbleweed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Tumbleweed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 69 | AD2FAA261CD0B6D800659CF4 /* Tumbleweed.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Tumbleweed.plist; sourceTree = ""; }; 70 | AD2FAA281CD0B6E100659CF4 /* TumbleweedTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = TumbleweedTests.plist; sourceTree = ""; }; 71 | D96042CC1E962B6E002F4A75 /* SessionMetrics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SessionMetrics.swift; path = Sources/SessionMetrics.swift; sourceTree = ""; }; 72 | D96042CF1E962BC6002F4A75 /* SessionMetricsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SessionMetricsTests.swift; path = Tests/SessionMetricsTests.swift; sourceTree = ""; }; 73 | D96042D11E962C0B002F4A75 /* Renderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Renderer.swift; path = Sources/Renderer.swift; sourceTree = ""; }; 74 | D985F9341E963AD000E31C0B /* MetricsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MetricsTests.swift; path = Tests/MetricsTests.swift; sourceTree = ""; }; 75 | D985F9381E964EFC00E31C0B /* Metric.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Metric.swift; path = Sources/Metric.swift; sourceTree = ""; }; 76 | D9A296FA1E97888D003638DE /* RenderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RenderTests.swift; path = Tests/RenderTests.swift; sourceTree = ""; }; 77 | D9A296FB1E97888D003638DE /* TestPrinter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TestPrinter.swift; path = Tests/TestPrinter.swift; sourceTree = ""; }; 78 | DD75027A1C68FCFC006590AF /* Tumbleweed-macOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tumbleweed-macOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 79 | DD75028D1C690C7A006590AF /* Tumbleweed-tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tumbleweed-tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | /* End PBXFileReference section */ 81 | 82 | /* Begin PBXFrameworksBuildPhase section */ 83 | 52D6D9781BEFF229002C0205 /* Frameworks */ = { 84 | isa = PBXFrameworksBuildPhase; 85 | buildActionMask = 2147483647; 86 | files = ( 87 | ); 88 | runOnlyForDeploymentPostprocessing = 0; 89 | }; 90 | 52D6D9831BEFF229002C0205 /* Frameworks */ = { 91 | isa = PBXFrameworksBuildPhase; 92 | buildActionMask = 2147483647; 93 | files = ( 94 | 52D6D9871BEFF229002C0205 /* Tumbleweed.framework in Frameworks */, 95 | ); 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | 52D6D9DE1BEFFF6E002C0205 /* Frameworks */ = { 99 | isa = PBXFrameworksBuildPhase; 100 | buildActionMask = 2147483647; 101 | files = ( 102 | ); 103 | runOnlyForDeploymentPostprocessing = 0; 104 | }; 105 | 52D6D9EC1BEFFFBE002C0205 /* Frameworks */ = { 106 | isa = PBXFrameworksBuildPhase; 107 | buildActionMask = 2147483647; 108 | files = ( 109 | ); 110 | runOnlyForDeploymentPostprocessing = 0; 111 | }; 112 | 52D6DA0B1BF000BD002C0205 /* Frameworks */ = { 113 | isa = PBXFrameworksBuildPhase; 114 | buildActionMask = 2147483647; 115 | files = ( 116 | ); 117 | runOnlyForDeploymentPostprocessing = 0; 118 | }; 119 | DD7502771C68FCFC006590AF /* Frameworks */ = { 120 | isa = PBXFrameworksBuildPhase; 121 | buildActionMask = 2147483647; 122 | files = ( 123 | DD7502881C68FEDE006590AF /* Tumbleweed.framework in Frameworks */, 124 | ); 125 | runOnlyForDeploymentPostprocessing = 0; 126 | }; 127 | DD75028A1C690C7A006590AF /* Frameworks */ = { 128 | isa = PBXFrameworksBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | DD7502921C690C7A006590AF /* Tumbleweed.framework in Frameworks */, 132 | ); 133 | runOnlyForDeploymentPostprocessing = 0; 134 | }; 135 | /* End PBXFrameworksBuildPhase section */ 136 | 137 | /* Begin PBXGroup section */ 138 | 52D6D9721BEFF229002C0205 = { 139 | isa = PBXGroup; 140 | children = ( 141 | D96042CB1E962B61002F4A75 /* Sources */, 142 | D96042CE1E962B85002F4A75 /* Tests */, 143 | 52D6D99C1BEFF38C002C0205 /* Configs */, 144 | 52D6D97D1BEFF229002C0205 /* Products */, 145 | ); 146 | sourceTree = ""; 147 | }; 148 | 52D6D97D1BEFF229002C0205 /* Products */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 52D6D97C1BEFF229002C0205 /* Tumbleweed.framework */, 152 | 52D6D9861BEFF229002C0205 /* Tumbleweed-iOS Tests.xctest */, 153 | 52D6D9E21BEFFF6E002C0205 /* Tumbleweed.framework */, 154 | 52D6D9F01BEFFFBE002C0205 /* Tumbleweed.framework */, 155 | 52D6DA0F1BF000BD002C0205 /* Tumbleweed.framework */, 156 | DD75027A1C68FCFC006590AF /* Tumbleweed-macOS Tests.xctest */, 157 | DD75028D1C690C7A006590AF /* Tumbleweed-tvOS Tests.xctest */, 158 | ); 159 | name = Products; 160 | sourceTree = ""; 161 | }; 162 | 52D6D99C1BEFF38C002C0205 /* Configs */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | DD7502721C68FC1B006590AF /* Frameworks */, 166 | DD7502731C68FC20006590AF /* Tests */, 167 | ); 168 | path = Configs; 169 | sourceTree = ""; 170 | }; 171 | D96042CB1E962B61002F4A75 /* Sources */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | D96042CC1E962B6E002F4A75 /* SessionMetrics.swift */, 175 | D985F9381E964EFC00E31C0B /* Metric.swift */, 176 | D96042D11E962C0B002F4A75 /* Renderer.swift */, 177 | ); 178 | name = Sources; 179 | sourceTree = ""; 180 | }; 181 | D96042CE1E962B85002F4A75 /* Tests */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | D96042CF1E962BC6002F4A75 /* SessionMetricsTests.swift */, 185 | D9A296FA1E97888D003638DE /* RenderTests.swift */, 186 | D985F9341E963AD000E31C0B /* MetricsTests.swift */, 187 | D9A296FB1E97888D003638DE /* TestPrinter.swift */, 188 | ); 189 | name = Tests; 190 | sourceTree = ""; 191 | }; 192 | DD7502721C68FC1B006590AF /* Frameworks */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | AD2FAA261CD0B6D800659CF4 /* Tumbleweed.plist */, 196 | ); 197 | name = Frameworks; 198 | sourceTree = ""; 199 | }; 200 | DD7502731C68FC20006590AF /* Tests */ = { 201 | isa = PBXGroup; 202 | children = ( 203 | AD2FAA281CD0B6E100659CF4 /* TumbleweedTests.plist */, 204 | ); 205 | name = Tests; 206 | sourceTree = ""; 207 | }; 208 | /* End PBXGroup section */ 209 | 210 | /* Begin PBXHeadersBuildPhase section */ 211 | 52D6D9791BEFF229002C0205 /* Headers */ = { 212 | isa = PBXHeadersBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | }; 218 | 52D6D9DF1BEFFF6E002C0205 /* Headers */ = { 219 | isa = PBXHeadersBuildPhase; 220 | buildActionMask = 2147483647; 221 | files = ( 222 | ); 223 | runOnlyForDeploymentPostprocessing = 0; 224 | }; 225 | 52D6D9ED1BEFFFBE002C0205 /* Headers */ = { 226 | isa = PBXHeadersBuildPhase; 227 | buildActionMask = 2147483647; 228 | files = ( 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | }; 232 | 52D6DA0C1BF000BD002C0205 /* Headers */ = { 233 | isa = PBXHeadersBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | }; 239 | /* End PBXHeadersBuildPhase section */ 240 | 241 | /* Begin PBXNativeTarget section */ 242 | 52D6D97B1BEFF229002C0205 /* Tumbleweed-iOS */ = { 243 | isa = PBXNativeTarget; 244 | buildConfigurationList = 52D6D9901BEFF229002C0205 /* Build configuration list for PBXNativeTarget "Tumbleweed-iOS" */; 245 | buildPhases = ( 246 | 52D6D9771BEFF229002C0205 /* Sources */, 247 | 52D6D9781BEFF229002C0205 /* Frameworks */, 248 | 52D6D9791BEFF229002C0205 /* Headers */, 249 | 52D6D97A1BEFF229002C0205 /* Resources */, 250 | ); 251 | buildRules = ( 252 | ); 253 | dependencies = ( 254 | ); 255 | name = "Tumbleweed-iOS"; 256 | productName = Tumbleweed; 257 | productReference = 52D6D97C1BEFF229002C0205 /* Tumbleweed.framework */; 258 | productType = "com.apple.product-type.framework"; 259 | }; 260 | 52D6D9851BEFF229002C0205 /* Tumbleweed-iOS Tests */ = { 261 | isa = PBXNativeTarget; 262 | buildConfigurationList = 52D6D9931BEFF229002C0205 /* Build configuration list for PBXNativeTarget "Tumbleweed-iOS Tests" */; 263 | buildPhases = ( 264 | 52D6D9821BEFF229002C0205 /* Sources */, 265 | 52D6D9831BEFF229002C0205 /* Frameworks */, 266 | 52D6D9841BEFF229002C0205 /* Resources */, 267 | ); 268 | buildRules = ( 269 | ); 270 | dependencies = ( 271 | 52D6D9891BEFF229002C0205 /* PBXTargetDependency */, 272 | ); 273 | name = "Tumbleweed-iOS Tests"; 274 | productName = TumbleweedTests; 275 | productReference = 52D6D9861BEFF229002C0205 /* Tumbleweed-iOS Tests.xctest */; 276 | productType = "com.apple.product-type.bundle.unit-test"; 277 | }; 278 | 52D6D9E11BEFFF6E002C0205 /* Tumbleweed-watchOS */ = { 279 | isa = PBXNativeTarget; 280 | buildConfigurationList = 52D6D9E71BEFFF6E002C0205 /* Build configuration list for PBXNativeTarget "Tumbleweed-watchOS" */; 281 | buildPhases = ( 282 | 52D6D9DD1BEFFF6E002C0205 /* Sources */, 283 | 52D6D9DE1BEFFF6E002C0205 /* Frameworks */, 284 | 52D6D9DF1BEFFF6E002C0205 /* Headers */, 285 | 52D6D9E01BEFFF6E002C0205 /* Resources */, 286 | ); 287 | buildRules = ( 288 | ); 289 | dependencies = ( 290 | ); 291 | name = "Tumbleweed-watchOS"; 292 | productName = "Tumbleweed-watchOS"; 293 | productReference = 52D6D9E21BEFFF6E002C0205 /* Tumbleweed.framework */; 294 | productType = "com.apple.product-type.framework"; 295 | }; 296 | 52D6D9EF1BEFFFBE002C0205 /* Tumbleweed-tvOS */ = { 297 | isa = PBXNativeTarget; 298 | buildConfigurationList = 52D6DA011BEFFFBE002C0205 /* Build configuration list for PBXNativeTarget "Tumbleweed-tvOS" */; 299 | buildPhases = ( 300 | 52D6D9EB1BEFFFBE002C0205 /* Sources */, 301 | 52D6D9EC1BEFFFBE002C0205 /* Frameworks */, 302 | 52D6D9ED1BEFFFBE002C0205 /* Headers */, 303 | 52D6D9EE1BEFFFBE002C0205 /* Resources */, 304 | ); 305 | buildRules = ( 306 | ); 307 | dependencies = ( 308 | ); 309 | name = "Tumbleweed-tvOS"; 310 | productName = "Tumbleweed-tvOS"; 311 | productReference = 52D6D9F01BEFFFBE002C0205 /* Tumbleweed.framework */; 312 | productType = "com.apple.product-type.framework"; 313 | }; 314 | 52D6DA0E1BF000BD002C0205 /* Tumbleweed-macOS */ = { 315 | isa = PBXNativeTarget; 316 | buildConfigurationList = 52D6DA201BF000BD002C0205 /* Build configuration list for PBXNativeTarget "Tumbleweed-macOS" */; 317 | buildPhases = ( 318 | 52D6DA0A1BF000BD002C0205 /* Sources */, 319 | 52D6DA0B1BF000BD002C0205 /* Frameworks */, 320 | 52D6DA0C1BF000BD002C0205 /* Headers */, 321 | 52D6DA0D1BF000BD002C0205 /* Resources */, 322 | ); 323 | buildRules = ( 324 | ); 325 | dependencies = ( 326 | ); 327 | name = "Tumbleweed-macOS"; 328 | productName = "Tumbleweed-macOS"; 329 | productReference = 52D6DA0F1BF000BD002C0205 /* Tumbleweed.framework */; 330 | productType = "com.apple.product-type.framework"; 331 | }; 332 | DD7502791C68FCFC006590AF /* Tumbleweed-macOS Tests */ = { 333 | isa = PBXNativeTarget; 334 | buildConfigurationList = DD7502821C68FCFC006590AF /* Build configuration list for PBXNativeTarget "Tumbleweed-macOS Tests" */; 335 | buildPhases = ( 336 | DD7502761C68FCFC006590AF /* Sources */, 337 | DD7502771C68FCFC006590AF /* Frameworks */, 338 | DD7502781C68FCFC006590AF /* Resources */, 339 | ); 340 | buildRules = ( 341 | ); 342 | dependencies = ( 343 | DD7502811C68FCFC006590AF /* PBXTargetDependency */, 344 | ); 345 | name = "Tumbleweed-macOS Tests"; 346 | productName = "Tumbleweed-OS Tests"; 347 | productReference = DD75027A1C68FCFC006590AF /* Tumbleweed-macOS Tests.xctest */; 348 | productType = "com.apple.product-type.bundle.unit-test"; 349 | }; 350 | DD75028C1C690C7A006590AF /* Tumbleweed-tvOS Tests */ = { 351 | isa = PBXNativeTarget; 352 | buildConfigurationList = DD7502951C690C7A006590AF /* Build configuration list for PBXNativeTarget "Tumbleweed-tvOS Tests" */; 353 | buildPhases = ( 354 | DD7502891C690C7A006590AF /* Sources */, 355 | DD75028A1C690C7A006590AF /* Frameworks */, 356 | DD75028B1C690C7A006590AF /* Resources */, 357 | ); 358 | buildRules = ( 359 | ); 360 | dependencies = ( 361 | DD7502941C690C7A006590AF /* PBXTargetDependency */, 362 | ); 363 | name = "Tumbleweed-tvOS Tests"; 364 | productName = "Tumbleweed-tvOS Tests"; 365 | productReference = DD75028D1C690C7A006590AF /* Tumbleweed-tvOS Tests.xctest */; 366 | productType = "com.apple.product-type.bundle.unit-test"; 367 | }; 368 | /* End PBXNativeTarget section */ 369 | 370 | /* Begin PBXProject section */ 371 | 52D6D9731BEFF229002C0205 /* Project object */ = { 372 | isa = PBXProject; 373 | attributes = { 374 | LastSwiftUpdateCheck = 0720; 375 | LastUpgradeCheck = 0810; 376 | ORGANIZATIONNAME = NRK; 377 | TargetAttributes = { 378 | 52D6D97B1BEFF229002C0205 = { 379 | CreatedOnToolsVersion = 7.1; 380 | LastSwiftMigration = 0830; 381 | }; 382 | 52D6D9851BEFF229002C0205 = { 383 | CreatedOnToolsVersion = 7.1; 384 | LastSwiftMigration = 0800; 385 | }; 386 | 52D6D9E11BEFFF6E002C0205 = { 387 | CreatedOnToolsVersion = 7.1; 388 | LastSwiftMigration = 0830; 389 | }; 390 | 52D6D9EF1BEFFFBE002C0205 = { 391 | CreatedOnToolsVersion = 7.1; 392 | LastSwiftMigration = 0830; 393 | }; 394 | 52D6DA0E1BF000BD002C0205 = { 395 | CreatedOnToolsVersion = 7.1; 396 | LastSwiftMigration = 0830; 397 | }; 398 | DD7502791C68FCFC006590AF = { 399 | CreatedOnToolsVersion = 7.2.1; 400 | LastSwiftMigration = 0800; 401 | }; 402 | DD75028C1C690C7A006590AF = { 403 | CreatedOnToolsVersion = 7.2.1; 404 | LastSwiftMigration = 0800; 405 | }; 406 | }; 407 | }; 408 | buildConfigurationList = 52D6D9761BEFF229002C0205 /* Build configuration list for PBXProject "Tumbleweed" */; 409 | compatibilityVersion = "Xcode 6.3"; 410 | developmentRegion = English; 411 | hasScannedForEncodings = 0; 412 | knownRegions = ( 413 | en, 414 | ); 415 | mainGroup = 52D6D9721BEFF229002C0205; 416 | productRefGroup = 52D6D97D1BEFF229002C0205 /* Products */; 417 | projectDirPath = ""; 418 | projectRoot = ""; 419 | targets = ( 420 | 52D6D97B1BEFF229002C0205 /* Tumbleweed-iOS */, 421 | 52D6DA0E1BF000BD002C0205 /* Tumbleweed-macOS */, 422 | 52D6D9E11BEFFF6E002C0205 /* Tumbleweed-watchOS */, 423 | 52D6D9EF1BEFFFBE002C0205 /* Tumbleweed-tvOS */, 424 | 52D6D9851BEFF229002C0205 /* Tumbleweed-iOS Tests */, 425 | DD7502791C68FCFC006590AF /* Tumbleweed-macOS Tests */, 426 | DD75028C1C690C7A006590AF /* Tumbleweed-tvOS Tests */, 427 | ); 428 | }; 429 | /* End PBXProject section */ 430 | 431 | /* Begin PBXResourcesBuildPhase section */ 432 | 52D6D97A1BEFF229002C0205 /* Resources */ = { 433 | isa = PBXResourcesBuildPhase; 434 | buildActionMask = 2147483647; 435 | files = ( 436 | ); 437 | runOnlyForDeploymentPostprocessing = 0; 438 | }; 439 | 52D6D9841BEFF229002C0205 /* Resources */ = { 440 | isa = PBXResourcesBuildPhase; 441 | buildActionMask = 2147483647; 442 | files = ( 443 | ); 444 | runOnlyForDeploymentPostprocessing = 0; 445 | }; 446 | 52D6D9E01BEFFF6E002C0205 /* Resources */ = { 447 | isa = PBXResourcesBuildPhase; 448 | buildActionMask = 2147483647; 449 | files = ( 450 | ); 451 | runOnlyForDeploymentPostprocessing = 0; 452 | }; 453 | 52D6D9EE1BEFFFBE002C0205 /* Resources */ = { 454 | isa = PBXResourcesBuildPhase; 455 | buildActionMask = 2147483647; 456 | files = ( 457 | ); 458 | runOnlyForDeploymentPostprocessing = 0; 459 | }; 460 | 52D6DA0D1BF000BD002C0205 /* Resources */ = { 461 | isa = PBXResourcesBuildPhase; 462 | buildActionMask = 2147483647; 463 | files = ( 464 | ); 465 | runOnlyForDeploymentPostprocessing = 0; 466 | }; 467 | DD7502781C68FCFC006590AF /* Resources */ = { 468 | isa = PBXResourcesBuildPhase; 469 | buildActionMask = 2147483647; 470 | files = ( 471 | ); 472 | runOnlyForDeploymentPostprocessing = 0; 473 | }; 474 | DD75028B1C690C7A006590AF /* Resources */ = { 475 | isa = PBXResourcesBuildPhase; 476 | buildActionMask = 2147483647; 477 | files = ( 478 | ); 479 | runOnlyForDeploymentPostprocessing = 0; 480 | }; 481 | /* End PBXResourcesBuildPhase section */ 482 | 483 | /* Begin PBXSourcesBuildPhase section */ 484 | 52D6D9771BEFF229002C0205 /* Sources */ = { 485 | isa = PBXSourcesBuildPhase; 486 | buildActionMask = 2147483647; 487 | files = ( 488 | D985F9391E964EFC00E31C0B /* Metric.swift in Sources */, 489 | D96042CD1E962B6E002F4A75 /* SessionMetrics.swift in Sources */, 490 | D96042D21E962C0B002F4A75 /* Renderer.swift in Sources */, 491 | ); 492 | runOnlyForDeploymentPostprocessing = 0; 493 | }; 494 | 52D6D9821BEFF229002C0205 /* Sources */ = { 495 | isa = PBXSourcesBuildPhase; 496 | buildActionMask = 2147483647; 497 | files = ( 498 | D9A296FF1E97888D003638DE /* TestPrinter.swift in Sources */, 499 | D985F9311E9635D300E31C0B /* SessionMetricsTests.swift in Sources */, 500 | D9A296FC1E97888D003638DE /* RenderTests.swift in Sources */, 501 | D985F9351E963AD000E31C0B /* MetricsTests.swift in Sources */, 502 | ); 503 | runOnlyForDeploymentPostprocessing = 0; 504 | }; 505 | 52D6D9DD1BEFFF6E002C0205 /* Sources */ = { 506 | isa = PBXSourcesBuildPhase; 507 | buildActionMask = 2147483647; 508 | files = ( 509 | D96042D41E962C0B002F4A75 /* Renderer.swift in Sources */, 510 | D985F93B1E964EFC00E31C0B /* Metric.swift in Sources */, 511 | D9A297031E97B4CA003638DE /* SessionMetrics.swift in Sources */, 512 | ); 513 | runOnlyForDeploymentPostprocessing = 0; 514 | }; 515 | 52D6D9EB1BEFFFBE002C0205 /* Sources */ = { 516 | isa = PBXSourcesBuildPhase; 517 | buildActionMask = 2147483647; 518 | files = ( 519 | D96042D51E962C0B002F4A75 /* Renderer.swift in Sources */, 520 | D985F93C1E964EFC00E31C0B /* Metric.swift in Sources */, 521 | D9A297041E97B4CB003638DE /* SessionMetrics.swift in Sources */, 522 | ); 523 | runOnlyForDeploymentPostprocessing = 0; 524 | }; 525 | 52D6DA0A1BF000BD002C0205 /* Sources */ = { 526 | isa = PBXSourcesBuildPhase; 527 | buildActionMask = 2147483647; 528 | files = ( 529 | D96042D31E962C0B002F4A75 /* Renderer.swift in Sources */, 530 | D985F93A1E964EFC00E31C0B /* Metric.swift in Sources */, 531 | D9A297021E97B4C9003638DE /* SessionMetrics.swift in Sources */, 532 | ); 533 | runOnlyForDeploymentPostprocessing = 0; 534 | }; 535 | DD7502761C68FCFC006590AF /* Sources */ = { 536 | isa = PBXSourcesBuildPhase; 537 | buildActionMask = 2147483647; 538 | files = ( 539 | D9A297001E97888D003638DE /* TestPrinter.swift in Sources */, 540 | D985F9321E9635D400E31C0B /* SessionMetricsTests.swift in Sources */, 541 | D9A296FD1E97888D003638DE /* RenderTests.swift in Sources */, 542 | D985F9361E963AD000E31C0B /* MetricsTests.swift in Sources */, 543 | ); 544 | runOnlyForDeploymentPostprocessing = 0; 545 | }; 546 | DD7502891C690C7A006590AF /* Sources */ = { 547 | isa = PBXSourcesBuildPhase; 548 | buildActionMask = 2147483647; 549 | files = ( 550 | D9A297011E97888D003638DE /* TestPrinter.swift in Sources */, 551 | D985F9331E9635D400E31C0B /* SessionMetricsTests.swift in Sources */, 552 | D9A296FE1E97888D003638DE /* RenderTests.swift in Sources */, 553 | D985F9371E963AD000E31C0B /* MetricsTests.swift in Sources */, 554 | ); 555 | runOnlyForDeploymentPostprocessing = 0; 556 | }; 557 | /* End PBXSourcesBuildPhase section */ 558 | 559 | /* Begin PBXTargetDependency section */ 560 | 52D6D9891BEFF229002C0205 /* PBXTargetDependency */ = { 561 | isa = PBXTargetDependency; 562 | target = 52D6D97B1BEFF229002C0205 /* Tumbleweed-iOS */; 563 | targetProxy = 52D6D9881BEFF229002C0205 /* PBXContainerItemProxy */; 564 | }; 565 | DD7502811C68FCFC006590AF /* PBXTargetDependency */ = { 566 | isa = PBXTargetDependency; 567 | target = 52D6DA0E1BF000BD002C0205 /* Tumbleweed-macOS */; 568 | targetProxy = DD7502801C68FCFC006590AF /* PBXContainerItemProxy */; 569 | }; 570 | DD7502941C690C7A006590AF /* PBXTargetDependency */ = { 571 | isa = PBXTargetDependency; 572 | target = 52D6D9EF1BEFFFBE002C0205 /* Tumbleweed-tvOS */; 573 | targetProxy = DD7502931C690C7A006590AF /* PBXContainerItemProxy */; 574 | }; 575 | /* End PBXTargetDependency section */ 576 | 577 | /* Begin XCBuildConfiguration section */ 578 | 52D6D98E1BEFF229002C0205 /* Debug */ = { 579 | isa = XCBuildConfiguration; 580 | buildSettings = { 581 | ALWAYS_SEARCH_USER_PATHS = NO; 582 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 583 | CLANG_CXX_LIBRARY = "libc++"; 584 | CLANG_ENABLE_MODULES = YES; 585 | CLANG_ENABLE_OBJC_ARC = YES; 586 | CLANG_WARN_BOOL_CONVERSION = YES; 587 | CLANG_WARN_CONSTANT_CONVERSION = YES; 588 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 589 | CLANG_WARN_EMPTY_BODY = YES; 590 | CLANG_WARN_ENUM_CONVERSION = YES; 591 | CLANG_WARN_INFINITE_RECURSION = YES; 592 | CLANG_WARN_INT_CONVERSION = YES; 593 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 594 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 595 | CLANG_WARN_UNREACHABLE_CODE = YES; 596 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 597 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 598 | COPY_PHASE_STRIP = NO; 599 | CURRENT_PROJECT_VERSION = 1; 600 | DEBUG_INFORMATION_FORMAT = dwarf; 601 | ENABLE_STRICT_OBJC_MSGSEND = YES; 602 | ENABLE_TESTABILITY = YES; 603 | GCC_C_LANGUAGE_STANDARD = gnu99; 604 | GCC_DYNAMIC_NO_PIC = NO; 605 | GCC_NO_COMMON_BLOCKS = YES; 606 | GCC_OPTIMIZATION_LEVEL = 0; 607 | GCC_PREPROCESSOR_DEFINITIONS = ( 608 | "DEBUG=1", 609 | "$(inherited)", 610 | ); 611 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 612 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 613 | GCC_WARN_UNDECLARED_SELECTOR = YES; 614 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 615 | GCC_WARN_UNUSED_FUNCTION = YES; 616 | GCC_WARN_UNUSED_VARIABLE = YES; 617 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 618 | MTL_ENABLE_DEBUG_INFO = YES; 619 | ONLY_ACTIVE_ARCH = YES; 620 | SDKROOT = iphoneos; 621 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 622 | SWIFT_VERSION = 3.0; 623 | TARGETED_DEVICE_FAMILY = "1,2"; 624 | VERSIONING_SYSTEM = "apple-generic"; 625 | VERSION_INFO_PREFIX = ""; 626 | }; 627 | name = Debug; 628 | }; 629 | 52D6D98F1BEFF229002C0205 /* Release */ = { 630 | isa = XCBuildConfiguration; 631 | buildSettings = { 632 | ALWAYS_SEARCH_USER_PATHS = NO; 633 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 634 | CLANG_CXX_LIBRARY = "libc++"; 635 | CLANG_ENABLE_MODULES = YES; 636 | CLANG_ENABLE_OBJC_ARC = YES; 637 | CLANG_WARN_BOOL_CONVERSION = YES; 638 | CLANG_WARN_CONSTANT_CONVERSION = YES; 639 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 640 | CLANG_WARN_EMPTY_BODY = YES; 641 | CLANG_WARN_ENUM_CONVERSION = YES; 642 | CLANG_WARN_INFINITE_RECURSION = YES; 643 | CLANG_WARN_INT_CONVERSION = YES; 644 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 645 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 646 | CLANG_WARN_UNREACHABLE_CODE = YES; 647 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 648 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 649 | COPY_PHASE_STRIP = NO; 650 | CURRENT_PROJECT_VERSION = 1; 651 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 652 | ENABLE_NS_ASSERTIONS = NO; 653 | ENABLE_STRICT_OBJC_MSGSEND = YES; 654 | GCC_C_LANGUAGE_STANDARD = gnu99; 655 | GCC_NO_COMMON_BLOCKS = YES; 656 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 657 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 658 | GCC_WARN_UNDECLARED_SELECTOR = YES; 659 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 660 | GCC_WARN_UNUSED_FUNCTION = YES; 661 | GCC_WARN_UNUSED_VARIABLE = YES; 662 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 663 | MTL_ENABLE_DEBUG_INFO = NO; 664 | SDKROOT = iphoneos; 665 | SWIFT_VERSION = 3.0; 666 | TARGETED_DEVICE_FAMILY = "1,2"; 667 | VALIDATE_PRODUCT = YES; 668 | VERSIONING_SYSTEM = "apple-generic"; 669 | VERSION_INFO_PREFIX = ""; 670 | }; 671 | name = Release; 672 | }; 673 | 52D6D9911BEFF229002C0205 /* Debug */ = { 674 | isa = XCBuildConfiguration; 675 | buildSettings = { 676 | APPLICATION_EXTENSION_API_ONLY = YES; 677 | CLANG_ENABLE_MODULES = YES; 678 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 679 | DEFINES_MODULE = YES; 680 | DYLIB_COMPATIBILITY_VERSION = 1; 681 | DYLIB_CURRENT_VERSION = 1; 682 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 683 | INFOPLIST_FILE = Configs/Tumbleweed.plist; 684 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 685 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 686 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 687 | ONLY_ACTIVE_ARCH = NO; 688 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-iOS"; 689 | PRODUCT_NAME = Tumbleweed; 690 | SKIP_INSTALL = YES; 691 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 692 | SWIFT_VERSION = 3.0; 693 | }; 694 | name = Debug; 695 | }; 696 | 52D6D9921BEFF229002C0205 /* Release */ = { 697 | isa = XCBuildConfiguration; 698 | buildSettings = { 699 | APPLICATION_EXTENSION_API_ONLY = YES; 700 | CLANG_ENABLE_MODULES = YES; 701 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 702 | DEFINES_MODULE = YES; 703 | DYLIB_COMPATIBILITY_VERSION = 1; 704 | DYLIB_CURRENT_VERSION = 1; 705 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 706 | INFOPLIST_FILE = Configs/Tumbleweed.plist; 707 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 708 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 709 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 710 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-iOS"; 711 | PRODUCT_NAME = Tumbleweed; 712 | SKIP_INSTALL = YES; 713 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 714 | SWIFT_VERSION = 3.0; 715 | }; 716 | name = Release; 717 | }; 718 | 52D6D9941BEFF229002C0205 /* Debug */ = { 719 | isa = XCBuildConfiguration; 720 | buildSettings = { 721 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 722 | CLANG_ENABLE_MODULES = YES; 723 | INFOPLIST_FILE = Configs/TumbleweedTests.plist; 724 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 725 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 726 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-iOS-Tests"; 727 | PRODUCT_NAME = "$(TARGET_NAME)"; 728 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 729 | SWIFT_VERSION = 3.0; 730 | WATCHOS_DEPLOYMENT_TARGET = 3.0; 731 | }; 732 | name = Debug; 733 | }; 734 | 52D6D9951BEFF229002C0205 /* Release */ = { 735 | isa = XCBuildConfiguration; 736 | buildSettings = { 737 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 738 | CLANG_ENABLE_MODULES = YES; 739 | INFOPLIST_FILE = Configs/TumbleweedTests.plist; 740 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 741 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 742 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-iOS-Tests"; 743 | PRODUCT_NAME = "$(TARGET_NAME)"; 744 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 745 | SWIFT_VERSION = 3.0; 746 | WATCHOS_DEPLOYMENT_TARGET = 3.0; 747 | }; 748 | name = Release; 749 | }; 750 | 52D6D9E81BEFFF6E002C0205 /* Debug */ = { 751 | isa = XCBuildConfiguration; 752 | buildSettings = { 753 | APPLICATION_EXTENSION_API_ONLY = YES; 754 | CLANG_ENABLE_MODULES = YES; 755 | "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; 756 | DEFINES_MODULE = YES; 757 | DYLIB_COMPATIBILITY_VERSION = 1; 758 | DYLIB_CURRENT_VERSION = 1; 759 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 760 | INFOPLIST_FILE = Configs/Tumbleweed.plist; 761 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 762 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 763 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-watchOS"; 764 | PRODUCT_NAME = Tumbleweed; 765 | SDKROOT = watchos; 766 | SKIP_INSTALL = YES; 767 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 768 | SWIFT_VERSION = 3.0; 769 | TARGETED_DEVICE_FAMILY = 4; 770 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 771 | }; 772 | name = Debug; 773 | }; 774 | 52D6D9E91BEFFF6E002C0205 /* Release */ = { 775 | isa = XCBuildConfiguration; 776 | buildSettings = { 777 | APPLICATION_EXTENSION_API_ONLY = YES; 778 | CLANG_ENABLE_MODULES = YES; 779 | "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; 780 | DEFINES_MODULE = YES; 781 | DYLIB_COMPATIBILITY_VERSION = 1; 782 | DYLIB_CURRENT_VERSION = 1; 783 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 784 | INFOPLIST_FILE = Configs/Tumbleweed.plist; 785 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 786 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 787 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-watchOS"; 788 | PRODUCT_NAME = Tumbleweed; 789 | SDKROOT = watchos; 790 | SKIP_INSTALL = YES; 791 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 792 | SWIFT_VERSION = 3.0; 793 | TARGETED_DEVICE_FAMILY = 4; 794 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 795 | }; 796 | name = Release; 797 | }; 798 | 52D6DA021BEFFFBE002C0205 /* Debug */ = { 799 | isa = XCBuildConfiguration; 800 | buildSettings = { 801 | APPLICATION_EXTENSION_API_ONLY = YES; 802 | CLANG_ENABLE_MODULES = YES; 803 | "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; 804 | DEFINES_MODULE = YES; 805 | DYLIB_COMPATIBILITY_VERSION = 1; 806 | DYLIB_CURRENT_VERSION = 1; 807 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 808 | INFOPLIST_FILE = Configs/Tumbleweed.plist; 809 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 810 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 811 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-tvOS"; 812 | PRODUCT_NAME = Tumbleweed; 813 | SDKROOT = appletvos; 814 | SKIP_INSTALL = YES; 815 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 816 | SWIFT_VERSION = 3.0; 817 | TARGETED_DEVICE_FAMILY = 3; 818 | TVOS_DEPLOYMENT_TARGET = 9.0; 819 | }; 820 | name = Debug; 821 | }; 822 | 52D6DA031BEFFFBE002C0205 /* Release */ = { 823 | isa = XCBuildConfiguration; 824 | buildSettings = { 825 | APPLICATION_EXTENSION_API_ONLY = YES; 826 | CLANG_ENABLE_MODULES = YES; 827 | "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; 828 | DEFINES_MODULE = YES; 829 | DYLIB_COMPATIBILITY_VERSION = 1; 830 | DYLIB_CURRENT_VERSION = 1; 831 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 832 | INFOPLIST_FILE = Configs/Tumbleweed.plist; 833 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 834 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 835 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-tvOS"; 836 | PRODUCT_NAME = Tumbleweed; 837 | SDKROOT = appletvos; 838 | SKIP_INSTALL = YES; 839 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 840 | SWIFT_VERSION = 3.0; 841 | TARGETED_DEVICE_FAMILY = 3; 842 | TVOS_DEPLOYMENT_TARGET = 9.0; 843 | }; 844 | name = Release; 845 | }; 846 | 52D6DA211BF000BD002C0205 /* Debug */ = { 847 | isa = XCBuildConfiguration; 848 | buildSettings = { 849 | APPLICATION_EXTENSION_API_ONLY = YES; 850 | CLANG_ENABLE_MODULES = YES; 851 | CODE_SIGN_IDENTITY = "-"; 852 | COMBINE_HIDPI_IMAGES = YES; 853 | DEFINES_MODULE = YES; 854 | DYLIB_COMPATIBILITY_VERSION = 1; 855 | DYLIB_CURRENT_VERSION = 1; 856 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 857 | FRAMEWORK_VERSION = A; 858 | INFOPLIST_FILE = Configs/Tumbleweed.plist; 859 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 860 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 861 | MACOSX_DEPLOYMENT_TARGET = 10.10; 862 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-macOS"; 863 | PRODUCT_NAME = Tumbleweed; 864 | SDKROOT = macosx; 865 | SKIP_INSTALL = YES; 866 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 867 | SWIFT_VERSION = 3.0; 868 | }; 869 | name = Debug; 870 | }; 871 | 52D6DA221BF000BD002C0205 /* Release */ = { 872 | isa = XCBuildConfiguration; 873 | buildSettings = { 874 | APPLICATION_EXTENSION_API_ONLY = YES; 875 | CLANG_ENABLE_MODULES = YES; 876 | CODE_SIGN_IDENTITY = "-"; 877 | COMBINE_HIDPI_IMAGES = YES; 878 | DEFINES_MODULE = YES; 879 | DYLIB_COMPATIBILITY_VERSION = 1; 880 | DYLIB_CURRENT_VERSION = 1; 881 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 882 | FRAMEWORK_VERSION = A; 883 | INFOPLIST_FILE = Configs/Tumbleweed.plist; 884 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 885 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 886 | MACOSX_DEPLOYMENT_TARGET = 10.10; 887 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-macOS"; 888 | PRODUCT_NAME = Tumbleweed; 889 | SDKROOT = macosx; 890 | SKIP_INSTALL = YES; 891 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 892 | SWIFT_VERSION = 3.0; 893 | }; 894 | name = Release; 895 | }; 896 | DD7502831C68FCFC006590AF /* Debug */ = { 897 | isa = XCBuildConfiguration; 898 | buildSettings = { 899 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 900 | CODE_SIGN_IDENTITY = "-"; 901 | COMBINE_HIDPI_IMAGES = YES; 902 | INFOPLIST_FILE = Configs/TumbleweedTests.plist; 903 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 904 | MACOSX_DEPLOYMENT_TARGET = 10.12; 905 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-macOS-Tests"; 906 | PRODUCT_NAME = "$(TARGET_NAME)"; 907 | SDKROOT = macosx; 908 | SWIFT_VERSION = 3.0; 909 | }; 910 | name = Debug; 911 | }; 912 | DD7502841C68FCFC006590AF /* Release */ = { 913 | isa = XCBuildConfiguration; 914 | buildSettings = { 915 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 916 | CODE_SIGN_IDENTITY = "-"; 917 | COMBINE_HIDPI_IMAGES = YES; 918 | INFOPLIST_FILE = Configs/TumbleweedTests.plist; 919 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 920 | MACOSX_DEPLOYMENT_TARGET = 10.12; 921 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-macOS-Tests"; 922 | PRODUCT_NAME = "$(TARGET_NAME)"; 923 | SDKROOT = macosx; 924 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 925 | SWIFT_VERSION = 3.0; 926 | }; 927 | name = Release; 928 | }; 929 | DD7502961C690C7A006590AF /* Debug */ = { 930 | isa = XCBuildConfiguration; 931 | buildSettings = { 932 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 933 | INFOPLIST_FILE = Configs/TumbleweedTests.plist; 934 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 935 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-tvOS-Tests"; 936 | PRODUCT_NAME = "$(TARGET_NAME)"; 937 | SDKROOT = appletvos; 938 | SWIFT_VERSION = 3.0; 939 | TVOS_DEPLOYMENT_TARGET = 10.0; 940 | }; 941 | name = Debug; 942 | }; 943 | DD7502971C690C7A006590AF /* Release */ = { 944 | isa = XCBuildConfiguration; 945 | buildSettings = { 946 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 947 | INFOPLIST_FILE = Configs/TumbleweedTests.plist; 948 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 949 | PRODUCT_BUNDLE_IDENTIFIER = "com.Tumbleweed.Tumbleweed-tvOS-Tests"; 950 | PRODUCT_NAME = "$(TARGET_NAME)"; 951 | SDKROOT = appletvos; 952 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 953 | SWIFT_VERSION = 3.0; 954 | TVOS_DEPLOYMENT_TARGET = 10.0; 955 | }; 956 | name = Release; 957 | }; 958 | /* End XCBuildConfiguration section */ 959 | 960 | /* Begin XCConfigurationList section */ 961 | 52D6D9761BEFF229002C0205 /* Build configuration list for PBXProject "Tumbleweed" */ = { 962 | isa = XCConfigurationList; 963 | buildConfigurations = ( 964 | 52D6D98E1BEFF229002C0205 /* Debug */, 965 | 52D6D98F1BEFF229002C0205 /* Release */, 966 | ); 967 | defaultConfigurationIsVisible = 0; 968 | defaultConfigurationName = Release; 969 | }; 970 | 52D6D9901BEFF229002C0205 /* Build configuration list for PBXNativeTarget "Tumbleweed-iOS" */ = { 971 | isa = XCConfigurationList; 972 | buildConfigurations = ( 973 | 52D6D9911BEFF229002C0205 /* Debug */, 974 | 52D6D9921BEFF229002C0205 /* Release */, 975 | ); 976 | defaultConfigurationIsVisible = 0; 977 | defaultConfigurationName = Release; 978 | }; 979 | 52D6D9931BEFF229002C0205 /* Build configuration list for PBXNativeTarget "Tumbleweed-iOS Tests" */ = { 980 | isa = XCConfigurationList; 981 | buildConfigurations = ( 982 | 52D6D9941BEFF229002C0205 /* Debug */, 983 | 52D6D9951BEFF229002C0205 /* Release */, 984 | ); 985 | defaultConfigurationIsVisible = 0; 986 | defaultConfigurationName = Release; 987 | }; 988 | 52D6D9E71BEFFF6E002C0205 /* Build configuration list for PBXNativeTarget "Tumbleweed-watchOS" */ = { 989 | isa = XCConfigurationList; 990 | buildConfigurations = ( 991 | 52D6D9E81BEFFF6E002C0205 /* Debug */, 992 | 52D6D9E91BEFFF6E002C0205 /* Release */, 993 | ); 994 | defaultConfigurationIsVisible = 0; 995 | defaultConfigurationName = Release; 996 | }; 997 | 52D6DA011BEFFFBE002C0205 /* Build configuration list for PBXNativeTarget "Tumbleweed-tvOS" */ = { 998 | isa = XCConfigurationList; 999 | buildConfigurations = ( 1000 | 52D6DA021BEFFFBE002C0205 /* Debug */, 1001 | 52D6DA031BEFFFBE002C0205 /* Release */, 1002 | ); 1003 | defaultConfigurationIsVisible = 0; 1004 | defaultConfigurationName = Release; 1005 | }; 1006 | 52D6DA201BF000BD002C0205 /* Build configuration list for PBXNativeTarget "Tumbleweed-macOS" */ = { 1007 | isa = XCConfigurationList; 1008 | buildConfigurations = ( 1009 | 52D6DA211BF000BD002C0205 /* Debug */, 1010 | 52D6DA221BF000BD002C0205 /* Release */, 1011 | ); 1012 | defaultConfigurationIsVisible = 0; 1013 | defaultConfigurationName = Release; 1014 | }; 1015 | DD7502821C68FCFC006590AF /* Build configuration list for PBXNativeTarget "Tumbleweed-macOS Tests" */ = { 1016 | isa = XCConfigurationList; 1017 | buildConfigurations = ( 1018 | DD7502831C68FCFC006590AF /* Debug */, 1019 | DD7502841C68FCFC006590AF /* Release */, 1020 | ); 1021 | defaultConfigurationIsVisible = 0; 1022 | defaultConfigurationName = Release; 1023 | }; 1024 | DD7502951C690C7A006590AF /* Build configuration list for PBXNativeTarget "Tumbleweed-tvOS Tests" */ = { 1025 | isa = XCConfigurationList; 1026 | buildConfigurations = ( 1027 | DD7502961C690C7A006590AF /* Debug */, 1028 | DD7502971C690C7A006590AF /* Release */, 1029 | ); 1030 | defaultConfigurationIsVisible = 0; 1031 | defaultConfigurationName = Release; 1032 | }; 1033 | /* End XCConfigurationList section */ 1034 | }; 1035 | rootObject = 52D6D9731BEFF229002C0205 /* Project object */; 1036 | } 1037 | --------------------------------------------------------------------------------