├── .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 |
--------------------------------------------------------------------------------