3

We have an array of CGPoints:

let points = [(1234.0, 1053.0), (1241.0, 1111.0), (1152.0, 1043.0)]

What I'm trying to do is get the CGPoint with the highest x value and the one with the highest y value in the array. I will be using these points to create a CGRect:

extension CGRect {
    init(p1: CGPoint, p2: CGPoint) {
        self.init(x: min(p1.x, p2.x),
                  y: min(p1.y, p2.y),
                  width: abs(p1.x - p2.x),
                  height: abs(p1.y - p2.y))
    }
}

I know there a way to get max and min values in an array by doing something like this:

points.min()
points.max()

but these don't seem to work since its an array of CGPoints. Is it possible to get these values from the array?

SwiftyJD
  • 5,257
  • 7
  • 41
  • 92
  • `CGPoint` isn't comparable because it's not obvious what `pointA < pointB` would mean. Are you trying to create a minimal rectangle that contains all the `points`? – Alexander May 16 '20 at 21:40
  • @Alexander-ReinstateMonica I'm trying to create a CGRect based off of the highest and lowest of the y and x coordinates, so yes, pretty much a shape – SwiftyJD May 16 '20 at 21:42

3 Answers3

4

You can map values to find the min and max in x and y coordinates like below. If you're not sure if points array contains any data, use guard statement to avoid force unwrapping:

let xArray = points.map(\.x)
let yArray = points.map(\.y)
guard let minX = xArray.min(),
      let maxX = xArray.max(),
      let minY = yArray.min(),
      let maxY = yArray.max() else { return }

And from there:

let minPoint = CGPoint(x: minX, y: minY)
let maxPoint = CGPoint(x: maxX, y: maxY)

then you can modify your extension function because you already know which values are min and max:

extension CGRect {
    init(minPoint: CGPoint, maxPoint: CGPoint) {
        self.init(x: minPoint.x,
                  y: minPoint.y,
                  width: maxPoint.x - minPoint.x,
                  height: maxPoint.y - minPoint.y)
    }
}

As Leo Dabus suggested in the comment below, you can do it all in one go inside failable initializer extension:

extension CGRect {
    init?(points: [CGPoint]) {
        let xArray = points.map(\.x)
        let yArray = points.map(\.y)
        if  let minX = xArray.min(),
            let maxX = xArray.max(),
            let minY = yArray.min(),
            let maxY = yArray.max() {

            self.init(x: minX,
                      y: minY,
                      width: maxX - minX,
                      height: maxY - minY)
        } else {
            return nil
        }
    }
}
RealUglyDuck
  • 338
  • 2
  • 13
  • 1
    There is no need to use a closure `map(\.x)` and `map(\.y)` and keep a reference to them instead of mapping them twice. You can also move them inside you initialization method and make it fallible. – Leo Dabus May 16 '20 at 23:01
  • 1
    Thank you @LeoDabus. I updated my answer based on your suggestions. – RealUglyDuck May 16 '20 at 23:32
3

You can use max(by:).

let minXPoint = points.min(by: {$0.x < $1.x}) //(1152.0, 1043.0)
let maxYPoint = points.max(by: {$0.y < $1.y}) //(1241.0, 1111.0)
arturdev
  • 10,884
  • 2
  • 39
  • 67
  • I really like this answer. – HalR May 16 '20 at 22:07
  • For the `points` mentioned in the question it works but what if the point with minX won't be the same as the point with minY ? Then it will return minXPoint which won't have minY as well. – RealUglyDuck May 16 '20 at 22:37
0

You can't do that with just a single call to min()/max()

off of the highest and lowest of the y and x coordinates

The point with the minimal x value could be different from the point with the minimal x value. So any method that returns only one of the points (as min()/max() do) isn't sufficient.

You either need two separate calls to each, like:

let minX = points.min(by: { $0.x < $1.x })!.x
let minY = points.min(by: { $0.y < $1.y })!.y
let maxX = points.min(by: { $0.x > $1.x })!.x
let maxY = points.min(by: { $0.y > $1.y })!.y

or you could try doing it all in one swoop with reduce:

let initialAccumulator = (
    minX: CGFloat.max, minY: CGFloat.max, maxX: CGFloat.min, maxY: CGFloat.min)
let (minX, minY, maxX, maxY) = points.reduce(initialAccumulator) { accumulator, point in
    return (
        minX: min(accumulator.minX, point.x),
        minY: min(accumulator.minY, point.y),
        maxX: max(accumulator.maxX, point.x),
        maxY: max(accumulator.maxY, point.y)
    )
}

I would probably do this like so:


extension CGRect {
    static func minimalRect(containing points: [CGPoint]) -> CGRect? {
        if points.isEmpty { return nil }

        let minX = points.min(by: { $0.x < $1.x })!.x
        let minY = points.min(by: { $0.y < $1.y })!.y
        let maxX = points.min(by: { $0.x > $1.x })!.x
        let maxY = points.min(by: { $0.y > $1.y })!.y

        return CGRect(
            x: minX,
            y: minY,
            width: (minX - maxX).magnitude,
            height: (minY - maxY).magnitude
        )
    }
}


let points = [(1234.0, 1053.0), (1241.0, 1111.0), (1152.0, 1043.0)].map(CGPoint.init)
let r = CGRect.minimalRect(containing: points)
print(r ?? .zero) // => (1152.0, 1043.0, 89.0, 68.0)
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Why not a fallible initializer instead of a static method? – Leo Dabus May 16 '20 at 22:44
  • I got back and forth on the two, but in this case, I didn't use an initializer because I I wanted to use the word "rect" in the name. I like callsite `CGRect.minimalRect(containing: points)` more than `CGRect(minimalRectContaining: points)` Points or `CGRect(minimalContaining: Points)` – Alexander May 17 '20 at 00:21