Skip to content

Instantly share code, notes, and snippets.

@alexpersian
Last active January 15, 2020 02:12
Show Gist options
  • Save alexpersian/909786d0095ff00c8a2e2fcd85186f35 to your computer and use it in GitHub Desktop.
Save alexpersian/909786d0095ff00c8a2e2fcd85186f35 to your computer and use it in GitHub Desktop.
ViewController class that will render a shape and then on-tap will draw a bounding box around that shape.
//
// ViewController with a single shape drawn inside it.
// Tapping on the shape will cause a red minimum-bounding box to be drawn
// around the shape's bounds.
//
// Included as a single monofile for the sake of including it all within a single gist.
// Core logic centers around the Flood Fill algorithm with an interative, BFS approach used.
//
import UIKit
import CoreGraphics
final class ViewController: UIViewController {
private var screen: UIImage?
private var maxX: CGFloat { return view.frame.width }
private var maxY: CGFloat { return view.frame.height }
private var visited: Set<CGPoint> = Set()
private var boundingPoints: [CGPoint] = []
private var topLeftBound: CGPoint = .zero
private var bottomRightBound: CGPoint = .zero
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let polyFrame = CGRect(x: view.frame.midX - 100,
y: view.frame.midY - 100,
width: 200,
height: 200)
let polygon = PolygonView(frame: polyFrame)
view.addSubview(polygon)
// We take a snapshot of the screen to analyze instead of checking
// the actual screen data every iteration of floodFill.
screen = captureScreen()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(start))
polygon.addGestureRecognizer(tapGesture)
}
@objc private func start(gestureRecognizer: UITapGestureRecognizer) {
let startPoint = gestureRecognizer.location(in: view)
floodFill(start: startPoint)
findBoundingCoordinates()
drawBoundingBox()
}
private func captureScreen() -> UIImage? {
defer { UIGraphicsEndImageContext() }
UIGraphicsBeginImageContext(self.view.frame.size)
if let ctx = UIGraphicsGetCurrentContext() {
self.view.layer.render(in: ctx)
return UIGraphicsGetImageFromCurrentImageContext()
} else {
return nil
}
}
private func floodFill(start: CGPoint) {
var pointQueue: [CGPoint] = []
pointQueue.append(start)
while !pointQueue.isEmpty {
let point = pointQueue.removeFirst()
// Make sure we havne't visited this point already
guard !visited.contains(point) else { continue }
if colorIsBlack(at: point) { // We are still within the object
// Add all of its neighbors
pointQueue.append(CGPoint(x: point.x - 1, y: point.y))
pointQueue.append(CGPoint(x: point.x + 1, y: point.y))
pointQueue.append(CGPoint(x: point.x, y: point.y - 1))
pointQueue.append(CGPoint(x: point.x, y: point.y + 1))
} else { // We've reached the boundary
// Add the point to our list of boundary points
boundingPoints.append(point)
}
// Mark the point as visited
visited.insert(point)
}
}
private func colorIsBlack(at point: CGPoint) -> Bool {
if let color = screen?.color(at: point), color.equals(.black) {
return true
} else {
return false
}
}
private func findBoundingCoordinates() {
boundingPoints.sort { $0.x < $1.x }
topLeftBound.x = boundingPoints.first?.x ?? 0
bottomRightBound.x = boundingPoints.last?.x ?? 0
boundingPoints.sort { $0.y < $1.y }
topLeftBound.y = boundingPoints.first?.y ?? 0
bottomRightBound.y = boundingPoints.last?.y ?? 0
}
private func drawBoundingBox() {
let boundingRect = CGRect(x: topLeftBound.x,
y: topLeftBound.y,
width: bottomRightBound.x - topLeftBound.x,
height: bottomRightBound.y - topLeftBound.y)
let boundingView = UIView(frame: boundingRect)
boundingView.backgroundColor = .none
boundingView.layer.borderColor = CGColor(srgbRed: 1.0, green: 0, blue: 0, alpha: 1.0)
boundingView.layer.borderWidth = 2.0
self.view.addSubview(boundingView)
}
}
// Borrowed from https://stackoverflow.com/a/50624060/3434244
extension UIImage {
func color(at point: CGPoint) -> UIColor? {
if point.x < 0 || point.x > self.size.width || point.y < 0 || point.y > self.size.height { return nil }
guard
let provider = self.cgImage?.dataProvider,
let providerData = provider.data,
let data = CFDataGetBytePtr(providerData)
else { return nil }
let numberOfComponents = 4
let pixelData = Int((size.width * point.y) + point.x) * numberOfComponents
let r = CGFloat(data[pixelData]) / 255.0
let g = CGFloat(data[pixelData + 1]) / 255.0
let b = CGFloat(data[pixelData + 2]) / 255.0
let a = CGFloat(data[pixelData + 3]) / 255.0
return UIColor(red: r, green: g, blue: b, alpha: a)
}
}
// Borrowed from https://stackoverflow.com/a/40486973/3434244
extension UIColor {
func equals(_ rhs: UIColor) -> Bool {
var lhsR: CGFloat = 0
var lhsG: CGFloat = 0
var lhsB: CGFloat = 0
var lhsA: CGFloat = 0
self.getRed(&lhsR, green: &lhsG, blue: &lhsB, alpha: &lhsA)
var rhsR: CGFloat = 0
var rhsG: CGFloat = 0
var rhsB: CGFloat = 0
var rhsA: CGFloat = 0
rhs.getRed(&rhsR, green: &rhsG, blue: &rhsB, alpha: &rhsA)
return lhsR == rhsR &&
lhsG == rhsG &&
lhsB == rhsB &&
lhsA == rhsA
}
}
extension CGPoint: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(x)
hasher.combine(y)
}
static func == (lhs: CGPoint, rhs: CGPoint) -> Bool {
return lhs.x == rhs.x && lhs.y == rhs.y
}
}
final class PolygonView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect) {
let size = self.bounds.size
let origin = self.bounds.origin
let h1 = size.height * 0.25
let h2 = size.height * 0.50
// Calculate the 5 points of the polygon
let p1 = CGPoint(x: origin.x + (size.width / 2), y: origin.y)
let p2 = CGPoint(x: origin.x, y: p1.y + h1)
let p3 = CGPoint(x: p2.x, y: p2.y + h2)
let p4 = CGPoint(x: p1.x, y: size.height)
let p5 = CGPoint(x: size.width, y: p3.y)
let p6 = CGPoint(x: size.width, y: p2.y)
// Create the path
let path = UIBezierPath()
path.move(to: p1)
path.addLine(to: p2)
path.addLine(to: p3)
path.addLine(to: p4)
path.addLine(to: p5)
path.addLine(to: p6)
path.close()
// Fill the path with color
UIColor.black.set()
path.fill()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment