Interactive Tracking with iOS Charts. How to
I figured it out to save you sometime
extension GraphViewController: ChartViewDelegate {
func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
// I used the x pixel property of the highlight to update the
// position of the floating label, and set its text to the
// x value of the selected entry
floatingLabelCenterConstraint?.constant = highlight.xPx
floatingLabel.text = " Mar 14 \(highlight.x)"
// Create the nice transition animation
// fadeIn() and fadeOut() are simple extension methods I wrote
if floatingLabel.isHidden {
floatingLabel.fadeIn()
subscripts.fadeOut()
}
// This gives you the y value of the selected entry
greenNumber.text = NSString(format: "%.2f", highlight.y) as String
}
}
However, this delegate method is called only when you are moving your finger. You need a way to tell when the pan gesture ended. Charts does not have this delegate method out of the box, so you need to add it. Go to the ChartViewBase.swift
source file, add the following to ChartViewDelegate
protocol
public protocol ChartViewDelegate
{
...
@objc optional func panGestureEnded(_ chartView: ChartViewBase)
}
Then go to BarLineChartViewBase.swift
, find panGestureRecognized
function
@objc private func panGestureRecognized(_ recognizer: NSUIPanGestureRecognizer)
{
...
else if recognizer.state == NSUIGestureRecognizerState.ended || recognizer.state == NSUIGestureRecognizerState.cancelled
{
...
// Add this line at the end
delegate?.panGestureEnded?(self)
}
}
Finally, go back to your viewController and add the following. Now everything works like a charm
func panGestureEnded(_ chartView: ChartViewBase) {
subscripts.fadeIn()
floatingLabel.fadeOut()
// clear selection by setting highlightValue to nil
chartView.highlightValue(nil)
}
UPDATE
This feature has now been added to the latest version of IOS-Charts via this pull request.
ChartViewDelegate
was updated to now include the following method.
public protocol ChartViewDelegate
{
...
/// Called when a user stop highlighting values while panning
@objc optional func chartViewDidEndPanning(_ chartView: ChartViewBase)
}
All you have to do is implement that method wherever you're implementing ChartViewDelegate (the viewController with your chart).
Original Answer Sub-Class LineChartView
@JGuo's answer works, but it requires you to change code inside the pod. These means that every time you update your pod(s) you'll have to re-implement these changes. You can achieve the same results without modifying the pod as follows:
Create a protocol
import Charts
@objc protocol MyChartViewDelegate {
@objc optional func chartValueNoLongerSelected(_ chartView: MyLineChartView)
}
SubClass LineChartView
open class MyLineChartView: LineChartView {
@objc weak var myChartViewDelegate: MyChartViewDelegate?
private var touchesMoved = false
// Haptic Feedback
private let impactGenerator = UIImpactFeedbackGenerator(style: .light)
private let selectionGenerator = UISelectionFeedbackGenerator()
override open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
// This is here to prevent the UITapGesture from blocking touches moved from firing
if gestureRecognizer.isKind(of: NSUITapGestureRecognizer.classForCoder()){
return false
}
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
override open func nsuiTouchesBegan(_ touches: Set<NSUITouch>, withEvent event: NSUIEvent?) {
impactGenerator.impactOccurred()
selectionGenerator.prepare()
// adds the highlight to the graph when tapped
super.nsuiTouchesBegan(touches, withEvent: event)
touchesMoved = false
if let touch = touches.first {
let h = getHighlightByTouchPoint(touch.location(in: self))
if h === nil || h == self.lastHighlighted {
lastHighlighted = nil
highlightValue(nil, callDelegate: true)
}
else {
lastHighlighted = h
highlightValue(h, callDelegate: true)
}
}
}
open override func nsuiTouchesEnded(_ touches: Set<NSUITouch>, withEvent event: NSUIEvent?) {
super.nsuiTouchesEnded(touches, withEvent: event)
myChartViewDelegate?.chartValueNoLongerSelected?(self) // remove the highlight
}
open override func nsuiTouchesCancelled(_ touches: Set<NSUITouch>?, withEvent event: NSUIEvent?) {
super.nsuiTouchesCancelled(touches, withEvent: event)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// if a tap turns into a panGesture touches cancelled is called this prevents the highlight from being moved
if !self.touchesMoved {
self.myChartViewDelegate?.chartValueNoLongerSelected?(self)
}
}
}
override open func nsuiTouchesMoved(_ touches: Set<NSUITouch>, withEvent event: NSUIEvent?) {
super.nsuiTouchesMoved(touches, withEvent: event)
touchesMoved = true
if let touch = touches.first {
let h = getHighlightByTouchPoint(touch.location(in: self))
if h === nil {
lastHighlighted = nil
highlightValue(nil, callDelegate: true)
}
else if h == self.lastHighlighted {
return
}
else {
lastHighlighted = h
highlightValue(h, callDelegate: true)
selectionGenerator.selectionChanged()
}
}
}
}
Create your Chart
let lineChartView = MyLineChartView()
override func viewDidLoad() {
super.viewDidLoad()
lineChartView.delegate = self // built in delegate for user interaction
lineChartView.myChartViewDelegate = self // delegate with our additions (knowing when a value is no longer selected)
lineChartView.highlightPerTapEnabled = false // disable tap gesture to highlight
lineChartView.highlightPerDragEnabled = false // disable pan gesture
}
Implement Delegates
extension MyViewController: ChartViewDelegate {
func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
// Do something on selection
}
}
extension MyViewController: MyChartViewDelegate {
func chartValueNoLongerSelected(_ chartView: FlyLineChartView) {
// Do something on deselection
}
}