围绕多个子视图的 UIBezierPath 轮廓

UIBezierPath outline around multiple subviews

我有一个包含多个子视图的视图。我需要在用户选择的所有子视图周围画一个轮廓,并忽略那些没有被选中的子视图。我曾尝试创建一个凸包,但它没有正确解决我的目的。 iOS 是否有内置的东西可以用来在所选视图周围绘制边界?

我找到了这个,但它仅适用于相交视图:link

这是我正在尝试做的事情的草图。带有 S 的视图表示选中,NS 表示未选中。红色标记线是轮廓。

澄清一下,如果示例中左上角、右上角和左下角之间存在视图视图,则无法制作路径,因此不应绘制。

如果你只需要设置边界,你应该子类化或扩展对象,例如UIButton,要覆盖控制事件调用或不进行子类化,在 IBAction 或手势回调中,在按钮上设置 button.layer.borderWidth = 1.0。当你想隐藏它时,将它设置回 0.0。

您还可以设置边框颜色和角半径。

根据按钮内容,您可能需要设置 clipsSubviews = true

这是一个计算路径的函数(在 Playground 中)。我还没来得及添加排除逻辑。我相信这可以通过将顶线和底线转换为您可以测试交叉点的矩形列表来完成。 (如果我有时间,我会编辑我的 post 以添加它)。

 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({[=10=].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: { [=10=] + [ CGPoint(x:.minX,y:.minY),
                                             CGPoint(x:.maxX,y:.minY) ] })
   topPoints = topPoints.sort({ [=10=].x == .x ? [=10=].y < .y : [=10=].x < .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({[=10=].minX == point.x && [=10=].minY < previousPoint.y })
         || point.y > previousPoint.y
            && !frames.contains({ [=10=].maxX > point.x && [=10=].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: { [=10=] + [ CGPoint(x:.minX,y:.maxY),
                                                  CGPoint(x:.maxX,y:.maxY) ] })
   bottomPoints = bottomPoints.sort({ [=10=].x == .x ? [=10=].y > .y : [=10=].x > .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({[=10=].maxX == point.x && [=10=].maxY > previousPoint.y })
         || point.y < previousPoint.y
            && !frames.contains({ [=10=].minX < point.x && [=10=].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()