Greg
Greg

Reputation: 1842

Why does UIView not stay on centre while it rotates?

I am trying to calibrate a UIView to stay on centre as it rotates. This follows up a problem identified by the best answers to my previous question. I have since devised code based on one answer proposing to use viewWillAppear or viewDidLayoutSubviews instead of viewDidLoad.

As I am new to Swift I had to revisit the Swift tutorial Implement a Custom Control which describes how to make a subClass of UIView. My code can draw a circle using any one of three functions: viewWillAppear, viewDidLayoutSubviews or viewDidLoad.

However when I render the code, the circle doesn’t always remain on centre as the screen rotates. Below are successive rotations that happen using viewDidLoad or viewWillAppearviewDidLoad or viewWillAppear When the app is launched on a screen in portrait orientation the problem appears immediately after the first rotation irrespective of left or right. If launched in landscape orientation it is possible to miss a problem if one has not tried at least four successive rotations.

Below are successive rotations that happen using viewDidLayoutSubviews where rotation occasionally results in an on-centre circle overlaying an off-centre circle. viewDidLayoutSubviews This can happen after several rotations but one must complete at least four rotations in order to notice a problem.

Here is the code for CircleView - my attempt to make a subclass of UIView

import UIKit

class CircleView: UIView {
    let radius          = CGFloat(200)
    let x: CGFloat      = 0
    let y: CGFloat      = 0

// MARK: Initialization

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

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

func setup() {

    // Create a CAShapeLayer
    let shapeLayer = CAShapeLayer()

    // The Bezier path that we made needs to be converted
    // to a CGPath before it can be used on a layer.

    shapeLayer.path = createBezierPath().cgPath

    // apply other properties related to the path

    shapeLayer.strokeColor      = UIColor.red.cgColor
    shapeLayer.fillColor        = UIColor.white.cgColor
    shapeLayer.lineWidth        = 8.0

    // add the new layer to our custom view
    self.layer.addSublayer(shapeLayer)
}

func createBezierPath() -> UIBezierPath {
    // create a new path

    let path  = UIBezierPath(arcCenter: CGPoint(x: x, y: y),
                radius: radius,
                startAngle: CGFloat(M_PI * -0.5),
                endAngle:CGFloat(M_PI * 1.5),
                clockwise: true)

    path.fill()
    path.stroke()
    return path
    } 
}

and here is the code for ViewController

import UIKit

class ViewController: UIViewController {
    var circle: CircleView?
    var x: CGFloat?
    var y: CGFloat?

    override func viewDidLoad() {
    super.viewDidLoad()
    let centre = centreCircle()

    // create a new UIView and add it to the view controller
    let circleView = CircleView()
    circleView.frame = CGRect(x: centre.x, y: centre.y, width: 0, height: 0)
    view.addSubview(circleView)
    }

    func centreCircle() -> CGPoint {
    circle?.bounds = UIScreen.main.bounds
    return CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY)
    }
}

And here is code for the UIScreen extension that returns the screen's centre point

import UIKit

extension UIScreen {
    var centre: CGPoint {
    return CGPoint(x: bounds.midX, y: bounds.midY)
    }
}

To produce the above rotations I replaced viewDidLoad function with viewWillAppear

    override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    let centre = centreCircle()

    // create a new UIView and add it to the view controller
    let circleView = CircleView()
    circleView.frame = CGRect(x: centre.x, y: centre.y, width: 0, height: 0)
    view.addSubview(circleView)
    }

or viewDidLayoutSubviews

    override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    let centre = centreCircle()

    // create a new UIView and add it to the view controller
    let circleView = CircleView()
    circleView.frame = CGRect(x: centre.x, y: centre.y, width: 0, height: 0)
    view.addSubview(circleView)
    }

Can anyone see a problem in my code that might be stopping rotation from working ?

Upvotes: 1

Views: 203

Answers (2)

twiz_
twiz_

Reputation: 1198

First of all, the view's aren't laid out properly in viewdidload and viewwillappear, and you're explicitly defining the frame with values, something I found extremely frustrating as a newish programmer. It still annoys me particularly when it comes to resizing labels. So putting your view and mainscreen's centreX and centreY means your view will stick at that point when you rotate.

Fix this by learning autolayout. It sucks at first but it will eventually make your life easier. Take a look at the method in this extension. I use it when I want to centre a view to another view

extension UIView {


    func constrainAndCenter(in view: UIView, withRelativeHeight: CGFloat, withRelativeWidth: CGFloat) -> [NSLayoutConstraint] {
        self.translatesAutoresizingMaskIntoConstraints = false

        let x = NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: 0)
        let w = NSLayoutConstraint(item: self, attribute: .width, relatedBy: .equal, toItem: view, attribute: .width, multiplier: withRelativeWidth, constant: 0)
        let h = NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: view, attribute: .height, multiplier: withRelativeHeight, constant: 0)
        let y = NSLayoutConstraint(item: self, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1, constant: 0)

        return ([x,w,h,y])
    }
}

// then in viewdidload or whereever
let circleView = CircleView() 
let circleViewConstraints = circleView.constrainAndCenter(in: view,....)
view.addSubview(circleView)
NSLayoutConstraint.activate(circleViewConstraints)

That will pin it to the middle. You can also do it in interface builder.

Upvotes: 1

DeyaEldeen
DeyaEldeen

Reputation: 11807

you must replace frame.bounds with frame.center to have the center calculated right.

Upvotes: 0

Related Questions