Here's a function to compute the path (in the Playground). I haven't had time to add the exclusion logic. I believe it could be done by converting the top and bottom lines into a list of rectangles that you can test intersections against. (I'll edit my post to add that if I find the time).
import Foundation
import UIKit
import XCPlayground
// compute enclosing Path for list of views
// ----------------------------------------
// - path is composed of a top line that hugs the topmost views
// and of a bottom line that hugs the bottom most views
// - The two lines span the minimum and maximum x coordinates of
// the views in the list
// NOTE: to do this cleanly, all four sides should be considered
// (I merely showed top and bottom to give an idea of the method)
//
func enclosingPathForViews(views:[UIView], margin:CGFloat = 3) -> UIBezierPath
{
let frames = views.map({$0.frame.insetBy(dx: -margin, dy: -margin)})
var path = UIBezierPath()
// top left and right corners of each view
// sorted from left to right, top to bottom
var topPoints:[CGPoint] = frames.reduce( Array<CGPoint>(),
combine: { $0 + [ CGPoint(x:$1.minX,y:$1.minY),
CGPoint(x:$1.maxX,y:$1.minY) ] })
topPoints = topPoints.sort({ $0.x == $1.x ? $0.y < $1.y : $0.x < $1.x })
// trace top line from left to right
// moving up or down when appropriate
var previousPoint = topPoints.first!
path.moveToPoint(previousPoint)
for point in topPoints
{
guard point.y == previousPoint.y
|| point.y < previousPoint.y
&& frames.contains({$0.minX == point.x && $0.minY < previousPoint.y })
|| point.y > previousPoint.y
&& !frames.contains({ $0.maxX > point.x && $0.minY < point.y })
else { continue }
if point.y < previousPoint.y
{ path.addLineToPoint(CGPoint(x:point.x, y:previousPoint.y)) }
if point.y > previousPoint.y
{ path.addLineToPoint(CGPoint(x:previousPoint.x, y:point.y)) }
path.addLineToPoint(point)
previousPoint = point
}
// botom left and right corners of each view
// sorted from right to left, bottom to top
var bottomPoints:[CGPoint] = frames.reduce( Array<CGPoint>(),
combine: { $0 + [ CGPoint(x:$1.minX,y:$1.maxY),
CGPoint(x:$1.maxX,y:$1.maxY) ] })
bottomPoints = bottomPoints.sort({ $0.x == $1.x ? $0.y > $1.y : $0.x > $1.x })
// trace bottom line from right to left
// starting where top line left off (rightmost top corner)
// moving up or down when appropriate
for point in bottomPoints
{
guard point.y == previousPoint.y
|| point.y > previousPoint.y
&& frames.contains({$0.maxX == point.x && $0.maxY > previousPoint.y })
|| point.y < previousPoint.y
&& !frames.contains({ $0.minX < point.x && $0.maxY > point.y })
else { continue }
if point.y > previousPoint.y
{ path.addLineToPoint(CGPoint(x:point.x, y:previousPoint.y)) }
if point.y < previousPoint.y
{ path.addLineToPoint(CGPoint(x:previousPoint.x, y:point.y)) }
path.addLineToPoint(point)
previousPoint = point
}
// close back to leftmost point of top line
path.closePath()
return path
}
// TESTS:
// ======
// UIView (container)
// ------------------
let viewSize = CGSize(width: 300, height: 300)
let view:UIView = UIView(frame: CGRect(origin: CGPointZero, size: viewSize))
view.backgroundColor = UIColor.whiteColor()
XCPlaygroundPage.currentPage.liveView = view
// Selected Views
// --------------
var selectedViews:[UIView] =
[
UIView(frame:CGRect(x: 130, y: 50, width: 50, height: 50)),
UIView(frame:CGRect(x: 60, y: 30, width: 50, height: 50)),
UIView(frame:CGRect(x: 20, y: 110, width: 50, height: 50))
// , UIView(frame:CGRect(x: 150, y: 150, width: 50, height: 50))
]
for subView in selectedViews
{
subView.backgroundColor = UIColor.greenColor()
view.addSubview(subView)
}
// Excluded views (non-selected)
// --------------
var excludedViews:[UIView] =
[
UIView(frame:CGRect(x: 150, y: 110, width: 50, height: 50)),
]
for subView in excludedViews
{
subView.backgroundColor = UIColor.redColor()
view.addSubview(subView)
}
// CoreGraphics drawing
// --------------------
UIGraphicsBeginImageContextWithOptions(viewSize, false, 0)
UIColor.blackColor().setStroke()
let path = enclosingPathForViews(selectedViews)
path.stroke()
// set image to view layer
view.layer.contents = UIGraphicsGetImageFromCurrentImageContext().CGImage
UIGraphicsEndImageContext()