Search code examples
iosswiftuiviewframe

Determining if custom iOS views overlap


I've defined a CircleView class:

class CircleView: UIView {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.clear
    }
        
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        // Get the Graphics Context
        if let context = UIGraphicsGetCurrentContext() {
            
            // Set the circle outerline-width
            context.setLineWidth(5.0);
            
            // Set the circle outerline-colour
            UIColor.blue.set()
            
            // Create Circle
            let center = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
            let radius = (frame.size.width - 10)/2
            context.addArc(center: center, radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
                        
            context.setFillColor(UIColor.blue.cgColor)
                
            // Draw
            context.strokePath()
            context.fillPath()
        }
    }

}

And created an array of them with a randomly set number:

    var numberOfCircles: Int!
    var circles: [CircleView] = []

    numberOfCircles = Int.random(in: 1..<10)
    let circleWidth = CGFloat(50)
    let circleHeight = circleWidth
    
    var i = 0
    while i < numberOfCircles {
        let circleView = CircleView(frame: CGRect(x: 0.0, y: 0.0, width: circleWidth, height: circleHeight))
        circles.append(circleView)
        i += 1
    }

After creating the circles, I call a function, drawCircles, that will draw them on the screen:

 func drawCircles(){
        
        for c in circles {
            c.frame.origin = c.frame.randomPoint
            while !UIScreen.main.bounds.contains(c.frame.origin) {
                                    c.frame.origin = CGPoint()
                                    c.frame.origin = c.frame.randomPoint
 
                let prev = circles.before(c)
                if prev?.frame.intersects(c.frame) == true {
                    c.frame.origin = c.frame.randomPoint
                }
            }
        }
                         
        for c in circles {
            self.view.addSubview(c)
        }
    }

The while loop in the drawCircles method makes sure that no circles are placed outside of the bounds of the screen, and works as expected.

What I'm struggling with is to make sure that the circles don't overlap each other, like so:

enter image description here

enter image description here

I'm using the following methods to determine either the next

I'm using this methods to determine what the previous / next element in the array of circles:

extension BidirectionalCollection where Iterator.Element: Equatable {
    typealias Element = Self.Iterator.Element

    func after(_ item: Element, loop: Bool = false) -> Element? {
        if let itemIndex = self.firstIndex(of: item) {
            let lastItem: Bool = (index(after:itemIndex) == endIndex)
            if loop && lastItem {
                return self.first
            } else if lastItem {
                return nil
            } else {
                return self[index(after:itemIndex)]
            }
        }
        return nil
    }

    func before(_ item: Element, loop: Bool = false) -> Element? {
        if let itemIndex = self.firstIndex(of: item) {
            let firstItem: Bool = (itemIndex == startIndex)
            if loop && firstItem {
                return self.last
            } else if firstItem {
                return nil
            } else {
                return self[index(before:itemIndex)]
            }
        }
        return nil
    }
}

This if statement, however; doesn't seem to be doing what I'm wanting; which is to make sure that if a circle intersects with another one, to change it's origin to be something new:

if prev?.frame.intersects(c.frame) == true {
    c.frame.origin = c.frame.randomPoint
}

If anyone has any ideas where the logic may be, or of other ideas on how to make sure that the circles don't overlap with each other, that would be helpful!

EDIT: I did try the suggestion that Eugene gave in his answer like so, but still get the same result:

  func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
        let xDist = a.x - b.x
        let yDist = a.y - b.y
        return CGFloat(sqrt(xDist * xDist + yDist * yDist))
    }
    

       if prev != nil {
                
                if distance((prev?.frame.origin)!, c.frame.origin) <= 40 {
                    print("2")
                    c.frame.origin = CGPoint()
                    c.frame.origin = c.frame.randomPoint
                    
                }
            }

But still the same result

EDIT 2

Modified my for loop based on Eugene's edited answer / clarifications; still having issues with overlapping circles:

for c in circles { c.frame.origin = c.frame.randomPoint

    let prev = circles.before(c)
    let viewMidX = self.circlesView.bounds.midX
    let viewMidY = self.circlesView.bounds.midY

    let xPosition = self.circlesView.frame.midX - viewMidX + CGFloat(arc4random_uniform(UInt32(viewMidX*2)))
    let yPosition = self.circlesView.frame.midY - viewMidY + CGFloat(arc4random_uniform(UInt32(viewMidY*2)))

    
    if let prev = prev {
        if distance(prev.center, c.center) <= 50 {
            c.center = CGPoint(x: xPosition, y: yPosition)
        }
    }

}

Solution

  • That’s purely geometric challenge. Just ensure that distance between the circle centers greater than or equal to sum of their radiuses.

    Edit 1

    Use UIView.center instead of UIView.frame.origin. UIView.frame.origin gives you the top left corner of UIView.

    if let prev = prev {
        if distance(prev.center, c.center) <= 50 {
             print("2")
             c.center = ...
        }
    }
    

    Edit 2

    func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
        let xDist = a.x - b.x
        let yDist = a.y - b.y
        return CGFloat(hypot(xDist, yDist))
    }
    
    let prev = circles.before(c)
    
    if let prevCircleCenter = prev?.center {
        let distance = distance(prevCenter, c.center)
        if distance <= 50 {
            let viewMidX = c.bounds.midX
            let viewMidY = c.bounds.midY
          
            var newCenter = c.center
            var centersVector = CGVector(dx: newCenter.x - prevCircleCenter.x, dy: newCenter.y - prevCircleCenter.y)
            centersVector.dx *= 51 / distance
            centersVector.dy *= 51 / distance
            newCenter.x = prevCircleCenter.x + centersVector.dx
            newCenter.y = prevCircleCenter.y + centersVector.dy
            c.center = newCenter
        }
    }