├── 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 | ![](example.gif) 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 | --------------------------------------------------------------------------------