UIView with draw function override always at wrong place on screen

My UIView is always drawn in the same place regardless of where I have placed it in Interface Builder, if I have the following code in the draw function.

  override func draw(_ rect: CGRect) {
    let radius = CGFloat(min(frame.size.width, frame.size.height) / 2.1)
    let angle: CGFloat = 2.0 * .pi / 360
    var colorPath = ColorPath(path: UIBezierPath(), color: .clear)
    center = CGPoint(x: frame.width / 2.0,
                     y: frame.height / 2.0)
    for sector in 0..<360 {
      let sector = CGFloat(sector)
      colorPath.path = UIBezierPath(arcCenter: center, radius: radius,
                                    startAngle: sector * angle,
                                    endAngle: (sector + 1.0) * angle,
                                    clockwise: true)
      colorPath.path.addLine(to: center)
      colorPath.path.close()
      let color = UIColor(hue: sector / 360,
                          saturation: 1, brightness: 1, alpha: 1)
      color.setFill()
      color.setStroke()
      colorPath.path.fill()
      colorPath.path.stroke()
      colorPath.color = color
      paths.append(colorPath)
    }
  }


To me it looks like a bug, but maybe I'm doing something wrong.

Replies

You're (possibly) confusing two different meanings of "draw" here. Your view's "draw (_:)" method redraws the view's content, and the positioning of the stuff you draw within the view depends only on the coordinates you specify, not on any IB placement.


We also talk about a view being "drawn" somewhere on the screen (well, somewhere within its superview), referring mainly to its placement. This is a result of how you place it in IB, plus any changes that result from auto-layout. If the view draws in the wrong place, that can (for example) be because your layout constraints are incorrect.


You're going to have to do a bit more investigation to determine whether the view is mis-positioned because of auto-layout, or for some other reason. Keep in mind that, depending on your view hierarchy, it might be correctly positioned within its superview, but its superview might be in the wrong place.


Note that there is a bug in the above code, though it's probably unrelated to your positioning problem. The view's frame is in the coordinate system of its superview, so your "center" and "radius" calculations will be incorrect if your view is scaled. Use "bounds" instead of "frame".

But when I comment out the draw function and only change its background color, it shows up where I want it to be. I have exactly the same wrong behavior, when I add the view in code, and IB isn't involved at all.


For example:

    colorWheel.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height / 2)


Nothing change when I have my code in the draw function, with out my code the view shows up in the center of the screen.

You have more going on here, I notice, since you're saving the paths you draw with as ColorPath objects in a non-local array "paths". Are you doing drawing elsewhere?


IAC, it would still be good methodology to use the debugger (or logging) to check the actual location your view and its superview(s). If the view coordinates are correct relative to the screen, then there's a problem where you're drawing the content at some unintended offset (I guess). If the view coordinates are not correct, then the problem isn't in the drawing (or the drawing is wrong too and just complicating the symptoms).


Are you using Core Animation (layers) in conjunction with this view. If so, the presence or absence of the custom "draw" function may make some difference in the behavior.

Thank you Quincey, but your explanation still did not help me.I'm doing nothing special, the whole class looks like this.


class ColorWheelView: UIView {

  struct ColorPath {
    var path: UIBezierPath
    var color: UIColor
  }

  var paths = [ColorPath]()

  weak var delegate: ColorWheelDelegate?

  override init(frame: CGRect) {
    super.init(frame: frame)
  }

  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)!
  }

  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let point = touches.first?.location(in: self) else { return }
    let color = colorAtPoint(point)
    backgroundColor = color
    delegate?.colorSelected(color)
  }

  override func draw(_ rect: CGRect) {
    super.draw(rect)

    let context = UIGraphicsGetCurrentContext()
    let radius = CGFloat(bounds.size.height / 2.1)
    let angle: CGFloat = 2.0 * .pi / 360
    var colorPath = ColorPath(path: UIBezierPath(), color: .clear)
  
    center = CGPoint(x: bounds.width / 2.0,
                     y: bounds.height / 2.0)
  
    for sector in 0..<360 {
    
      let sector = CGFloat(sector)
    
      colorPath.path = UIBezierPath(arcCenter: center, radius: radius,
                                    startAngle: sector * angle,
                                    endAngle: (sector + 1.0) * angle,
                                    clockwise: true)
    
      colorPath.path.addLine(to: center)
      colorPath.path.close()
    
      let color = UIColor(hue: sector / 360,
                          saturation: 1, brightness: 1, alpha: 1)
      color.setFill()
      color.setStroke()
    
      colorPath.path.fill()
      context?.addPath(colorPath.path.cgPath)
      context?.strokePath()
      colorPath.color = color
    
      paths.append(colorPath)
    }
  }

  func colorAtPoint ( _ point: CGPoint) -> UIColor {
    for colorPath in 0..<paths.count {
      if paths[colorPath].path.contains(point) {
        return paths[colorPath].color
      }
    }
    return .clear
  }
}


And I'm using it here:


import UIKit
class FirstViewController: UIViewController {
  let colorWheel = ColorWheelView(frame:
    CGRect(origin: .zero,
           size: CGSize(width: UIScreen.main.bounds.width,
                        height: UIScreen.main.bounds.width)))

  override func viewDidLoad() {
    super.viewDidLoad()
    colorWheel.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height / 2)
    view.addSubview(colorWheel)
    /
  }
}

>> My UIView is always drawn in the same place regardless of where I have placed it in Interface Builder


If you've placed "it" in IB, then that view is being created for you when your storyboard is loaded. You're also creating a second ColorWheelView in viewDidLoad, at a fixed position, large enough to cover the whole screen, on top of whatever other views already exist, including the ColorWheelView from your storyboard.


If your intention is to have your "colorWheel" instance property refer to the ColorWheelView in your storyboard, then you need to make it an outlet:


class FirstViewController: UIViewController {
  @IBOutlet var colorWheel: ColorWheelView! // nil until view is loaded, therefore must be some kind of optional


and then in IB, connect the view controller's "colorWheel" outlet to the ColorWheelView you put in the storyboard.

I don't do this, it thought it would be easy to show what is going on when I do it in code. I do not do anything in the storyboard. There is only one view, and it's just as strange to me.

The view is always at the top of the screen, changing the view properties has no effect.

colorWheel.center = CGPoint(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height / 2)

You're flailing around here. (That's not an insult. It's a path we all go down sometimes when we hit a frustrating problem.) If you want a helpful answer, you have to bring the problem you actually have, not a different problem caused by unrelated code you added.


Back to the basics:


— What do you have in the storyboard? It sounds like you have a scene for the FirstViewController, which has a root view of type UIView? And a subview of type ColorWheelView? And you specified the correct type for the subview in IB? And you're trying to position the subview relative to the root view? How? What autolayout constraints are you using?


— Does your real FirstViewController class definition have a "colorWheel" property? How is it declared and what is it initialized to? Are you expecting to move this subview around in code (aside from anything you might be doing to debug the problem)?

I think the real problem is I'm failing to descibe what my problem is, but I try it again.

I want my ColorWheelView centered on the screen, and I tried to do this first in the storyboard and after that I tried the same but only with code.

But both ways didn't work out. So I'm puzzled and have no idea what I'm doing wrong.

>> I want my ColorWheelView centered on the screen, and I tried to do this first in the storyboard


OK, what exactly did you try?


The easiest way is to add layout constraints to the view to center it horizontally and vertically in the superview.