├── example.gif
├── Example
├── InteractiveLineGraphExample
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── AppDelegate.swift
│ ├── Info.plist
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── ExampleView.swift
│ ├── ExampleViewController.swift
│ ├── ExampleDetailCardView.swift
│ └── UIView+.swift
├── InteractiveLineGraphExample.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── project.pbxproj
└── InteractiveLineGraphExample.xcworkspace
│ ├── xcshareddata
│ └── IDEWorkspaceChecks.plist
│ └── contents.xcworkspacedata
├── InteractiveLineGraph.xcodeproj
├── InteractiveLineGraphExample.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── project.pbxproj
├── InteractiveLineGraph
├── Sources
│ ├── Extensions
│ │ ├── Collection+.swift
│ │ └── UIView+.swift
│ └── Graph
│ │ ├── Protocols
│ │ ├── DataProviders.swift
│ │ └── GraphViewInteractionDelegate.swift
│ │ ├── Accessory Layers
│ │ ├── GridLayer.swift
│ │ └── DotLayer.swift
│ │ ├── Interaction
│ │ ├── InteractionEchoView.swift
│ │ └── GraphInteractionView.swift
│ │ ├── Graph Layers
│ │ └── LineGraphLayer.swift
│ │ └── InteractiveLineGraphView.swift
└── Info.plist
├── ILG.podspec
├── LICENSE
├── .gitignore
└── README.md
/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeynelson42/ILG/HEAD/example.gif
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/InteractiveLineGraph.xcodeproj/InteractiveLineGraphExample.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/InteractiveLineGraph.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/InteractiveLineGraph.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/InteractiveLineGraph.xcodeproj/InteractiveLineGraphExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/InteractiveLineGraph/Sources/Extensions/Collection+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Collection+.swift
3 | //
4 |
5 | import Foundation
6 |
7 | extension Collection {
8 | /// Returns the element at the specified index iff it is within bounds, otherwise nil.
9 | subscript (safe index: Index) -> Element? {
10 | return indices.contains(index) ? self[index] : nil
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/InteractiveLineGraph/Sources/Graph/Protocols/DataProviders.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GraphDataProvider.swift
3 | // InteractiveLineGraph
4 | //
5 | // Created by Joey Nelson on 1/23/19.
6 | //
7 |
8 | import UIKit
9 |
10 | // TODO: Spice these up/Format them correctly/Don't be so lazy.
11 |
12 | internal protocol LineGraphDataProvider:class {
13 | func position(forColumn column: Int) -> CGPoint
14 | func totalDataPoints() -> Int
15 | }
16 |
17 | internal protocol InteractionDataProvider:class {
18 | func position(nearest point: CGPoint) -> CGPoint
19 | func interactionDidBegin()
20 | func interactionDidEnd()
21 | }
22 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | //
4 |
5 | import UIKit
6 | import CoreData
7 |
8 | @UIApplicationMain
9 | class AppDelegate: UIResponder, UIApplicationDelegate {
10 |
11 | var window: UIWindow?
12 |
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | let initialViewController = ExampleViewController()
15 | window = UIWindow(frame: UIScreen.main.bounds)
16 | window?.makeKeyAndVisible()
17 | window?.rootViewController = initialViewController
18 | return true
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ILG.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 |
3 | s.name = "ILG"
4 |
5 | s.version = "1.0.0"
6 |
7 | s.summary = "A simplified line graph with Robinhood-esque interaction. Because there aren't enough iOS graph libraries on Github."
8 |
9 | s.platform = :ios, "10.0"
10 |
11 | s.homepage = "https://github.com/joeynelson42/ILG"
12 |
13 | s.license = { :type => "MIT", :file => "LICENSE" }
14 |
15 | s.author = { "Joey" => "joeynelson42@gmail.com" }
16 |
17 | s.source = { :git => "https://github.com/joeynelson42/ILG.git", :tag => "#{s.version}" }
18 |
19 | s.framework = "UIKit"
20 |
21 | s.source_files = "InteractiveLineGraph/Sources/**/*.{swift}"
22 |
23 | s.requires_arc = true
24 |
25 | s.swift_version = "4.2"
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/InteractiveLineGraph/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/InteractiveLineGraph/Sources/Graph/Protocols/GraphViewInteractionDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GraphViewInteractionDelegate.swift
3 | // InteractiveLineGraph
4 | //
5 | // Created by Joey Nelson on 1/23/19.
6 | //
7 |
8 | import UIKit
9 |
10 | public protocol GraphViewInteractionDelegate:class {
11 | func graphViewInteraction(userInputDidChange currentIndex: Int, graphView: InteractiveLineGraphView, detailCardView: UIView?)
12 |
13 | func graphViewInteraction(userInputDidBeginOn graphView: InteractiveLineGraphView, detailCardView: UIView?)
14 | func graphViewInteraction(userInputDidEndOn graphView: InteractiveLineGraphView, detailCardView: UIView?)
15 | }
16 |
17 | public extension GraphViewInteractionDelegate {
18 | func graphViewInteraction(userInputDidChange currentIndex: Int, graphView: InteractiveLineGraphView, detailCardView: UIView?) {}
19 | func graphViewInteraction(userInputDidBeginOn graphView: InteractiveLineGraphView, detailCardView: UIView?) {}
20 | func graphViewInteraction(userInputDidEndOn graphView: InteractiveLineGraphView, detailCardView: UIView?) {}
21 | }
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Joey Nelson
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 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIRequiredDeviceCapabilities
26 |
27 | armv7
28 |
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | # Package.pins
40 | # Package.resolved
41 | .build/
42 |
43 | # CocoaPods
44 | #
45 | # We recommend against adding the Pods directory to your .gitignore. However
46 | # you should judge for yourself, the pros and cons are mentioned at:
47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48 | #
49 | # Pods/
50 |
51 | # Carthage
52 | #
53 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
54 | # Carthage/Checkouts
55 |
56 | Carthage/Build
57 |
58 | # fastlane
59 | #
60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
61 | # screenshots whenever they are needed.
62 | # For more information about the recommended setup visit:
63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
64 |
65 | fastlane/report.xml
66 | fastlane/Preview.html
67 | fastlane/screenshots/**/*.png
68 | fastlane/test_output
69 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/InteractiveLineGraph/Sources/Graph/Accessory Layers/GridLayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GridLayer.swift
3 | // InteractiveLineGraph
4 | //
5 | // Created by Joey Nelson on 1/14/19.
6 | //
7 |
8 | import UIKit
9 |
10 | final class GridLayer: CAShapeLayer {
11 |
12 | var insets = UIEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0)
13 | var horizontalLines: Int = 0
14 | var verticalLines: Int = 0
15 |
16 | func drawGrid() {
17 |
18 | if horizontalLines + verticalLines <= 0 { return }
19 |
20 | let linePath = UIBezierPath()
21 | if horizontalLines > 0 {
22 | for i in 0...(horizontalLines - 1) {
23 |
24 | var y: CGFloat = frame.height / CGFloat((horizontalLines - 1)) * CGFloat(i) + insets.top
25 |
26 | if i == 0 {
27 | y += lineWidth
28 | } else if i == (horizontalLines - 1) {
29 | y -= lineWidth
30 | }
31 |
32 | let moveX: CGFloat = insets.left
33 | let addLineX: CGFloat = frame.width - insets.right
34 |
35 | linePath.move(to: CGPoint(x: moveX + lineWidth, y: y))
36 | linePath.addLine(to: CGPoint(x: addLineX - lineWidth, y: y))
37 | }
38 | }
39 |
40 | if verticalLines > 0 {
41 | for i in 0...(verticalLines - 1) {
42 | let x: CGFloat = frame.width / CGFloat((verticalLines - 1)) * CGFloat(i) + insets.left
43 | let moveY: CGFloat = insets.top
44 | let addLineY: CGFloat = insets.top + frame.height
45 |
46 | if i == verticalLines - 1 {
47 | linePath.move(to: CGPoint(x: x - lineWidth, y: moveY))
48 | linePath.addLine(to: CGPoint(x: x - lineWidth, y: addLineY))
49 | } else {
50 | linePath.move(to: CGPoint(x: x + lineWidth, y: moveY))
51 | linePath.addLine(to: CGPoint(x: x + lineWidth, y: addLineY))
52 | }
53 | }
54 | }
55 |
56 | path = linePath.cgPath
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample/ExampleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleView.swift
3 | // InteractiveLineGraphExample
4 | //
5 | // Created by Joey Nelson on 1/23/19.
6 | //
7 |
8 | import UIKit
9 | import InteractiveLineGraph
10 |
11 | class ExampleView: UIView {
12 |
13 | // MARK: - Subviews
14 | let graphView = InteractiveLineGraphView()
15 |
16 | let graphDetailCard = ExampleDetailCardView()
17 |
18 | // MARK: - Initialization
19 | convenience init() {
20 | self.init(frame: .zero)
21 | configureSubviews()
22 | configureTesting()
23 | configureLayout()
24 | }
25 |
26 | /// Set view/subviews appearances
27 | fileprivate func configureSubviews() {
28 | backgroundColor = .white
29 |
30 | graphView.lineWidth = 2
31 | graphView.lineColor = .cyan
32 | graphView.lineMinY = 0
33 | graphView.lineMaxY = 50
34 | graphView.gridEnabled = false
35 | graphView.dotsEnabled = true
36 | graphView.dotColor = .darkGray
37 | graphView.dotSize = 4
38 | graphView.interactionHighlightColor = .blue
39 | graphView.interactionHighlightAlpha = 0.25
40 | graphView.interactionDetailCard = graphDetailCard
41 | }
42 |
43 | /// Set AccessibilityIdentifiers for view/subviews
44 | fileprivate func configureTesting() {
45 | accessibilityIdentifier = "ExampleView"
46 | }
47 |
48 | /// Add subviews, set layoutMargins, initialize stored constraints, set layout priorities, activate constraints
49 | fileprivate func configureLayout() {
50 |
51 | addAutoLayoutSubview(graphView)
52 |
53 | // Activate NSLayoutAnchors within this closure
54 | NSLayoutConstraint.activate([
55 | graphView.topAnchor.constraint(equalTo: safeTopAnchor, constant: 60),
56 | graphView.centerXAnchor.constraint(equalTo: centerXAnchor),
57 | graphView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 24),
58 | graphView.heightAnchor.constraint(equalToConstant: 250)
59 | ])
60 | }
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample/ExampleViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleViewController.swift
3 | // InteractiveLineGraphExample
4 | //
5 | // Created by Joey Nelson on 1/23/19.
6 | //
7 |
8 | import UIKit
9 | import InteractiveLineGraph
10 |
11 | class ExampleViewController: UIViewController {
12 |
13 | // MARK: - Properties
14 | private var points = [Double]()
15 |
16 | // MARK: - View
17 | let baseView = ExampleView()
18 |
19 | // MARK: - Life Cycle
20 | override func loadView() {
21 | super.loadView()
22 | view = baseView
23 | }
24 |
25 | override func viewDidLoad() {
26 | super.viewDidLoad()
27 |
28 | baseView.graphView.interactionDelegate = self
29 |
30 | baseView.addGestureRecognizer(UITapGestureRecognizer.init(target: self, action: #selector(handleTap)))
31 |
32 | baseView.layoutIfNeeded()
33 | points = generateRandomList()
34 | baseView.graphView.update(withDataPoints: generateRandomList(), animated: true)
35 | }
36 |
37 | @objc func handleTap() {
38 | points = generateRandomList()
39 | baseView.graphView.update(withDataPoints: generateRandomList(), animated: true)
40 | }
41 |
42 | fileprivate func generateRandomList() -> [Double] {
43 | var list = [Double]()
44 | for _ in 0 ..< Int.random(in: 12...20) {
45 | list.append(Double.random(in: 0...50))
46 | }
47 | return list
48 | }
49 | }
50 |
51 | extension ExampleViewController: GraphViewInteractionDelegate {
52 | func graphViewInteraction(userInputDidChange currentIndex: Int, graphView: InteractiveLineGraphView, detailCardView: UIView?) {
53 | baseView.graphDetailCard.textLabel.text = "Index #\(currentIndex)"
54 | }
55 |
56 | func graphViewInteraction(userInputDidBeginOn graphView: InteractiveLineGraphView, detailCardView: UIView?) {
57 | print("Began interaction")
58 | }
59 |
60 | func graphViewInteraction(userInputDidEndOn graphView: InteractiveLineGraphView, detailCardView: UIView?) {
61 | print("Ended interaction")
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/InteractiveLineGraph/Sources/Graph/Accessory Layers/DotLayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DotLayer.swift
3 | // InteractiveLineGraph
4 | //
5 | // Created by Joey Nelson on 1/14/19.
6 | //
7 |
8 | import UIKit
9 |
10 | final class DotLayer: CAShapeLayer {
11 |
12 | weak var dataProvider: LineGraphDataProvider!
13 | private var dotLayers = [CAShapeLayer]()
14 |
15 | // TODO: This needs some work.
16 | func update() {
17 | guard let _ = dataProvider else { return }
18 |
19 | var updatedDotLayers = [CAShapeLayer]()
20 | for index in 0 ..< dataProvider.totalDataPoints() {
21 |
22 | let circlePath = createCirclePath(forPoint: dataProvider.position(forColumn: index))
23 |
24 | if let existingDot = dotLayers[safe: index] {
25 | let animation = CABasicAnimation(keyPath: "path")
26 | animation.fromValue = existingDot.path
27 | animation.toValue = circlePath.cgPath
28 | animation.duration = 0.4
29 | animation.fillMode = CAMediaTimingFillMode.forwards
30 | animation.isRemovedOnCompletion = false
31 | animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
32 | existingDot.add(animation, forKey: "path")
33 | existingDot.fillColor = strokeColor
34 | existingDot.path = circlePath.cgPath
35 | updatedDotLayers.append(existingDot)
36 | } else {
37 | let newDot = CAShapeLayer()
38 | newDot.path = circlePath.cgPath
39 | newDot.fillColor = strokeColor
40 | updatedDotLayers.append(newDot)
41 | addSublayer(newDot)
42 | }
43 | }
44 |
45 | let toRemove = dotLayers.filter { !updatedDotLayers.contains($0) }
46 | toRemove.forEach { $0.removeFromSuperlayer() }
47 | dotLayers = updatedDotLayers
48 | }
49 |
50 | private func createCirclePath(forPoint point: CGPoint) -> UIBezierPath {
51 | var adjustedPoint = point
52 | adjustedPoint.x -= lineWidth/2
53 | adjustedPoint.y -= lineWidth/2
54 | return UIBezierPath(ovalIn: CGRect(origin: adjustedPoint, size: CGSize(width: lineWidth, height: lineWidth)))
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample/ExampleDetailCardView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleDetailswift
3 | // InteractiveLineGraphExample
4 | //
5 | // Created by Joey Nelson on 1/28/19.
6 | //
7 |
8 | import UIKit
9 |
10 | class ExampleDetailCardView: UIView {
11 |
12 | // MARK: - Properties
13 | private let circleSize: CGFloat = 12
14 |
15 | // MARK: - Subviews
16 | let textLabel = UILabel()
17 | let circleView = UIView()
18 |
19 | // MARK: - Initialization
20 | convenience init() {
21 | self.init(frame: .zero)
22 | configureSubviews()
23 | configureTesting()
24 | configureLayout()
25 | }
26 |
27 | /// Set view/subviews appearances
28 | fileprivate func configureSubviews() {
29 | backgroundColor = .white
30 | layer.cornerRadius = 3
31 | layer.shadowColor = UIColor.black.cgColor
32 | layer.shadowRadius = 3
33 | layer.shadowOpacity = 0.25
34 | layer.shadowOffset = CGSize(width: -1, height: 1)
35 |
36 | textLabel.font = UIFont.init(name: "Avenir", size: 14)
37 | textLabel.textColor = .darkGray
38 | textLabel.text = "TEST"
39 |
40 | circleView.layer.cornerRadius = circleSize / 2
41 | circleView.backgroundColor = .black
42 | }
43 |
44 | /// Set AccessibilityIdentifiers for view/subviews
45 | fileprivate func configureTesting() {
46 | accessibilityIdentifier = "ExampleDetailCardView"
47 | }
48 |
49 | /// Add subviews, set layoutMargins, initialize stored constraints, set layout priorities, activate constraints
50 | fileprivate func configureLayout() {
51 |
52 | addAutoLayoutSubview(textLabel)
53 | addAutoLayoutSubview(circleView)
54 |
55 | // Activate NSLayoutAnchors within this closure
56 | NSLayoutConstraint.activate([
57 | heightAnchor.constraint(equalTo: circleView.heightAnchor, multiplier: 2.25),
58 |
59 | textLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
60 | textLabel.leftAnchor.constraint(equalTo: circleView.rightAnchor, constant: 8),
61 | textLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -8),
62 |
63 | circleView.centerYAnchor.constraint(equalTo: centerYAnchor),
64 | circleView.leftAnchor.constraint(equalTo: leftAnchor, constant: 8),
65 | circleView.widthAnchor.constraint(equalToConstant: circleSize),
66 | circleView.heightAnchor.constraint(equalToConstant: circleSize)
67 | ])
68 | }
69 | }
70 |
71 |
--------------------------------------------------------------------------------
/InteractiveLineGraph/Sources/Graph/Interaction/InteractionEchoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InteractionEchoView.swift
3 | // InteractiveLineGraph
4 | //
5 | // Created by Joey Nelson on 1/17/19.
6 | //
7 |
8 | import UIKit
9 |
10 | class InteractionEchoView: UIView {
11 |
12 | // MARK: - Properties
13 | fileprivate var animator: UIViewPropertyAnimator!
14 |
15 | var highlightColor: UIColor {
16 | get {
17 | return dot.backgroundColor ?? .clear
18 | }
19 |
20 | set {
21 | dot.backgroundColor = newValue
22 | echo.layer.borderColor = newValue.cgColor
23 | echo.backgroundColor = newValue
24 | }
25 | }
26 |
27 | // MARK: - Subviews
28 | fileprivate let dot = UIView()
29 | fileprivate var echo = UIView()
30 |
31 | // MARK: - Stored Constraints
32 | // (Store any constraints that might need to be changed or animated later)
33 | fileprivate var dotCenterY: NSLayoutConstraint!
34 |
35 | // MARK: - Initialization
36 |
37 | convenience init() {
38 | self.init(frame: .zero)
39 | configureSubviews()
40 | configureTesting()
41 | configureLayout()
42 | }
43 |
44 | /// Set view/subviews appearances
45 | fileprivate func configureSubviews() {
46 | clipsToBounds = false
47 |
48 | dot.layer.cornerRadius = 2.5
49 |
50 | echo.layer.cornerRadius = 2.5
51 | echo.layer.borderWidth = 1
52 |
53 | animator = UIViewPropertyAnimator.init(duration: 1, curve: .easeOut) {
54 | UIView.setAnimationRepeatCount(.infinity)
55 | self.echo.transform = CGAffineTransform.init(scaleX: 4, y: 4)
56 | self.echo.alpha = 0
57 | }
58 | }
59 |
60 | /// Set AccessibilityIdentifiers for view/subviews
61 | fileprivate func configureTesting() {
62 | accessibilityIdentifier = "InteractionLineView"
63 | }
64 |
65 | /// Add subviews, set layoutMargins, initialize stored constraints, set layout priorities, activate constraints
66 | fileprivate func configureLayout() {
67 |
68 | addAutoLayoutSubview(echo)
69 | addAutoLayoutSubview(dot)
70 |
71 | dotCenterY = dot.centerYAnchor.constraint(equalTo: topAnchor)
72 |
73 | // Activate NSLayoutAnchors within this closure
74 | NSLayoutConstraint.activate([
75 | dot.centerXAnchor.constraint(equalTo: centerXAnchor),
76 | dot.widthAnchor.constraint(equalToConstant: 5),
77 | dot.heightAnchor.constraint(equalToConstant: 5),
78 | dotCenterY,
79 |
80 | echo.centerXAnchor.constraint(equalTo: dot.centerXAnchor),
81 | echo.centerYAnchor.constraint(equalTo: dot.centerYAnchor),
82 | echo.widthAnchor.constraint(equalToConstant: 5),
83 | echo.heightAnchor.constraint(equalToConstant: 5),
84 | ])
85 | }
86 |
87 | public func setDotPosition(_ yPos: CGFloat) {
88 | dotCenterY.constant = yPos
89 | layoutIfNeeded()
90 |
91 | if !animator.isRunning {
92 | animator.startAnimation()
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ILG (InteractiveLineGraph) 📈
2 | > I was tasked with building something akin to Robinhood's graph and while there are several thousand iOS chart frameworks I decided it would be more fun to roll my own and less tedious than modifying someone else's.
3 | > I tried my best to keep it simple, while still allowing for enough customization for it to remain potentially useful for others.
4 |
5 |
6 | 
7 |
8 | ## Disclaimer
9 | I built this for work and while it meets my job's requirements, many areas haven't been fleshed out. I like to think that I'll get back to it but I am very good at putting things on my get-back-to-it shelf.
10 |
11 | Things to be aware of (or fix/add if you're feeling communal):
12 | - Not sure where the grid is at, probably still works?
13 | - There was a gradient beneath the line at one point but it broke and I haven't bothered to fix it.
14 | - Line and dot animations leave something to be desired.
15 |
16 |
17 | Things I do plan on working on:
18 | - GraphViewInteractionDelegate could be fancier/a little more helpful.
19 | - Naming and other general housekeeping.
20 | - Documentation.
21 | - Testing!
22 |
23 |
24 | ## Requirements
25 |
26 | - Swift 4.2
27 | - iOS 10.0+
28 |
29 | ## Installation
30 |
31 | #### CocoaPods ☕️
32 | You can use [CocoaPods](http://cocoapods.org/) to install `ILG` by adding it to your `Podfile`:
33 |
34 | ```ruby
35 | pod 'ILG'
36 | ```
37 |
38 |
39 | ## Usage
40 |
41 | Don't forget to import `ILG`:
42 |
43 | ``` swift
44 | import ILG
45 | ```
46 |
47 | Create an instance of `InteractiveLineGraphView` and add it to your view hierarchy however you would like:
48 | ```swift
49 | let graphView = InteractiveLineGraphView()
50 | ```
51 |
52 | Then call `graphView.update(...)` and you're off to the races.
53 |
54 |
55 | ### Properties
56 | There are a number of public properties you'll find in `InteractiveLineGraphView.swift`, most of them are self-explanatory but here are a few that may not be:
57 |
58 | `lineMinY` and `lineMaxY` will force set the lower and upper y-axis limits, if nil then the `.min()` or `.max()` of your data will be used.
59 |
60 | `interactionDetailCard` is the floating card. It's entirely optional, simply assign it any UIView and it will do the rest. If you do use it and would like to update it be sure to keep a reference to your card so you can update it in the `GraphViewInteractionDelegate` callback (maybe in the future I'll have fancier protocols).
61 |
62 | ### Protocols
63 | `GraphViewInteractionDelegate` will relay all interaction information back to you. And when I say "all" I mean it will just tell you when the highlighted index has changed. Spicing it up a little wouldn't be hard, and I would like to in the future but for now it is what it is.
64 |
65 | ## Meta(l!!! 🎸🎸🎸)
66 |
67 | Joey Nelson – [@jedmondn](https://twitter.com/jedmondn) – joeyedmondnelson@gmail.com
68 |
69 | Distributed under the MIT license. See ``LICENSE`` for more information.
70 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample/UIView+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+.swift
3 | //
4 |
5 | import UIKit
6 |
7 | extension UIView {
8 |
9 | // MARK: - NSLayoutConstraint Convenience Methods
10 |
11 | func addAutoLayoutSubview(_ subview: UIView) {
12 | addSubview(subview)
13 | subview.translatesAutoresizingMaskIntoConstraints = false
14 | }
15 |
16 | func insertAutoLayoutSubview(_ view: UIView, belowSubview: UIView) {
17 | insertSubview(view, belowSubview: belowSubview)
18 | view.translatesAutoresizingMaskIntoConstraints = false
19 | }
20 |
21 | func insertAutoLayoutSubview(_ view: UIView, aboveSubview: UIView) {
22 | insertSubview(view, aboveSubview: aboveSubview)
23 | view.translatesAutoresizingMaskIntoConstraints = false
24 | }
25 |
26 | // MARK: - Layout
27 |
28 | func fillSuperview() {
29 | guard let superview = self.superview else { return }
30 | NSLayoutConstraint.activate([
31 | leftAnchor.constraint(equalTo: superview.leftAnchor),
32 | rightAnchor.constraint(equalTo: superview.rightAnchor),
33 | topAnchor.constraint(equalTo: superview.topAnchor),
34 | bottomAnchor.constraint(equalTo: superview.bottomAnchor)
35 | ])
36 | }
37 |
38 | func fillSuperviewLayoutMargins() {
39 | guard let superview = self.superview else { return }
40 | NSLayoutConstraint.activate([
41 | leftAnchor.constraint(equalTo: superview.leftMargin),
42 | rightAnchor.constraint(equalTo: superview.rightMargin),
43 | topAnchor.constraint(equalTo: superview.topMargin),
44 | bottomAnchor.constraint(equalTo: superview.bottomMargin)
45 | ])
46 | }
47 |
48 | func centerInSuperView() {
49 | guard let superview = self.superview else { return }
50 | NSLayoutConstraint.activate([
51 | centerXAnchor.constraint(equalTo: superview.centerXAnchor),
52 | centerYAnchor.constraint(equalTo: superview.centerYAnchor)
53 | ])
54 | }
55 |
56 | // MARK: - Layout Shortcuts
57 | func rightToRight(constant: CGFloat = 0) {
58 | guard let superview = self.superview else { return }
59 | rightAnchor.constraint(equalTo: superview.rightAnchor, constant: constant).isActive = true
60 | }
61 |
62 | // MARK: - Iphone X Constraints
63 | var safeTopAnchor: NSLayoutYAxisAnchor {
64 | if #available(iOS 11, *) {
65 | return safeAreaLayoutGuide.topAnchor
66 | } else {
67 | return topAnchor
68 | }
69 | }
70 |
71 | var safeLeftAnchor: NSLayoutXAxisAnchor {
72 | if #available(iOS 11, *) {
73 | return safeAreaLayoutGuide.leftAnchor
74 | } else {
75 | return leftAnchor
76 | }
77 | }
78 |
79 | var safeBottomAnchor: NSLayoutYAxisAnchor {
80 | if #available(iOS 11, *) {
81 | return safeAreaLayoutGuide.bottomAnchor
82 | } else {
83 | return bottomAnchor
84 | }
85 | }
86 |
87 | var safeRightAnchor: NSLayoutXAxisAnchor {
88 | if #available(iOS 11, *) {
89 | return safeAreaLayoutGuide.rightAnchor
90 | } else {
91 | return rightAnchor
92 | }
93 | }
94 |
95 | // MARK: - Layout Margins Guide Shortcut
96 |
97 | var leftMargin: NSLayoutXAxisAnchor {
98 | return layoutMarginsGuide.leftAnchor
99 | }
100 |
101 | var rightMargin: NSLayoutXAxisAnchor {
102 | return layoutMarginsGuide.rightAnchor
103 | }
104 |
105 | var centerXMargin: NSLayoutXAxisAnchor {
106 | return layoutMarginsGuide.centerXAnchor
107 | }
108 |
109 | var widthMargin: NSLayoutDimension {
110 | return layoutMarginsGuide.widthAnchor
111 | }
112 |
113 | var topMargin: NSLayoutYAxisAnchor {
114 | return layoutMarginsGuide.topAnchor
115 | }
116 |
117 | var bottomMargin: NSLayoutYAxisAnchor {
118 | return layoutMarginsGuide.bottomAnchor
119 | }
120 |
121 | var centerYMargin: NSLayoutYAxisAnchor {
122 | return layoutMarginsGuide.centerYAnchor
123 | }
124 |
125 | var heightMargin: NSLayoutDimension {
126 | return layoutMarginsGuide.heightAnchor
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/InteractiveLineGraph/Sources/Extensions/UIView+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+.swift
3 | //
4 |
5 | import UIKit
6 |
7 | extension UIView {
8 |
9 | // MARK: - NSLayoutConstraint Convenience Methods
10 |
11 | func addAutoLayoutSubview(_ subview: UIView) {
12 | addSubview(subview)
13 | subview.translatesAutoresizingMaskIntoConstraints = false
14 | }
15 |
16 | func insertAutoLayoutSubview(_ view: UIView, belowSubview: UIView) {
17 | insertSubview(view, belowSubview: belowSubview)
18 | view.translatesAutoresizingMaskIntoConstraints = false
19 | }
20 |
21 | func insertAutoLayoutSubview(_ view: UIView, aboveSubview: UIView) {
22 | insertSubview(view, aboveSubview: aboveSubview)
23 | view.translatesAutoresizingMaskIntoConstraints = false
24 | }
25 |
26 | // MARK: - Layout
27 |
28 | func fillSuperview() {
29 | guard let superview = self.superview else { return }
30 | NSLayoutConstraint.activate([
31 | leftAnchor.constraint(equalTo: superview.leftAnchor),
32 | rightAnchor.constraint(equalTo: superview.rightAnchor),
33 | topAnchor.constraint(equalTo: superview.topAnchor),
34 | bottomAnchor.constraint(equalTo: superview.bottomAnchor)
35 | ])
36 | }
37 |
38 | func fillSuperviewLayoutMargins() {
39 | guard let superview = self.superview else { return }
40 | NSLayoutConstraint.activate([
41 | leftAnchor.constraint(equalTo: superview.leftMargin),
42 | rightAnchor.constraint(equalTo: superview.rightMargin),
43 | topAnchor.constraint(equalTo: superview.topMargin),
44 | bottomAnchor.constraint(equalTo: superview.bottomMargin)
45 | ])
46 | }
47 |
48 | func centerInSuperView() {
49 | guard let superview = self.superview else { return }
50 | NSLayoutConstraint.activate([
51 | centerXAnchor.constraint(equalTo: superview.centerXAnchor),
52 | centerYAnchor.constraint(equalTo: superview.centerYAnchor)
53 | ])
54 | }
55 |
56 | // MARK: - Layout Shortcuts
57 | func rightToRight(constant: CGFloat = 0) {
58 | guard let superview = self.superview else { return }
59 | rightAnchor.constraint(equalTo: superview.rightAnchor, constant: constant).isActive = true
60 | }
61 |
62 | // MARK: - Iphone X Constraints
63 | var safeTopAnchor: NSLayoutYAxisAnchor {
64 | if #available(iOS 11, *) {
65 | return safeAreaLayoutGuide.topAnchor
66 | } else {
67 | return topAnchor
68 | }
69 | }
70 |
71 | var safeLeftAnchor: NSLayoutXAxisAnchor {
72 | if #available(iOS 11, *) {
73 | return safeAreaLayoutGuide.leftAnchor
74 | } else {
75 | return leftAnchor
76 | }
77 | }
78 |
79 | var safeBottomAnchor: NSLayoutYAxisAnchor {
80 | if #available(iOS 11, *) {
81 | return safeAreaLayoutGuide.bottomAnchor
82 | } else {
83 | return bottomAnchor
84 | }
85 | }
86 |
87 | var safeRightAnchor: NSLayoutXAxisAnchor {
88 | if #available(iOS 11, *) {
89 | return safeAreaLayoutGuide.rightAnchor
90 | } else {
91 | return rightAnchor
92 | }
93 | }
94 |
95 | // MARK: - Layout Margins Guide Shortcut
96 |
97 | var leftMargin: NSLayoutXAxisAnchor {
98 | return layoutMarginsGuide.leftAnchor
99 | }
100 |
101 | var rightMargin: NSLayoutXAxisAnchor {
102 | return layoutMarginsGuide.rightAnchor
103 | }
104 |
105 | var centerXMargin: NSLayoutXAxisAnchor {
106 | return layoutMarginsGuide.centerXAnchor
107 | }
108 |
109 | var widthMargin: NSLayoutDimension {
110 | return layoutMarginsGuide.widthAnchor
111 | }
112 |
113 | var topMargin: NSLayoutYAxisAnchor {
114 | return layoutMarginsGuide.topAnchor
115 | }
116 |
117 | var bottomMargin: NSLayoutYAxisAnchor {
118 | return layoutMarginsGuide.bottomAnchor
119 | }
120 |
121 | var centerYMargin: NSLayoutYAxisAnchor {
122 | return layoutMarginsGuide.centerYAnchor
123 | }
124 |
125 | var heightMargin: NSLayoutDimension {
126 | return layoutMarginsGuide.heightAnchor
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/InteractiveLineGraph/Sources/Graph/Graph Layers/LineGraphLayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LineGraphLayer.swift
3 | // InteractiveLineGraph
4 | //
5 | // Created by Joey Nelson on 1/14/19.
6 | //
7 |
8 | import UIKit
9 |
10 | class LineGraphLayer: CAShapeLayer {
11 |
12 | weak var dataProvider: LineGraphDataProvider!
13 |
14 | var linePath: UIBezierPath!
15 | var fullPath: UIBezierPath!
16 | var contentInsets: UIEdgeInsets = UIEdgeInsets(top: 24.0, left: 0.0, bottom: 40.0, right: 44.0)
17 |
18 | // Layers
19 | private var graphLayer = CAShapeLayer()
20 | private var gradientLayer = CAGradientLayer()
21 | private var lineLayer = CAShapeLayer()
22 |
23 | override var strokeColor: CGColor? {
24 | get {
25 | return lineLayer.strokeColor
26 | }
27 |
28 | set {
29 | lineLayer.strokeColor = newValue
30 | }
31 | }
32 |
33 | override var lineWidth: CGFloat {
34 | get {
35 | return lineLayer.lineWidth
36 | }
37 |
38 | set {
39 | lineLayer.lineWidth = newValue
40 | }
41 | }
42 |
43 | override init() {
44 | super.init()
45 | addSublayer(graphLayer)
46 | gradientLayer.mask = graphLayer
47 | addSublayer(gradientLayer)
48 | addSublayer(lineLayer)
49 |
50 | lineLayer.strokeColor = UIColor.blue.cgColor
51 | lineLayer.fillColor = UIColor.clear.cgColor
52 | lineLayer.lineWidth = lineWidth
53 |
54 | graphLayer.fillColor = UIColor.blue.cgColor
55 |
56 | gradientLayer.colors = [UIColor.clear.cgColor]
57 | gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
58 | gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
59 | }
60 |
61 | required init?(coder aDecoder: NSCoder) {
62 | fatalError("init(coder:) has not been implemented")
63 | }
64 |
65 | /// Update the paths for both the line layer and the graph mask layer
66 | func updatePaths(animated: Bool = false) {
67 | guard let _ = dataProvider else {
68 | print("No GraphDataProvider providing.")
69 | return
70 | }
71 |
72 | // Capture current state for potential animation
73 | let previousLine = linePath ?? UIBezierPath()
74 | let previousFullPath = fullPath ?? UIBezierPath()
75 |
76 | updateLinePath()
77 | updateFullPath()
78 |
79 | if animated {
80 | let animation = CABasicAnimation(keyPath: "path")
81 | animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
82 | animation.fromValue = previousFullPath.cgPath
83 | animation.toValue = fullPath.cgPath
84 | animation.duration = 0.4
85 | animation.fillMode = CAMediaTimingFillMode.forwards
86 | animation.isRemovedOnCompletion = false
87 | animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
88 | graphLayer.add(animation, forKey: "path")
89 |
90 | let lineAnimation = CABasicAnimation(keyPath: "path")
91 | lineAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
92 | lineAnimation.fromValue = previousLine.cgPath
93 | lineAnimation.toValue = linePath.cgPath
94 | lineAnimation.duration = 0.4
95 | lineAnimation.fillMode = CAMediaTimingFillMode.forwards
96 | lineAnimation.isRemovedOnCompletion = false
97 | lineAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
98 | lineLayer.add(lineAnimation, forKey: "path")
99 | } else {
100 | displayIfNeeded()
101 | }
102 | }
103 |
104 | /// Updates the line path according the dataProvider's data
105 | private func updateLinePath() {
106 | linePath = UIBezierPath()
107 |
108 | for i in 0 ..< dataProvider.totalDataPoints() {
109 | let point = dataProvider.position(forColumn: i)
110 | if i == 0 {
111 | linePath.move(to: point)
112 | } else {
113 | linePath.addLine(to: point)
114 | }
115 | }
116 |
117 | lineLayer.path = linePath.cgPath
118 | }
119 |
120 | /// Takes the current linePath and creates a full polygon. Needed for the gradient.
121 | /// TODO: solve all gradient issues...
122 | private func updateFullPath() {
123 | // Clip to full polygon for fill. Connect to bottom right then bottom left, then close.
124 | let clippingPath = linePath.copy() as! UIBezierPath
125 | clippingPath.addLine(to: CGPoint(x: dataProvider.position(forColumn: dataProvider.totalDataPoints() - 1).x, y: bounds.height))
126 | clippingPath.addLine(to: CGPoint(x: dataProvider.position(forColumn: 0).x, y: bounds.height))
127 | clippingPath.close()
128 | fullPath = clippingPath
129 |
130 | graphLayer.path = clippingPath.cgPath
131 | gradientLayer.frame = bounds
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/InteractiveLineGraph/Sources/Graph/InteractiveLineGraphView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InteractiveLineGraphView.swift
3 | // InteractiveLineGraph
4 | //
5 | // Created by Joey Nelson on 1/14/19.
6 | //
7 |
8 | import UIKit
9 |
10 | open class InteractiveLineGraphView: UIView {
11 |
12 | // MARK: Public Properties
13 |
14 | // MARK: Interaction
15 | weak public var interactionDelegate: GraphViewInteractionDelegate?
16 |
17 | public var interactionHighlightColor: UIColor {
18 | get {
19 | return interactionView.highlightColor
20 | }
21 |
22 | set {
23 | interactionView.highlightColor = newValue
24 | }
25 | }
26 |
27 | public var interactionHighlightAlpha: CGFloat {
28 | get {
29 | return interactionView.highlightAlpha
30 | }
31 |
32 | set {
33 | interactionView.highlightAlpha = newValue
34 | }
35 | }
36 |
37 | public var interactionDetailCard: UIView? {
38 | get {
39 | return nil
40 | }
41 |
42 | set {
43 | guard let _ = newValue else { return }
44 | interactionView.set(newDetailCard: newValue!)
45 | }
46 | }
47 |
48 | // MARK: Line Attributes
49 | public var lineMinY: CGFloat?
50 | public var lineMaxY: CGFloat?
51 |
52 | public var lineColor: UIColor {
53 | get {
54 | if let color = graphLayer.strokeColor {
55 | return UIColor.init(cgColor: color)
56 | } else {
57 | return .white
58 | }
59 | }
60 |
61 | set {
62 | graphLayer.strokeColor = newValue.cgColor
63 | }
64 | }
65 |
66 | public var lineWidth: CGFloat {
67 | get {
68 | return graphLayer.lineWidth
69 | }
70 |
71 | set {
72 | graphLayer.lineWidth = newValue
73 | }
74 | }
75 |
76 | // MARK: Grid Attributes
77 | public var gridEnabled = true
78 |
79 | public var gridColor: UIColor {
80 | get {
81 | if let color = gridLayer.strokeColor {
82 | return UIColor.init(cgColor: color)
83 | } else {
84 | return .white
85 | }
86 | }
87 |
88 | set {
89 | gridLayer.strokeColor = newValue.withAlphaComponent(gridAlpha).cgColor
90 | }
91 | }
92 |
93 | public var gridAlpha: CGFloat {
94 | get {
95 | return gridLayer.strokeColor?.alpha ?? 1.0
96 | }
97 |
98 | set {
99 | gridLayer.strokeColor = gridColor.withAlphaComponent(newValue).cgColor
100 | }
101 | }
102 |
103 | public var gridLineWidth: CGFloat {
104 | get {
105 | return gridLayer.lineWidth
106 | }
107 |
108 | set {
109 | gridLayer.lineWidth = newValue
110 | }
111 | }
112 |
113 | public var horizontalLines: Int {
114 | get {
115 | return gridLayer.horizontalLines
116 | }
117 |
118 | set {
119 | gridLayer.horizontalLines = newValue
120 | }
121 | }
122 |
123 | public var verticalLines: Int {
124 | get {
125 | return gridLayer.verticalLines
126 | }
127 |
128 | set {
129 | gridLayer.verticalLines = newValue
130 | }
131 | }
132 |
133 | // MARK: Dot Attributes
134 | public var dotsEnabled = true
135 |
136 | public var dotColor: UIColor {
137 | get {
138 | if let color = dotsLayer.strokeColor {
139 | return UIColor.init(cgColor: color)
140 | } else {
141 | return .white
142 | }
143 | }
144 |
145 | set {
146 | dotsLayer.strokeColor = newValue.cgColor
147 | }
148 | }
149 |
150 | public var dotSize: CGFloat {
151 | get {
152 | return dotsLayer.lineWidth
153 | }
154 |
155 | set {
156 | dotsLayer.lineWidth = newValue
157 | }
158 | }
159 |
160 | // MARK: Private Properties
161 |
162 | // MARK: Data
163 | fileprivate var dataPoints = [Double]() {
164 | didSet {
165 | updateGraphPoints()
166 | }
167 | }
168 |
169 | fileprivate let graphPadding = UIEdgeInsets.init(top: 5, left: 5, bottom: 5, right: 5)
170 | fileprivate var graphPoints = [CGPoint]()
171 |
172 | // MARK: Sublayers/views
173 | fileprivate var graphLayer = LineGraphLayer()
174 | fileprivate var gridLayer = GridLayer()
175 | fileprivate var dotsLayer = DotLayer()
176 | fileprivate var interactionView = GraphInteractionView()
177 |
178 | // MARK: - Initializers
179 | convenience init() {
180 | self.init(frame: .zero)
181 | }
182 |
183 | required override public init(frame: CGRect) {
184 | super.init(frame: frame)
185 |
186 | layer.addSublayer(gridLayer)
187 |
188 | graphLayer.dataProvider = self
189 | layer.addSublayer(graphLayer)
190 |
191 | dotsLayer.dataProvider = self
192 | layer.addSublayer(dotsLayer)
193 |
194 | interactionView.dataProvider = self
195 | addAutoLayoutSubview(interactionView)
196 | interactionView.fillSuperview()
197 | }
198 |
199 | required public init?(coder aDecoder: NSCoder) {
200 | fatalError("init?(coder aDecoder: NSCoder) is not required")
201 | }
202 |
203 | open override func layoutSubviews() {
204 | super.layoutSubviews()
205 | updateGraphFrame()
206 | }
207 |
208 | // MARK: - Public Methods
209 | public func update(withDataPoints dataPoints: [Double], animated: Bool) {
210 | self.dataPoints = dataPoints
211 |
212 | graphLayer.updatePaths(animated: animated)
213 |
214 | if dotsEnabled {
215 | drawDots()
216 | }
217 |
218 | if gridEnabled {
219 | drawGrid()
220 | }
221 | }
222 | }
223 |
224 | // MARK: Private Methods
225 | extension InteractiveLineGraphView {
226 | fileprivate func updateGraphPoints() {
227 | graphPoints.removeAll()
228 | for (index,_) in dataPoints.enumerated() {
229 | graphPoints.append(CGPoint.init(x: columnXPoint(column: index), y: columnYPoint(column: index)))
230 | }
231 | }
232 |
233 | fileprivate func updateGraphFrame() {
234 | let insetFrameSize = CGSize.init(width: frame.size.width - (graphPadding.left * 2), height: frame.size.height - (graphPadding.left * 2))
235 | graphLayer.frame.size = insetFrameSize
236 | }
237 |
238 | fileprivate func drawGrid() {
239 | gridLayer.frame = CGRect.init(origin: .init(x: graphPadding.left, y: graphPadding.top), size: graphLayer.frame.size)
240 | gridLayer.drawGrid()
241 | }
242 |
243 | fileprivate func drawDots() {
244 | dotsLayer.frame = bounds
245 | dotsLayer.update()
246 | }
247 | }
248 |
249 | extension InteractiveLineGraphView: LineGraphDataProvider {
250 | func position(forColumn column: Int) -> CGPoint {
251 | return CGPoint.init(x: columnXPoint(column: column), y: columnYPoint(column: column))
252 | }
253 |
254 | func totalDataPoints() -> Int {
255 | return dataPoints.count
256 | }
257 |
258 | fileprivate func columnYPoint(column: Int) -> CGFloat {
259 | let minY = frame.height - graphPadding.bottom
260 |
261 | if dataPoints.isEmpty { return minY }
262 |
263 | let minDataPoint = CGFloat(dataPoints.min() ?? 0)
264 | let maxDataPoint = CGFloat(dataPoints.max() ?? 0)
265 |
266 | let minValue = lineMinY ?? minDataPoint
267 | let maxValue = lineMaxY ?? maxDataPoint
268 | let dataPoint = CGFloat(dataPoints[column])
269 |
270 | if minValue + maxValue <= 0 || (maxValue - minValue == 0) {
271 | return minY
272 | } else {
273 | // TODO: Think of clearer names, good to know my fading math skills are continuing to fail me
274 | let proportion = (dataPoint - minValue) / (maxValue - minValue)
275 | let proportionalHeight = proportion * graphLayer.frame.height
276 | let y = frame.height - (graphPadding.top) - proportionalHeight
277 | return y
278 | }
279 | }
280 |
281 | fileprivate func columnXPoint(column: Int) -> CGFloat {
282 | if dataPoints.count <= 1 {
283 | return graphPadding.left
284 | }
285 |
286 | let spacer = (bounds.width - (graphPadding.left + graphPadding.right)) / CGFloat((dataPoints.count - 1))
287 | var x = CGFloat(column) * spacer
288 | x += graphPadding.left
289 | return x
290 | }
291 | }
292 |
293 | extension InteractiveLineGraphView: InteractionDataProvider {
294 | func position(nearest point: CGPoint) -> CGPoint {
295 | guard let first = graphPoints.first else { return .zero }
296 | var nearest = first
297 | var index: Int = 0
298 | for (i,graphPoint) in graphPoints.enumerated() {
299 | let currentDiff = abs(point.x - nearest.x)
300 | let newDiff = abs(point.x - graphPoint.x)
301 | if newDiff < currentDiff {
302 | nearest = graphPoint
303 | index = i
304 | }
305 | }
306 |
307 | interactionDelegate?.graphViewInteraction(userInputDidChange: index, graphView: self, detailCardView: interactionDetailCard)
308 |
309 | return nearest
310 | }
311 |
312 | func interactionDidBegin() {
313 | interactionDelegate?.graphViewInteraction(userInputDidBeginOn: self, detailCardView: interactionDetailCard)
314 | }
315 |
316 | func interactionDidEnd() {
317 | interactionDelegate?.graphViewInteraction(userInputDidEndOn: self, detailCardView: interactionDetailCard)
318 | }
319 | }
320 |
--------------------------------------------------------------------------------
/InteractiveLineGraph/Sources/Graph/Interaction/GraphInteractionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GraphInteractionView.swift
3 | // InteractiveLineGraph
4 | //
5 | // Created by Joey Nelson on 1/17/19.
6 | //
7 |
8 | import UIKit
9 |
10 | class GraphInteractionView: UIView {
11 |
12 | // MARK: - Public Properties
13 | weak var dataProvider: InteractionDataProvider!
14 |
15 | var highlightColor: UIColor {
16 | get {
17 | return echo.highlightColor
18 | }
19 |
20 | set {
21 | echo.highlightColor = newValue.withAlphaComponent(highlightAlpha)
22 | ghostLine.backgroundColor = newValue.withAlphaComponent(0.25)
23 | }
24 | }
25 |
26 | var highlightAlpha: CGFloat {
27 | get {
28 | return echo.highlightColor.cgColor.alpha
29 | }
30 |
31 | set {
32 | echo.highlightColor = highlightColor.withAlphaComponent(newValue)
33 | }
34 | }
35 |
36 | // MARK: - Private Properties
37 | fileprivate var pan: UIPanGestureRecognizer!
38 | fileprivate var impactGenerator = UIImpactFeedbackGenerator.init(style: .light)
39 | fileprivate var lastPosition: CGPoint?
40 |
41 | fileprivate var currentCardPlacement: CardPlacement {
42 | guard let _ = cardRight, let _ = cardLeft else { return .right }
43 | if cardRight.isActive {
44 | return .left
45 | } else {
46 | return .right
47 | }
48 | }
49 |
50 | // MARK: - Subviews
51 | fileprivate let echo = InteractionEchoView()
52 | fileprivate var detailCard: UIView?
53 | fileprivate var guideLine = UIView()
54 | fileprivate let ghostLine = UIView()
55 |
56 | // MARK: - Stored Constraints
57 | // (Store any constraints that might need to be changed or animated later)
58 | fileprivate var echoLeft: NSLayoutConstraint!
59 | fileprivate var ghostLineLeft: NSLayoutConstraint!
60 | fileprivate var guideLineLeft: NSLayoutConstraint!
61 | fileprivate var cardLeft: NSLayoutConstraint!
62 | fileprivate var cardRight: NSLayoutConstraint!
63 | fileprivate var cardCenterY: NSLayoutConstraint!
64 |
65 | // MARK: - Initialization
66 |
67 | convenience init() {
68 | self.init(frame: .zero)
69 |
70 | configureSubviews()
71 | configureTesting()
72 | configureLayout()
73 |
74 | pan = UIPanGestureRecognizer.init(target: self, action: #selector(handlePan(gestureRecognizer:)))
75 | addGestureRecognizer(pan)
76 | }
77 |
78 | /// Set view/subviews appearances
79 | fileprivate func configureSubviews() {
80 | hide(animated: false)
81 | }
82 |
83 | /// Set AccessibilityIdentifiers for view/subviews
84 | fileprivate func configureTesting() {
85 | accessibilityIdentifier = "GraphInteractionView"
86 | }
87 |
88 | /// Add subviews, set layoutMargins, initialize stored constraints, set layout priorities, activate constraints
89 | fileprivate func configureLayout() {
90 |
91 | addAutoLayoutSubview(echo)
92 | addAutoLayoutSubview(ghostLine)
93 | addAutoLayoutSubview(guideLine)
94 |
95 | echoLeft = echo.centerXAnchor.constraint(equalTo: leftAnchor)
96 | ghostLineLeft = ghostLine.centerXAnchor.constraint(equalTo: leftAnchor)
97 | guideLineLeft = guideLine.centerXAnchor.constraint(equalTo: leftAnchor)
98 |
99 | NSLayoutConstraint.activate([
100 | echo.topAnchor.constraint(equalTo: topAnchor),
101 | echo.bottomAnchor.constraint(equalTo: bottomAnchor),
102 | echo.widthAnchor.constraint(equalToConstant: 1.5),
103 | echoLeft,
104 |
105 | guideLine.topAnchor.constraint(equalTo: topAnchor),
106 | guideLine.bottomAnchor.constraint(equalTo: bottomAnchor),
107 | guideLine.widthAnchor.constraint(equalToConstant: 1.5),
108 | guideLineLeft,
109 |
110 | ghostLine.topAnchor.constraint(equalTo: topAnchor),
111 | ghostLine.bottomAnchor.constraint(equalTo: bottomAnchor),
112 | ghostLine.widthAnchor.constraint(equalToConstant: 1.5),
113 | ghostLineLeft,
114 | ])
115 | }
116 |
117 | /// Reconfigure card if it exists.
118 | func set(newDetailCard: UIView) {
119 | if let card = detailCard {
120 | card.removeFromSuperview()
121 | }
122 |
123 | self.detailCard = newDetailCard
124 | hide(animated: false)
125 |
126 | addAutoLayoutSubview(detailCard!)
127 |
128 | cardLeft = detailCard!.leftAnchor.constraint(equalTo: guideLine.rightAnchor, constant: 12)
129 | cardRight = detailCard!.rightAnchor.constraint(equalTo: guideLine.leftAnchor, constant: -12)
130 | cardCenterY = detailCard!.centerYAnchor.constraint(equalTo: topAnchor)
131 |
132 | NSLayoutConstraint.activate([
133 | cardCenterY,
134 | cardLeft
135 | ])
136 | }
137 |
138 | /// Fade out. Animation optional.
139 | private func hide(animated: Bool) {
140 | let block = {
141 | self.echo.alpha = 0
142 | self.detailCard?.alpha = 0
143 | self.ghostLine.alpha = 0
144 | }
145 | if animated {
146 | UIView.animate(withDuration: 0.12, animations: block)
147 | } else {
148 | UIView.performWithoutAnimation(block)
149 | }
150 | }
151 |
152 | /// Fade in. Animation optional.
153 | private func show(animated: Bool) {
154 | let block = {
155 | self.echo.alpha = 1
156 | self.detailCard?.alpha = 1
157 | self.ghostLine.alpha = 1
158 | }
159 | if animated {
160 | UIView.animate(withDuration: 0.12, animations: block)
161 | } else {
162 | UIView.performWithoutAnimation(block)
163 | }
164 | }
165 |
166 | @objc func handlePan(gestureRecognizer: UIPanGestureRecognizer) {
167 | guard let _ = dataProvider else { return }
168 | let currentTouchPoint = gestureRecognizer.location(in: self.superview)
169 |
170 | switch gestureRecognizer.state {
171 | // On begin: Simply reveal the echo/card and trigger impact.
172 | case .began:
173 | show(animated: true)
174 | impactGenerator.impactOccurred()
175 |
176 | lastPosition = currentTouchPoint
177 | update(withTouchPoint: currentTouchPoint, animated: false)
178 |
179 | dataProvider.interactionDidBegin()
180 |
181 | // On changed: Find the touch location, get nearest dataPoint from dataProvider and move echo to it if needed.
182 | case .changed:
183 | let currentTouchPoint = gestureRecognizer.location(in: self.superview)
184 | update(withTouchPoint: currentTouchPoint, animated: true)
185 |
186 | // On end: Hide echo/card.
187 | case .cancelled, .ended, .failed:
188 | hide(animated: true)
189 | dataProvider.interactionDidEnd()
190 | default:
191 | return
192 | }
193 | }
194 |
195 | }
196 |
197 | // MARK: Update methods
198 | extension GraphInteractionView {
199 | fileprivate func update(withTouchPoint touchPoint: CGPoint, animated: Bool) {
200 | let dataPoint = dataProvider.position(nearest: touchPoint)
201 |
202 | if let last = lastPosition, last != dataPoint {
203 | impactGenerator.impactOccurred()
204 | updateEcho(withDataPoint: dataPoint)
205 | }
206 | lastPosition = dataPoint
207 |
208 | updateCard(withDataPoint: dataPoint)
209 | updateGhostLine(withTouchPoint: touchPoint)
210 |
211 | if animated {
212 | UIView.animate(withDuration: 0.25) {
213 | self.layoutIfNeeded()
214 | }
215 | } else {
216 | UIView.performWithoutAnimation(layoutIfNeeded)
217 | }
218 | }
219 |
220 | fileprivate func updateGhostLine(withTouchPoint point: CGPoint) {
221 | ghostLineLeft.constant = point.x
222 | }
223 |
224 | fileprivate func updateEcho(withDataPoint point: CGPoint) {
225 | echoLeft.constant = point.x
226 | echo.setDotPosition(point.y)
227 | UIView.performWithoutAnimation(layoutIfNeeded)
228 | }
229 |
230 | fileprivate func updateCard(withDataPoint point: CGPoint) {
231 | guard let _ = detailCard else { return }
232 | guideLineLeft.constant = point.x
233 | cardCenterY.constant = point.y
234 |
235 | if let newPlacement = recommendedCardPlacement() {
236 | set(cardPlacement: newPlacement, animated: true)
237 | }
238 | }
239 | }
240 |
241 | // MARK: Card Placement
242 |
243 | fileprivate enum CardPlacement {
244 | case right, left
245 |
246 | var opposite: CardPlacement {
247 | switch self {
248 | case .left:
249 | return .right
250 | case .right:
251 | return .left
252 | }
253 | }
254 | }
255 |
256 | extension GraphInteractionView {
257 | /// Checks if card's frame is outside frame, returns new placement if needed.
258 | ///
259 | /// - Returns: Recommended card placement if card is out of frame, otherwise nil.
260 | fileprivate func recommendedCardPlacement() -> CardPlacement? {
261 | guard let card = detailCard else { return nil }
262 |
263 | var extremity: CGPoint!
264 | switch currentCardPlacement {
265 | case .left:
266 | extremity = CGPoint.init(x: card.frame.origin.x, y: card.frame.maxY / 2)
267 | case .right:
268 | extremity = CGPoint.init(x: card.frame.maxX, y: card.frame.maxY / 2)
269 | }
270 |
271 | if frame.contains(extremity) {
272 | return nil
273 | } else {
274 | return currentCardPlacement.opposite
275 | }
276 | }
277 |
278 | /// Sets the card's placement, either to the right or left of the echo.
279 | fileprivate func set(cardPlacement: CardPlacement, animated: Bool) {
280 | guard let _ = cardRight, let _ = cardLeft else { return }
281 |
282 | switch cardPlacement {
283 | case .right:
284 | cardRight.isActive = false
285 | cardLeft.isActive = true
286 | case .left:
287 | cardLeft.isActive = false
288 | cardRight.isActive = true
289 | }
290 |
291 | if animated {
292 | UIView.animate(withDuration: 0.25) {
293 | self.layoutIfNeeded()
294 | }
295 | } else {
296 | UIView.performWithoutAnimation(layoutIfNeeded)
297 | }
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/Example/InteractiveLineGraphExample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | F106133F21FF6B50009CE30A /* ExampleDetailCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F106133E21FF6B50009CE30A /* ExampleDetailCardView.swift */; };
11 | F1E13C7021F8F9BC009FEB26 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C6F21F8F9BC009FEB26 /* AppDelegate.swift */; };
12 | F1E13C7821F8F9BC009FEB26 /* UIView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C7721F8F9BC009FEB26 /* UIView+.swift */; };
13 | F1E13C8221F8F9BD009FEB26 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F1E13C8121F8F9BD009FEB26 /* Assets.xcassets */; };
14 | F1E13C8521F8F9BD009FEB26 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F1E13C8321F8F9BD009FEB26 /* LaunchScreen.storyboard */; };
15 | F1E13C8D21F8F9FB009FEB26 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C8C21F8F9FB009FEB26 /* ExampleView.swift */; };
16 | F1E13C8F21F8FA05009FEB26 /* ExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C8E21F8FA05009FEB26 /* ExampleViewController.swift */; };
17 | F1E13C9821F8FB7E009FEB26 /* InteractiveLineGraph.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F1E13C9721F8FB7E009FEB26 /* InteractiveLineGraph.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
18 | F1E13C9A21F8FFA0009FEB26 /* InteractiveLineGraph.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = F1E13C9721F8FB7E009FEB26 /* InteractiveLineGraph.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
19 | /* End PBXBuildFile section */
20 |
21 | /* Begin PBXCopyFilesBuildPhase section */
22 | F1E13C9921F8FF7B009FEB26 /* CopyFiles */ = {
23 | isa = PBXCopyFilesBuildPhase;
24 | buildActionMask = 2147483647;
25 | dstPath = "";
26 | dstSubfolderSpec = 10;
27 | files = (
28 | F1E13C9A21F8FFA0009FEB26 /* InteractiveLineGraph.framework in CopyFiles */,
29 | );
30 | runOnlyForDeploymentPostprocessing = 0;
31 | };
32 | /* End PBXCopyFilesBuildPhase section */
33 |
34 | /* Begin PBXFileReference section */
35 | F106133E21FF6B50009CE30A /* ExampleDetailCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleDetailCardView.swift; sourceTree = ""; };
36 | F1E13C6B21F8F9BC009FEB26 /* InteractiveLineGraphExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InteractiveLineGraphExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
37 | F1E13C6F21F8F9BC009FEB26 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
38 | F1E13C7721F8F9BC009FEB26 /* UIView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+.swift"; sourceTree = ""; };
39 | F1E13C8121F8F9BD009FEB26 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
40 | F1E13C8421F8F9BD009FEB26 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
41 | F1E13C8621F8F9BD009FEB26 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
42 | F1E13C8C21F8F9FB009FEB26 /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; };
43 | F1E13C8E21F8FA05009FEB26 /* ExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleViewController.swift; sourceTree = ""; };
44 | F1E13C9721F8FB7E009FEB26 /* InteractiveLineGraph.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = InteractiveLineGraph.framework; sourceTree = BUILT_PRODUCTS_DIR; };
45 | /* End PBXFileReference section */
46 |
47 | /* Begin PBXFrameworksBuildPhase section */
48 | F1E13C6821F8F9BC009FEB26 /* Frameworks */ = {
49 | isa = PBXFrameworksBuildPhase;
50 | buildActionMask = 2147483647;
51 | files = (
52 | F1E13C9821F8FB7E009FEB26 /* InteractiveLineGraph.framework in Frameworks */,
53 | );
54 | runOnlyForDeploymentPostprocessing = 0;
55 | };
56 | /* End PBXFrameworksBuildPhase section */
57 |
58 | /* Begin PBXGroup section */
59 | F1E13C6221F8F9BC009FEB26 = {
60 | isa = PBXGroup;
61 | children = (
62 | F1E13C6D21F8F9BC009FEB26 /* InteractiveLineGraphExample */,
63 | F1E13C6C21F8F9BC009FEB26 /* Products */,
64 | F1E13C9621F8FB7E009FEB26 /* Frameworks */,
65 | );
66 | sourceTree = "";
67 | };
68 | F1E13C6C21F8F9BC009FEB26 /* Products */ = {
69 | isa = PBXGroup;
70 | children = (
71 | F1E13C6B21F8F9BC009FEB26 /* InteractiveLineGraphExample.app */,
72 | );
73 | name = Products;
74 | sourceTree = "";
75 | };
76 | F1E13C6D21F8F9BC009FEB26 /* InteractiveLineGraphExample */ = {
77 | isa = PBXGroup;
78 | children = (
79 | F1E13C8E21F8FA05009FEB26 /* ExampleViewController.swift */,
80 | F1E13C8C21F8F9FB009FEB26 /* ExampleView.swift */,
81 | F106133E21FF6B50009CE30A /* ExampleDetailCardView.swift */,
82 | F1E13C6E21F8F9BC009FEB26 /* App Files */,
83 | F1E13C7621F8F9BC009FEB26 /* Extensions */,
84 | );
85 | path = InteractiveLineGraphExample;
86 | sourceTree = "";
87 | };
88 | F1E13C6E21F8F9BC009FEB26 /* App Files */ = {
89 | isa = PBXGroup;
90 | children = (
91 | F1E13C6F21F8F9BC009FEB26 /* AppDelegate.swift */,
92 | F1E13C8121F8F9BD009FEB26 /* Assets.xcassets */,
93 | F1E13C8321F8F9BD009FEB26 /* LaunchScreen.storyboard */,
94 | F1E13C8621F8F9BD009FEB26 /* Info.plist */,
95 | );
96 | name = "App Files";
97 | sourceTree = "";
98 | };
99 | F1E13C7621F8F9BC009FEB26 /* Extensions */ = {
100 | isa = PBXGroup;
101 | children = (
102 | F1E13C7721F8F9BC009FEB26 /* UIView+.swift */,
103 | );
104 | name = Extensions;
105 | sourceTree = "";
106 | };
107 | F1E13C9621F8FB7E009FEB26 /* Frameworks */ = {
108 | isa = PBXGroup;
109 | children = (
110 | F1E13C9721F8FB7E009FEB26 /* InteractiveLineGraph.framework */,
111 | );
112 | name = Frameworks;
113 | sourceTree = "";
114 | };
115 | /* End PBXGroup section */
116 |
117 | /* Begin PBXNativeTarget section */
118 | F1E13C6A21F8F9BC009FEB26 /* InteractiveLineGraphExample */ = {
119 | isa = PBXNativeTarget;
120 | buildConfigurationList = F1E13C8921F8F9BD009FEB26 /* Build configuration list for PBXNativeTarget "InteractiveLineGraphExample" */;
121 | buildPhases = (
122 | F1E13C6721F8F9BC009FEB26 /* Sources */,
123 | F1E13C6821F8F9BC009FEB26 /* Frameworks */,
124 | F1E13C6921F8F9BC009FEB26 /* Resources */,
125 | F1E13C9921F8FF7B009FEB26 /* CopyFiles */,
126 | );
127 | buildRules = (
128 | );
129 | dependencies = (
130 | );
131 | name = InteractiveLineGraphExample;
132 | productName = InteractiveLineGraphExample;
133 | productReference = F1E13C6B21F8F9BC009FEB26 /* InteractiveLineGraphExample.app */;
134 | productType = "com.apple.product-type.application";
135 | };
136 | /* End PBXNativeTarget section */
137 |
138 | /* Begin PBXProject section */
139 | F1E13C6321F8F9BC009FEB26 /* Project object */ = {
140 | isa = PBXProject;
141 | attributes = {
142 | LastSwiftUpdateCheck = 1010;
143 | LastUpgradeCheck = 1010;
144 | ORGANIZATIONNAME = nelsonje;
145 | TargetAttributes = {
146 | F1E13C6A21F8F9BC009FEB26 = {
147 | CreatedOnToolsVersion = 10.1;
148 | };
149 | };
150 | };
151 | buildConfigurationList = F1E13C6621F8F9BC009FEB26 /* Build configuration list for PBXProject "InteractiveLineGraphExample" */;
152 | compatibilityVersion = "Xcode 9.3";
153 | developmentRegion = en;
154 | hasScannedForEncodings = 0;
155 | knownRegions = (
156 | en,
157 | Base,
158 | );
159 | mainGroup = F1E13C6221F8F9BC009FEB26;
160 | productRefGroup = F1E13C6C21F8F9BC009FEB26 /* Products */;
161 | projectDirPath = "";
162 | projectRoot = "";
163 | targets = (
164 | F1E13C6A21F8F9BC009FEB26 /* InteractiveLineGraphExample */,
165 | );
166 | };
167 | /* End PBXProject section */
168 |
169 | /* Begin PBXResourcesBuildPhase section */
170 | F1E13C6921F8F9BC009FEB26 /* Resources */ = {
171 | isa = PBXResourcesBuildPhase;
172 | buildActionMask = 2147483647;
173 | files = (
174 | F1E13C8221F8F9BD009FEB26 /* Assets.xcassets in Resources */,
175 | F1E13C8521F8F9BD009FEB26 /* LaunchScreen.storyboard in Resources */,
176 | );
177 | runOnlyForDeploymentPostprocessing = 0;
178 | };
179 | /* End PBXResourcesBuildPhase section */
180 |
181 | /* Begin PBXSourcesBuildPhase section */
182 | F1E13C6721F8F9BC009FEB26 /* Sources */ = {
183 | isa = PBXSourcesBuildPhase;
184 | buildActionMask = 2147483647;
185 | files = (
186 | F1E13C7821F8F9BC009FEB26 /* UIView+.swift in Sources */,
187 | F106133F21FF6B50009CE30A /* ExampleDetailCardView.swift in Sources */,
188 | F1E13C8F21F8FA05009FEB26 /* ExampleViewController.swift in Sources */,
189 | F1E13C7021F8F9BC009FEB26 /* AppDelegate.swift in Sources */,
190 | F1E13C8D21F8F9FB009FEB26 /* ExampleView.swift in Sources */,
191 | );
192 | runOnlyForDeploymentPostprocessing = 0;
193 | };
194 | /* End PBXSourcesBuildPhase section */
195 |
196 | /* Begin PBXVariantGroup section */
197 | F1E13C8321F8F9BD009FEB26 /* LaunchScreen.storyboard */ = {
198 | isa = PBXVariantGroup;
199 | children = (
200 | F1E13C8421F8F9BD009FEB26 /* Base */,
201 | );
202 | name = LaunchScreen.storyboard;
203 | sourceTree = "";
204 | };
205 | /* End PBXVariantGroup section */
206 |
207 | /* Begin XCBuildConfiguration section */
208 | F1E13C8721F8F9BD009FEB26 /* Debug */ = {
209 | isa = XCBuildConfiguration;
210 | buildSettings = {
211 | ALWAYS_SEARCH_USER_PATHS = NO;
212 | CLANG_ANALYZER_NONNULL = YES;
213 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
214 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
215 | CLANG_CXX_LIBRARY = "libc++";
216 | CLANG_ENABLE_MODULES = YES;
217 | CLANG_ENABLE_OBJC_ARC = YES;
218 | CLANG_ENABLE_OBJC_WEAK = YES;
219 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
220 | CLANG_WARN_BOOL_CONVERSION = YES;
221 | CLANG_WARN_COMMA = YES;
222 | CLANG_WARN_CONSTANT_CONVERSION = YES;
223 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
224 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
225 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
226 | CLANG_WARN_EMPTY_BODY = YES;
227 | CLANG_WARN_ENUM_CONVERSION = YES;
228 | CLANG_WARN_INFINITE_RECURSION = YES;
229 | CLANG_WARN_INT_CONVERSION = YES;
230 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
231 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
232 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
233 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
234 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
235 | CLANG_WARN_STRICT_PROTOTYPES = YES;
236 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
237 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
238 | CLANG_WARN_UNREACHABLE_CODE = YES;
239 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
240 | CODE_SIGN_IDENTITY = "iPhone Developer";
241 | COPY_PHASE_STRIP = NO;
242 | DEBUG_INFORMATION_FORMAT = dwarf;
243 | ENABLE_STRICT_OBJC_MSGSEND = YES;
244 | ENABLE_TESTABILITY = YES;
245 | GCC_C_LANGUAGE_STANDARD = gnu11;
246 | GCC_DYNAMIC_NO_PIC = NO;
247 | GCC_NO_COMMON_BLOCKS = YES;
248 | GCC_OPTIMIZATION_LEVEL = 0;
249 | GCC_PREPROCESSOR_DEFINITIONS = (
250 | "DEBUG=1",
251 | "$(inherited)",
252 | );
253 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
254 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
255 | GCC_WARN_UNDECLARED_SELECTOR = YES;
256 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
257 | GCC_WARN_UNUSED_FUNCTION = YES;
258 | GCC_WARN_UNUSED_VARIABLE = YES;
259 | IPHONEOS_DEPLOYMENT_TARGET = 12.1;
260 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
261 | MTL_FAST_MATH = YES;
262 | ONLY_ACTIVE_ARCH = YES;
263 | SDKROOT = iphoneos;
264 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
265 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
266 | };
267 | name = Debug;
268 | };
269 | F1E13C8821F8F9BD009FEB26 /* Release */ = {
270 | isa = XCBuildConfiguration;
271 | buildSettings = {
272 | ALWAYS_SEARCH_USER_PATHS = NO;
273 | CLANG_ANALYZER_NONNULL = YES;
274 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
275 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
276 | CLANG_CXX_LIBRARY = "libc++";
277 | CLANG_ENABLE_MODULES = YES;
278 | CLANG_ENABLE_OBJC_ARC = YES;
279 | CLANG_ENABLE_OBJC_WEAK = YES;
280 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
281 | CLANG_WARN_BOOL_CONVERSION = YES;
282 | CLANG_WARN_COMMA = YES;
283 | CLANG_WARN_CONSTANT_CONVERSION = YES;
284 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
286 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
287 | CLANG_WARN_EMPTY_BODY = YES;
288 | CLANG_WARN_ENUM_CONVERSION = YES;
289 | CLANG_WARN_INFINITE_RECURSION = YES;
290 | CLANG_WARN_INT_CONVERSION = YES;
291 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
292 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
293 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
294 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
295 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
296 | CLANG_WARN_STRICT_PROTOTYPES = YES;
297 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
298 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
299 | CLANG_WARN_UNREACHABLE_CODE = YES;
300 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
301 | CODE_SIGN_IDENTITY = "iPhone Developer";
302 | COPY_PHASE_STRIP = NO;
303 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
304 | ENABLE_NS_ASSERTIONS = NO;
305 | ENABLE_STRICT_OBJC_MSGSEND = YES;
306 | GCC_C_LANGUAGE_STANDARD = gnu11;
307 | GCC_NO_COMMON_BLOCKS = YES;
308 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
309 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
310 | GCC_WARN_UNDECLARED_SELECTOR = YES;
311 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
312 | GCC_WARN_UNUSED_FUNCTION = YES;
313 | GCC_WARN_UNUSED_VARIABLE = YES;
314 | IPHONEOS_DEPLOYMENT_TARGET = 12.1;
315 | MTL_ENABLE_DEBUG_INFO = NO;
316 | MTL_FAST_MATH = YES;
317 | SDKROOT = iphoneos;
318 | SWIFT_COMPILATION_MODE = wholemodule;
319 | SWIFT_OPTIMIZATION_LEVEL = "-O";
320 | VALIDATE_PRODUCT = YES;
321 | };
322 | name = Release;
323 | };
324 | F1E13C8A21F8F9BD009FEB26 /* Debug */ = {
325 | isa = XCBuildConfiguration;
326 | buildSettings = {
327 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
328 | CODE_SIGN_STYLE = Automatic;
329 | DEVELOPMENT_TEAM = 2RF92UCMKX;
330 | INFOPLIST_FILE = InteractiveLineGraphExample/Info.plist;
331 | LD_RUNPATH_SEARCH_PATHS = (
332 | "$(inherited)",
333 | "@executable_path/Frameworks",
334 | );
335 | PRODUCT_BUNDLE_IDENTIFIER = com.nelsonje.InteractiveLineGraphExample;
336 | PRODUCT_NAME = "$(TARGET_NAME)";
337 | SWIFT_VERSION = 4.2;
338 | TARGETED_DEVICE_FAMILY = "1,2";
339 | };
340 | name = Debug;
341 | };
342 | F1E13C8B21F8F9BD009FEB26 /* Release */ = {
343 | isa = XCBuildConfiguration;
344 | buildSettings = {
345 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
346 | CODE_SIGN_STYLE = Automatic;
347 | DEVELOPMENT_TEAM = 2RF92UCMKX;
348 | INFOPLIST_FILE = InteractiveLineGraphExample/Info.plist;
349 | LD_RUNPATH_SEARCH_PATHS = (
350 | "$(inherited)",
351 | "@executable_path/Frameworks",
352 | );
353 | PRODUCT_BUNDLE_IDENTIFIER = com.nelsonje.InteractiveLineGraphExample;
354 | PRODUCT_NAME = "$(TARGET_NAME)";
355 | SWIFT_VERSION = 4.2;
356 | TARGETED_DEVICE_FAMILY = "1,2";
357 | };
358 | name = Release;
359 | };
360 | /* End XCBuildConfiguration section */
361 |
362 | /* Begin XCConfigurationList section */
363 | F1E13C6621F8F9BC009FEB26 /* Build configuration list for PBXProject "InteractiveLineGraphExample" */ = {
364 | isa = XCConfigurationList;
365 | buildConfigurations = (
366 | F1E13C8721F8F9BD009FEB26 /* Debug */,
367 | F1E13C8821F8F9BD009FEB26 /* Release */,
368 | );
369 | defaultConfigurationIsVisible = 0;
370 | defaultConfigurationName = Release;
371 | };
372 | F1E13C8921F8F9BD009FEB26 /* Build configuration list for PBXNativeTarget "InteractiveLineGraphExample" */ = {
373 | isa = XCConfigurationList;
374 | buildConfigurations = (
375 | F1E13C8A21F8F9BD009FEB26 /* Debug */,
376 | F1E13C8B21F8F9BD009FEB26 /* Release */,
377 | );
378 | defaultConfigurationIsVisible = 0;
379 | defaultConfigurationName = Release;
380 | };
381 | /* End XCConfigurationList section */
382 | };
383 | rootObject = F1E13C6321F8F9BC009FEB26 /* Project object */;
384 | }
385 |
--------------------------------------------------------------------------------
/InteractiveLineGraph.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | F1E13C2621F8F5CD009FEB26 /* LineGraphLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C1C21F8F5CD009FEB26 /* LineGraphLayer.swift */; };
11 | F1E13C2721F8F5CD009FEB26 /* InteractiveLineGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C1D21F8F5CD009FEB26 /* InteractiveLineGraphView.swift */; };
12 | F1E13C2821F8F5CD009FEB26 /* GridLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C1F21F8F5CD009FEB26 /* GridLayer.swift */; };
13 | F1E13C2921F8F5CD009FEB26 /* DotLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C2021F8F5CD009FEB26 /* DotLayer.swift */; };
14 | F1E13C2A21F8F5CD009FEB26 /* InteractionEchoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C2221F8F5CD009FEB26 /* InteractionEchoView.swift */; };
15 | F1E13C2B21F8F5CD009FEB26 /* GraphInteractionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C2321F8F5CD009FEB26 /* GraphInteractionView.swift */; };
16 | F1E13C2C21F8F5CD009FEB26 /* DataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C2521F8F5CD009FEB26 /* DataProviders.swift */; };
17 | F1E13C3021F8F669009FEB26 /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C2E21F8F669009FEB26 /* Collection+.swift */; };
18 | F1E13C3121F8F669009FEB26 /* UIView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C2F21F8F669009FEB26 /* UIView+.swift */; };
19 | F1E13C3321F8F6DA009FEB26 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = F1E13C3221F8F6DA009FEB26 /* LICENSE */; };
20 | F1E13C9C21F90E79009FEB26 /* GraphViewInteractionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1E13C9B21F90E79009FEB26 /* GraphViewInteractionDelegate.swift */; };
21 | /* End PBXBuildFile section */
22 |
23 | /* Begin PBXFileReference section */
24 | F1E13C0C21F8F586009FEB26 /* InteractiveLineGraph.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = InteractiveLineGraph.framework; sourceTree = BUILT_PRODUCTS_DIR; };
25 | F1E13C1021F8F586009FEB26 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
26 | F1E13C1C21F8F5CD009FEB26 /* LineGraphLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LineGraphLayer.swift; sourceTree = ""; };
27 | F1E13C1D21F8F5CD009FEB26 /* InteractiveLineGraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InteractiveLineGraphView.swift; sourceTree = ""; };
28 | F1E13C1F21F8F5CD009FEB26 /* GridLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridLayer.swift; sourceTree = ""; };
29 | F1E13C2021F8F5CD009FEB26 /* DotLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotLayer.swift; sourceTree = ""; };
30 | F1E13C2221F8F5CD009FEB26 /* InteractionEchoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InteractionEchoView.swift; sourceTree = ""; };
31 | F1E13C2321F8F5CD009FEB26 /* GraphInteractionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphInteractionView.swift; sourceTree = ""; };
32 | F1E13C2521F8F5CD009FEB26 /* DataProviders.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataProviders.swift; sourceTree = ""; };
33 | F1E13C2E21F8F669009FEB26 /* Collection+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = ""; };
34 | F1E13C2F21F8F669009FEB26 /* UIView+.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+.swift"; sourceTree = ""; };
35 | F1E13C3221F8F6DA009FEB26 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; };
36 | F1E13C9B21F90E79009FEB26 /* GraphViewInteractionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphViewInteractionDelegate.swift; sourceTree = ""; };
37 | /* End PBXFileReference section */
38 |
39 | /* Begin PBXFrameworksBuildPhase section */
40 | F1E13C0921F8F586009FEB26 /* Frameworks */ = {
41 | isa = PBXFrameworksBuildPhase;
42 | buildActionMask = 2147483647;
43 | files = (
44 | );
45 | runOnlyForDeploymentPostprocessing = 0;
46 | };
47 | /* End PBXFrameworksBuildPhase section */
48 |
49 | /* Begin PBXGroup section */
50 | F1E13C0221F8F586009FEB26 = {
51 | isa = PBXGroup;
52 | children = (
53 | F1E13C3221F8F6DA009FEB26 /* LICENSE */,
54 | F1E13C0E21F8F586009FEB26 /* InteractiveLineGraph */,
55 | F1E13C0D21F8F586009FEB26 /* Products */,
56 | );
57 | sourceTree = "";
58 | };
59 | F1E13C0D21F8F586009FEB26 /* Products */ = {
60 | isa = PBXGroup;
61 | children = (
62 | F1E13C0C21F8F586009FEB26 /* InteractiveLineGraph.framework */,
63 | );
64 | name = Products;
65 | sourceTree = "";
66 | };
67 | F1E13C0E21F8F586009FEB26 /* InteractiveLineGraph */ = {
68 | isa = PBXGroup;
69 | children = (
70 | F1E13C1021F8F586009FEB26 /* Info.plist */,
71 | F1E13C1921F8F5CD009FEB26 /* Sources */,
72 | );
73 | path = InteractiveLineGraph;
74 | sourceTree = "";
75 | };
76 | F1E13C1921F8F5CD009FEB26 /* Sources */ = {
77 | isa = PBXGroup;
78 | children = (
79 | F1E13C1A21F8F5CD009FEB26 /* Graph */,
80 | F1E13C2D21F8F669009FEB26 /* Extensions */,
81 | );
82 | path = Sources;
83 | sourceTree = "";
84 | };
85 | F1E13C1A21F8F5CD009FEB26 /* Graph */ = {
86 | isa = PBXGroup;
87 | children = (
88 | F1E13C1D21F8F5CD009FEB26 /* InteractiveLineGraphView.swift */,
89 | F1E13C1B21F8F5CD009FEB26 /* Graph Layers */,
90 | F1E13C1E21F8F5CD009FEB26 /* Accessory Layers */,
91 | F1E13C2121F8F5CD009FEB26 /* Interaction */,
92 | F1E13C2421F8F5CD009FEB26 /* Protocols */,
93 | );
94 | path = Graph;
95 | sourceTree = "";
96 | };
97 | F1E13C1B21F8F5CD009FEB26 /* Graph Layers */ = {
98 | isa = PBXGroup;
99 | children = (
100 | F1E13C1C21F8F5CD009FEB26 /* LineGraphLayer.swift */,
101 | );
102 | path = "Graph Layers";
103 | sourceTree = "";
104 | };
105 | F1E13C1E21F8F5CD009FEB26 /* Accessory Layers */ = {
106 | isa = PBXGroup;
107 | children = (
108 | F1E13C1F21F8F5CD009FEB26 /* GridLayer.swift */,
109 | F1E13C2021F8F5CD009FEB26 /* DotLayer.swift */,
110 | );
111 | path = "Accessory Layers";
112 | sourceTree = "";
113 | };
114 | F1E13C2121F8F5CD009FEB26 /* Interaction */ = {
115 | isa = PBXGroup;
116 | children = (
117 | F1E13C2321F8F5CD009FEB26 /* GraphInteractionView.swift */,
118 | F1E13C2221F8F5CD009FEB26 /* InteractionEchoView.swift */,
119 | );
120 | path = Interaction;
121 | sourceTree = "";
122 | };
123 | F1E13C2421F8F5CD009FEB26 /* Protocols */ = {
124 | isa = PBXGroup;
125 | children = (
126 | F1E13C2521F8F5CD009FEB26 /* DataProviders.swift */,
127 | F1E13C9B21F90E79009FEB26 /* GraphViewInteractionDelegate.swift */,
128 | );
129 | path = Protocols;
130 | sourceTree = "";
131 | };
132 | F1E13C2D21F8F669009FEB26 /* Extensions */ = {
133 | isa = PBXGroup;
134 | children = (
135 | F1E13C2E21F8F669009FEB26 /* Collection+.swift */,
136 | F1E13C2F21F8F669009FEB26 /* UIView+.swift */,
137 | );
138 | path = Extensions;
139 | sourceTree = "";
140 | };
141 | /* End PBXGroup section */
142 |
143 | /* Begin PBXHeadersBuildPhase section */
144 | F1E13C0721F8F586009FEB26 /* Headers */ = {
145 | isa = PBXHeadersBuildPhase;
146 | buildActionMask = 2147483647;
147 | files = (
148 | );
149 | runOnlyForDeploymentPostprocessing = 0;
150 | };
151 | /* End PBXHeadersBuildPhase section */
152 |
153 | /* Begin PBXNativeTarget section */
154 | F1E13C0B21F8F586009FEB26 /* InteractiveLineGraph */ = {
155 | isa = PBXNativeTarget;
156 | buildConfigurationList = F1E13C1421F8F586009FEB26 /* Build configuration list for PBXNativeTarget "InteractiveLineGraph" */;
157 | buildPhases = (
158 | F1E13C0721F8F586009FEB26 /* Headers */,
159 | F1E13C0821F8F586009FEB26 /* Sources */,
160 | F1E13C0921F8F586009FEB26 /* Frameworks */,
161 | F1E13C0A21F8F586009FEB26 /* Resources */,
162 | );
163 | buildRules = (
164 | );
165 | dependencies = (
166 | );
167 | name = InteractiveLineGraph;
168 | productName = InteractiveLineGraph;
169 | productReference = F1E13C0C21F8F586009FEB26 /* InteractiveLineGraph.framework */;
170 | productType = "com.apple.product-type.framework";
171 | };
172 | /* End PBXNativeTarget section */
173 |
174 | /* Begin PBXProject section */
175 | F1E13C0321F8F586009FEB26 /* Project object */ = {
176 | isa = PBXProject;
177 | attributes = {
178 | LastUpgradeCheck = 1010;
179 | ORGANIZATIONNAME = nelsonje;
180 | TargetAttributes = {
181 | F1E13C0B21F8F586009FEB26 = {
182 | CreatedOnToolsVersion = 10.1;
183 | };
184 | };
185 | };
186 | buildConfigurationList = F1E13C0621F8F586009FEB26 /* Build configuration list for PBXProject "InteractiveLineGraph" */;
187 | compatibilityVersion = "Xcode 9.3";
188 | developmentRegion = en;
189 | hasScannedForEncodings = 0;
190 | knownRegions = (
191 | en,
192 | );
193 | mainGroup = F1E13C0221F8F586009FEB26;
194 | productRefGroup = F1E13C0D21F8F586009FEB26 /* Products */;
195 | projectDirPath = "";
196 | projectRoot = "";
197 | targets = (
198 | F1E13C0B21F8F586009FEB26 /* InteractiveLineGraph */,
199 | );
200 | };
201 | /* End PBXProject section */
202 |
203 | /* Begin PBXResourcesBuildPhase section */
204 | F1E13C0A21F8F586009FEB26 /* Resources */ = {
205 | isa = PBXResourcesBuildPhase;
206 | buildActionMask = 2147483647;
207 | files = (
208 | F1E13C3321F8F6DA009FEB26 /* LICENSE in Resources */,
209 | );
210 | runOnlyForDeploymentPostprocessing = 0;
211 | };
212 | /* End PBXResourcesBuildPhase section */
213 |
214 | /* Begin PBXSourcesBuildPhase section */
215 | F1E13C0821F8F586009FEB26 /* Sources */ = {
216 | isa = PBXSourcesBuildPhase;
217 | buildActionMask = 2147483647;
218 | files = (
219 | F1E13C2621F8F5CD009FEB26 /* LineGraphLayer.swift in Sources */,
220 | F1E13C9C21F90E79009FEB26 /* GraphViewInteractionDelegate.swift in Sources */,
221 | F1E13C2921F8F5CD009FEB26 /* DotLayer.swift in Sources */,
222 | F1E13C2B21F8F5CD009FEB26 /* GraphInteractionView.swift in Sources */,
223 | F1E13C3021F8F669009FEB26 /* Collection+.swift in Sources */,
224 | F1E13C2A21F8F5CD009FEB26 /* InteractionEchoView.swift in Sources */,
225 | F1E13C2821F8F5CD009FEB26 /* GridLayer.swift in Sources */,
226 | F1E13C3121F8F669009FEB26 /* UIView+.swift in Sources */,
227 | F1E13C2721F8F5CD009FEB26 /* InteractiveLineGraphView.swift in Sources */,
228 | F1E13C2C21F8F5CD009FEB26 /* DataProviders.swift in Sources */,
229 | );
230 | runOnlyForDeploymentPostprocessing = 0;
231 | };
232 | /* End PBXSourcesBuildPhase section */
233 |
234 | /* Begin XCBuildConfiguration section */
235 | F1E13C1221F8F586009FEB26 /* Debug */ = {
236 | isa = XCBuildConfiguration;
237 | buildSettings = {
238 | ALWAYS_SEARCH_USER_PATHS = NO;
239 | CLANG_ANALYZER_NONNULL = YES;
240 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
242 | CLANG_CXX_LIBRARY = "libc++";
243 | CLANG_ENABLE_MODULES = YES;
244 | CLANG_ENABLE_OBJC_ARC = YES;
245 | CLANG_ENABLE_OBJC_WEAK = YES;
246 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
247 | CLANG_WARN_BOOL_CONVERSION = YES;
248 | CLANG_WARN_COMMA = YES;
249 | CLANG_WARN_CONSTANT_CONVERSION = YES;
250 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
251 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
252 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
253 | CLANG_WARN_EMPTY_BODY = YES;
254 | CLANG_WARN_ENUM_CONVERSION = YES;
255 | CLANG_WARN_INFINITE_RECURSION = YES;
256 | CLANG_WARN_INT_CONVERSION = YES;
257 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
258 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
259 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
260 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
261 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
262 | CLANG_WARN_STRICT_PROTOTYPES = YES;
263 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
264 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
265 | CLANG_WARN_UNREACHABLE_CODE = YES;
266 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
267 | CODE_SIGN_IDENTITY = "iPhone Developer";
268 | COPY_PHASE_STRIP = NO;
269 | CURRENT_PROJECT_VERSION = 1;
270 | DEBUG_INFORMATION_FORMAT = dwarf;
271 | ENABLE_STRICT_OBJC_MSGSEND = YES;
272 | ENABLE_TESTABILITY = YES;
273 | GCC_C_LANGUAGE_STANDARD = gnu11;
274 | GCC_DYNAMIC_NO_PIC = NO;
275 | GCC_NO_COMMON_BLOCKS = YES;
276 | GCC_OPTIMIZATION_LEVEL = 0;
277 | GCC_PREPROCESSOR_DEFINITIONS = (
278 | "DEBUG=1",
279 | "$(inherited)",
280 | );
281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
283 | GCC_WARN_UNDECLARED_SELECTOR = YES;
284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
285 | GCC_WARN_UNUSED_FUNCTION = YES;
286 | GCC_WARN_UNUSED_VARIABLE = YES;
287 | IPHONEOS_DEPLOYMENT_TARGET = 12.1;
288 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
289 | MTL_FAST_MATH = YES;
290 | ONLY_ACTIVE_ARCH = YES;
291 | SDKROOT = iphoneos;
292 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
294 | VERSIONING_SYSTEM = "apple-generic";
295 | VERSION_INFO_PREFIX = "";
296 | };
297 | name = Debug;
298 | };
299 | F1E13C1321F8F586009FEB26 /* Release */ = {
300 | isa = XCBuildConfiguration;
301 | buildSettings = {
302 | ALWAYS_SEARCH_USER_PATHS = NO;
303 | CLANG_ANALYZER_NONNULL = YES;
304 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
305 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
306 | CLANG_CXX_LIBRARY = "libc++";
307 | CLANG_ENABLE_MODULES = YES;
308 | CLANG_ENABLE_OBJC_ARC = YES;
309 | CLANG_ENABLE_OBJC_WEAK = YES;
310 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
311 | CLANG_WARN_BOOL_CONVERSION = YES;
312 | CLANG_WARN_COMMA = YES;
313 | CLANG_WARN_CONSTANT_CONVERSION = YES;
314 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
315 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
316 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
317 | CLANG_WARN_EMPTY_BODY = YES;
318 | CLANG_WARN_ENUM_CONVERSION = YES;
319 | CLANG_WARN_INFINITE_RECURSION = YES;
320 | CLANG_WARN_INT_CONVERSION = YES;
321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
325 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
326 | CLANG_WARN_STRICT_PROTOTYPES = YES;
327 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
328 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
329 | CLANG_WARN_UNREACHABLE_CODE = YES;
330 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
331 | CODE_SIGN_IDENTITY = "iPhone Developer";
332 | COPY_PHASE_STRIP = NO;
333 | CURRENT_PROJECT_VERSION = 1;
334 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
335 | ENABLE_NS_ASSERTIONS = NO;
336 | ENABLE_STRICT_OBJC_MSGSEND = YES;
337 | GCC_C_LANGUAGE_STANDARD = gnu11;
338 | GCC_NO_COMMON_BLOCKS = YES;
339 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
340 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
341 | GCC_WARN_UNDECLARED_SELECTOR = YES;
342 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
343 | GCC_WARN_UNUSED_FUNCTION = YES;
344 | GCC_WARN_UNUSED_VARIABLE = YES;
345 | IPHONEOS_DEPLOYMENT_TARGET = 12.1;
346 | MTL_ENABLE_DEBUG_INFO = NO;
347 | MTL_FAST_MATH = YES;
348 | SDKROOT = iphoneos;
349 | SWIFT_COMPILATION_MODE = wholemodule;
350 | SWIFT_OPTIMIZATION_LEVEL = "-O";
351 | VALIDATE_PRODUCT = YES;
352 | VERSIONING_SYSTEM = "apple-generic";
353 | VERSION_INFO_PREFIX = "";
354 | };
355 | name = Release;
356 | };
357 | F1E13C1521F8F586009FEB26 /* Debug */ = {
358 | isa = XCBuildConfiguration;
359 | buildSettings = {
360 | CODE_SIGN_IDENTITY = "";
361 | CODE_SIGN_STYLE = Automatic;
362 | DEFINES_MODULE = YES;
363 | DEVELOPMENT_TEAM = 2RF92UCMKX;
364 | DYLIB_COMPATIBILITY_VERSION = 1;
365 | DYLIB_CURRENT_VERSION = 1;
366 | DYLIB_INSTALL_NAME_BASE = "@rpath";
367 | INFOPLIST_FILE = InteractiveLineGraph/Info.plist;
368 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
369 | LD_RUNPATH_SEARCH_PATHS = (
370 | "$(inherited)",
371 | "@executable_path/Frameworks",
372 | "@loader_path/Frameworks",
373 | );
374 | PRODUCT_BUNDLE_IDENTIFIER = com.nelsonje.InteractiveLineGraph;
375 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
376 | SKIP_INSTALL = YES;
377 | SWIFT_VERSION = 4.2;
378 | TARGETED_DEVICE_FAMILY = "1,2";
379 | };
380 | name = Debug;
381 | };
382 | F1E13C1621F8F586009FEB26 /* Release */ = {
383 | isa = XCBuildConfiguration;
384 | buildSettings = {
385 | CODE_SIGN_IDENTITY = "";
386 | CODE_SIGN_STYLE = Automatic;
387 | DEFINES_MODULE = YES;
388 | DEVELOPMENT_TEAM = 2RF92UCMKX;
389 | DYLIB_COMPATIBILITY_VERSION = 1;
390 | DYLIB_CURRENT_VERSION = 1;
391 | DYLIB_INSTALL_NAME_BASE = "@rpath";
392 | INFOPLIST_FILE = InteractiveLineGraph/Info.plist;
393 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
394 | LD_RUNPATH_SEARCH_PATHS = (
395 | "$(inherited)",
396 | "@executable_path/Frameworks",
397 | "@loader_path/Frameworks",
398 | );
399 | PRODUCT_BUNDLE_IDENTIFIER = com.nelsonje.InteractiveLineGraph;
400 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
401 | SKIP_INSTALL = YES;
402 | SWIFT_VERSION = 4.2;
403 | TARGETED_DEVICE_FAMILY = "1,2";
404 | };
405 | name = Release;
406 | };
407 | /* End XCBuildConfiguration section */
408 |
409 | /* Begin XCConfigurationList section */
410 | F1E13C0621F8F586009FEB26 /* Build configuration list for PBXProject "InteractiveLineGraph" */ = {
411 | isa = XCConfigurationList;
412 | buildConfigurations = (
413 | F1E13C1221F8F586009FEB26 /* Debug */,
414 | F1E13C1321F8F586009FEB26 /* Release */,
415 | );
416 | defaultConfigurationIsVisible = 0;
417 | defaultConfigurationName = Release;
418 | };
419 | F1E13C1421F8F586009FEB26 /* Build configuration list for PBXNativeTarget "InteractiveLineGraph" */ = {
420 | isa = XCConfigurationList;
421 | buildConfigurations = (
422 | F1E13C1521F8F586009FEB26 /* Debug */,
423 | F1E13C1621F8F586009FEB26 /* Release */,
424 | );
425 | defaultConfigurationIsVisible = 0;
426 | defaultConfigurationName = Release;
427 | };
428 | /* End XCConfigurationList section */
429 | };
430 | rootObject = F1E13C0321F8F586009FEB26 /* Project object */;
431 | }
432 |
--------------------------------------------------------------------------------