0

I have a view which contains multiple subviews. I need to draw an outline around all the subviews that the user selects and ignore those which are not selected. I have tried creating a convex hull but it does not solve my purpose correctly. Is there something built in iOS that I can use to draw the boundary around the selected views?

I found this but it is for intersecting views only: link

Here is a sketch to what I am trying to do. The views with S mean selected and NS mean not selected. Red marked line is the outline.

Sketch

To clarify, if there is a view view in between top left, top right and bottom left in the example, it will not be possible to make the path and thus should not be drawn.

Community
  • 1
  • 1
Rishi
  • 1,987
  • 6
  • 32
  • 49
  • 1
    It's not really clear what you want exactly to happen. What if the user selects the top left, bottom left and bottom right views in your example? – Tim Vermeulen Feb 29 '16 at 08:43
  • Then a path will be drawn avoiding the top right. If there is a view view in between top left, top right and bottom left in the example, it will not be possible to make the path and thus should not be drawn. – Rishi Feb 29 '16 at 09:10

2 Answers2

2

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()
Alain T.
  • 40,517
  • 4
  • 31
  • 51
0

If you just need to set the boundary, you should subclass or extend the object e.g. UIButton, to override control events calls or without subclassing, in the IBAction or gesture callback, set the button.layer.borderWidth = 1.0 on the button. Set it back to 0.0 when you want to hide it.

You can also set the borderColor and the cornerRadius.

Depending on the button content, you might need to set clipsSubviews = true

some_id
  • 29,466
  • 62
  • 182
  • 304
  • Thank you for your reply. I have added a link to explain my problem better with a diagram. – Rishi Feb 29 '16 at 08:32