Last active
October 30, 2020 21:12
-
-
Save aheze/ef074988348234dd6c828c14c288ee2c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func placeHighlights(atTheseLocations rectangles: [CGRect]) { | |
/// ↓ We're going to do more than just simple fade transitions, so remove or comment out the following ↓ | |
// ----------------------------------------------------------------- | |
/// First, we'll remove all the existing highlights in the drawingView | |
// for highlight in drawingView.subviews { | |
// | |
// /// Fading it out | |
// UIView.animate(withDuration: 0.3, animations: { | |
// highlight.alpha = 0 | |
// }) { _ in | |
// | |
// /// Then removing it once it's finished fading | |
// highlight.removeFromSuperview() | |
// } | |
// } | |
// ----------------------------------------------------------------- | |
/// this will be the updated views that will be currently shown to the user | |
var currentViews = [UIView]() | |
/// this is the maximum distance that we'll consider "near" | |
/// if any old highlight is less than this, we'll animate this to the new position | |
let maximumNearDistance = CGFloat(15) | |
/// to be efficient, instead of using the Distance Formula, we're going to use a modified version of it | |
/// the modified Distance Formula is the exact same, except we're not square rooting at the end | |
/// this saves a lot of processing power, as we'll just compare the non-square-rooted result with maximumNearDistanceSquared | |
let maximumNearDistanceSquared = pow(maximumNearDistance, 2) | |
/// we're going to loop through the rectangles, adding a highlight "in real life" every time | |
/// except now, if a previous highlight is less than 8 points away from one that we're about to add, we'll reuse it and animate it to the new position | |
for rectangle in rectangles { | |
/// We're going to add a variable called lowestDist. | |
/// Later, we'll check if this is under 225 (15*15). | |
var lowestDist = CGFloat(10000) | |
/// This is a dictionary mapping distances to UIViews. | |
var distToView = [CGFloat: UIView]() | |
/// previousHighlightComponents are the highlights that are currently on the screen (those that we're going to update) | |
for oldView in previousHighlightComponents { | |
/// we're going to loop over previousHighlightComponents to check if any view is NEAR the the current rectangle that we're going to place | |
/// check the centers of the rectangles instead of the origin (Vision doesn't always return exact values -- sometimes the width, height, and origin are off. The center acts as a more stable point, kind of like an average. | |
let currentCompPoint = CGPoint(x: rectangle.midX, y: rectangle.midY) | |
let oldCompPoint = oldView.center | |
/// because normal Distance Formula (includes square rooting) is time consuming, relativeDistance doesn't square it at the end | |
/// this is perfectly fine for out case because we're only comparing distances and we don't actually need an accurate distance. | |
let distanceBetweenPoints = relativeDistance(currentCompPoint, oldCompPoint) | |
/// If the distance is lower than any previous distances, we'll make this the lowestDist | |
if distanceBetweenPoints <= lowestDist { | |
lowestDist = distanceBetweenPoints | |
/// we're going to map the previous highlight to the lowest dist | |
distToView[lowestDist] = oldView | |
} | |
} | |
/// maximumNearDistanceSquared is a magic number, but it works pretty fine in our case (square root of 225 is 15) | |
/// so if there is a previous view that is 15 points away, we'll reuse it and slide it into the new position | |
if lowestDist <= maximumNearDistanceSquared { | |
guard let oldView = distToView[lowestDist] else { | |
return | |
} | |
/// these are the views that will be currently (this Vision pass) shown to the user | |
currentViews.append(oldView) | |
UIView.animate(withDuration: 0.5, animations: { | |
/// reuse and slide the oldView over | |
oldView.frame = rectangle | |
}) | |
} else { | |
/// There's no previous highlight that's near it, so we'll just fade it in | |
let highlight = UIView(frame: CGRect(x: rectangle.origin.x, y: rectangle.origin.y, width: rectangle.width, height: rectangle.height)) | |
/// alpha is 0 right now because we're going to fade it in | |
highlight.alpha = 0 | |
/// we're going to draw a rounded rectangle | |
let newLayer = CAShapeLayer() | |
let layerRect = CGRect(x: 0, y: 0, width: rectangle.width, height: rectangle.height) | |
newLayer.bounds = layerRect | |
newLayer.path = UIBezierPath(roundedRect: layerRect, cornerRadius: rectangle.height / 3.5).cgPath | |
newLayer.lineWidth = 3 | |
/// set the position of the rounded rectangle (by default it would appear centered in the upper left corner, we don't want that) | |
let x = newLayer.bounds.size.width / 2 | |
let y = newLayer.bounds.size.height / 2 | |
newLayer.position = CGPoint(x: x, y: y) | |
/// Color of the highlights | |
newLayer.fillColor = #colorLiteral(red: 0.6502560775, green: 0.9603669892, blue: 1, alpha: 0.3) | |
newLayer.strokeColor = #colorLiteral(red: 0, green: 0.6823529412, blue: 0.937254902, alpha: 1) | |
/// Add the rounded rectangle to the highlight (a UIView) | |
highlight.layer.addSublayer(newLayer) | |
/// we'll add the highlight to the drawingView now | |
drawingView.addSubview(highlight) | |
/// ↓ ADD THIS ↓ (these are the views that will be currently (this Vision pass) shown to the user) | |
currentViews.append(highlight) | |
/// and finally, we'll fade it in! | |
UIView.animate(withDuration: 0.3, animations: { | |
highlight.alpha = 1 | |
}) | |
} | |
} | |
/// Now, we'll fade out the old highlights, EXCEPT those that we reused and animated to a new position | |
for oldView in previousHighlightComponents { | |
/// currentViews is the array of highlights that will be currently shown to the user, including the reused old highlights | |
/// so if the currentViews doesn't contain oldView, we know that we don't need it anymore and we'll fade it out | |
if !currentViews.contains(oldView) { | |
UIView.animate(withDuration: 0.3, animations: { | |
oldView.alpha = 0 | |
}, completion: { _ in | |
/// remove it from drawingView now | |
oldView.removeFromSuperview() | |
}) | |
} | |
} | |
/// Once we're done doing all the UI updates, we're going to make previousHighlightComponents the currentViews | |
/// Because currentViews will no longer be current in the next Vision pass | |
/// In the next Vision pass, we'll be comparing positions against previousHighlightComponents again! | |
previousHighlightComponents = currentViews | |
} | |
/// This is the Distance Formula, except we're not going to square the result | |
/// Not squaring the result saves a lot of processing power | |
func relativeDistance(_ a: CGPoint, _ b: CGPoint) -> CGFloat { | |
let xDist = a.x - b.x | |
let yDist = a.y - b.y | |
return CGFloat(xDist * xDist + yDist * yDist) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment