if we're in the real pre-commit handler we can

2019-01-24 18:05发布

问题:

I am trying to find why a collection of custom UIButtons does not work. In a version described in my earlier post I created a circle of UIButtons programmatically in Swift 3 and anchored the circle to the centre of the screen. That version used a subclass of UIView - based on an Apple Swift tutorial (Implement the Button Action) - together with an implementation of autolayout that draws on Imanou Petit’s excellent code examples (No.6 Anchor). In that version I managed to get my buttons to rotate successfully when the iPhone rotates but the button action-target fails to work.

So I have now tried an alternative version using a viewcontroller instead of a subclass of UIView. This time the same button action-target works but rotating the phone causes the image to shift away from the centre as shown below.

With each rotation the following message also appears twice in the debug area of Xcode.

    ***[App] if we're in the real pre-commit handler we can't 
    actually add any new fences due to CA restriction***

The message happens three times out of four, i.e. there is no message when the phone is turned upside down. This occurs when I run either the code in my previous post or the code shown below. And in each case it made no difference whether the Upside Down box was checked or un-checked.

I also tried disabling OS_ACTIVITY MODE but that changed nothing except hide a message that might potentially explain the problem. Someone more experienced than me will hopefully recognise what this debug message means either in the context of my previous code (shown here) or my latest code, shown below.

ORIGINAL CODE

import UIKit

class ViewController: UIViewController {

// MARK: Initialization

let points: Int             = 10    // 80 25 16 10  5
let dotSize: CGFloat        = 60    // 12 35 50 60 99
let radius: CGFloat         = 48    // 72 70 64 48 42
var centre: CGPoint?

var arcPoint                = CGFloat(M_PI * -0.5)  // clockwise from 12+ (not 3+)!

override func viewDidLoad() {
    super.viewDidLoad()

    let myView = UIView()
    myView.translatesAutoresizingMaskIntoConstraints   = false
    view.addSubview(myView)
    centre = centrePoint()
    let horizontalConstraint = myView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
    let verticalConstraint = myView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
    NSLayoutConstraint.activate([horizontalConstraint, verticalConstraint])

    drawUberCircle()
    drawBoundaryCircles()

}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

func drawUberCircle() {

    // Create a CAShapeLayer

    let shapeLayer = CAShapeLayer()

    // give Bezier path layer properties
    shapeLayer.path = createBezierPath().cgPath

    // apply layer properties
    shapeLayer.strokeColor      = UIColor.cyan.cgColor
    shapeLayer.fillColor        = UIColor.cyan.cgColor
    shapeLayer.lineWidth        = 1.0

    // add layer
    view.layer.addSublayer(shapeLayer)
}

func createBezierPath() -> UIBezierPath {
    // create a new path
    let path  = UIBezierPath(arcCenter: centre!,
                             radius: radius * 2.0,
                             startAngle: CGFloat(M_PI * -0.5),
                             endAngle: CGFloat(M_PI * 1.5),
                             clockwise: true)
    return path
}

func drawBoundaryCircles() {

    for index in 1...points {
        let point: CGPoint  = makeBoundaryPoint(centre: centre!)
        drawButton(point: point, index: index)
    }
}

func makeBoundaryPoint(centre: CGPoint) -> (CGPoint) {
    arcPoint += arcAngle()
    print(arcPoint)
    let point   = CGPoint(x: centre.x + (radius * 2 * cos(arcPoint)), y: centre.y + (radius * 2 * sin(arcPoint)))
    return (point)
}

func arcAngle() -> CGFloat {
    return CGFloat(2.0 * M_PI) / CGFloat(points)
}


func centrePoint() -> CGPoint {
    return CGPoint(x: view.bounds.midX, y: view.bounds.midY)
}

func drawButton(point: CGPoint, index: Int) {
    let myButton = UIButton(type: .custom) as UIButton
    myButton.frame              = CGRect(x: point.x - (dotSize/2), y: point.y - (dotSize/2), width: dotSize, height: dotSize)
    myButton.backgroundColor    = UIColor.white
    myButton.layer.cornerRadius = dotSize / 2
    myButton.layer.borderWidth  = 1
    myButton.layer.borderColor  = UIColor.black.cgColor
    myButton.clipsToBounds      = true
    myButton.titleLabel!.font   =  UIFont(name: "HelveticaNeue-Thin", size: dotSize/2)
    myButton.setTitleColor(UIColor.red, for: .normal)
    myButton.setTitle(String(index), for: .normal)
    myButton.tag                = index;
    myButton.sendActions(for: .touchUpInside)
    myButton.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
    view.addSubview(myButton)
}

func buttonAction(myButton: UIButton) {
    let sender:UIButton = myButton
    print("Button \(sender.tag) works")
    }
}

I am still in the process of learning Swift so it doesn’t matter at this stage whether the solution uses a viewcontroller or a subclass of UIView so long as I can arrange a circle of UIButtons that still work after I configure them using autolayout. Every suggestion is welcome. Thanks.

SOLUTION

The message that appeared in Xcode’s debug area - and which I used in the subject line of this post - was clearly not the issue. Thanks to Rob Mayoff, NSLayoutConstraint now computes the dimensions and position of each button whereas these were computed prior to run-time in my original code. His solution along with several other improvements are now reflected in the code below. To this I added the original action-target for the buttons. These not only work but remain locked to the centre of the view whenever the device orientation changes.

The code can easily be made to work for a different size configuration by changing values for radius, buttonCount and buttonSideLength (see table).

Here is the code

import UIKit

class ViewController: UIViewController {

override func viewDidLoad() {
    super.viewDidLoad()
    createUberCircle()
    createButtons()
    }

override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .all }

private let radius: CGFloat = 85
private let buttonCount = 5
private let buttonSideLength: CGFloat = 100

private func createUberCircle() {
    let circle = ShapeView()
    circle.translatesAutoresizingMaskIntoConstraints = false
    circle.shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: -radius, y: -radius, width: 2*radius, height: 2*radius)).cgPath
    if buttonCount < 10 {
        circle.shapeLayer.fillColor = UIColor.clear.cgColor
    } else {
        circle.shapeLayer.fillColor = UIColor.cyan.cgColor
        }
    view.addSubview(circle)
    circle.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    circle.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }

private func createButtons() {
    for i in 1 ... buttonCount {
        createButton(number: i)
        }
    }

private func createButton(number: Int) {
    let button = UIButton(type: .custom)
    button.translatesAutoresizingMaskIntoConstraints = false
    button.backgroundColor = .white
    button.layer.cornerRadius = buttonSideLength / 2
    button.layer.borderWidth = 1
    button.layer.borderColor = UIColor.black.cgColor
    button.clipsToBounds = true
    button.titleLabel!.font = UIFont.systemFont(ofSize: buttonSideLength / 2)
    if buttonCount > 25 {
        button.setTitleColor(.clear, for: .normal)
    } else {
        button.setTitleColor(.red, for: .normal)
        }
    button.setTitle(String(number), for: .normal)
    button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
    button.tag = number

    view.addSubview(button)

    let radians = 2 * CGFloat.pi * CGFloat(number) / CGFloat(buttonCount) - CGFloat.pi / 2
    let xOffset = radius * cos(radians)
    let yOffset = radius * sin(radians)
    NSLayoutConstraint.activate([
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: xOffset),
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: yOffset),
        button.widthAnchor.constraint(equalToConstant: buttonSideLength),
        button.heightAnchor.constraint(equalToConstant: buttonSideLength)
        ])
        }

func buttonAction(myButton: UIButton) {
    let sender:UIButton = myButton
    print("Button \(sender.tag) works")
        }

    }


class ShapeView: UIView {
    override class var layerClass: Swift.AnyClass { return CAShapeLayer.self }
    lazy var shapeLayer: CAShapeLayer = { self.layer as! CAShapeLayer }()
    }

回答1:

Don't worry about the fences warning message. It seems to be harmless and not caused by anything you're doing.

There are several problems with the code you posted:

  1. You create myView and constrain its center, but you don't give it any size constraints. Furthermore, myView is a local variable and you don't add any subviews to myView. So myView is an invisible, sizeless view with no contents. Why are you creating it at all?

  2. You're drawing your “uberCircle” using a bare shape layer. By “bare”, I mean there's no view whose layer property is that layer. Bare layers don't participate in autolayout.

  3. You compute the position of each button based on the center of the top-level view's bounds. You're doing this during viewDidLoad, but viewDidLoad is called before the top-level view has been resized to fit the current device. So your wheel won't even be centered at launch on some devices.

  4. You don't set any constraints on the buttons, or set their autoresizing masks. The result is that, when the device rotates, the top-level view resizes but each button's position (relative to the top-left corner of the top-level view) stays the same.

  5. Turning on the “Upside Down” checkbox is not sufficient to allow upside-down orientation on iPhones, only on iPads.

Here are the changes you need to make:

  1. Use a view to draw the “uberCircle”. If you want to use a shape layer, make a subclass of UIView that uses a CAShapeLayer for its layer. You can copy the ShapeView class from this answer.

  2. Set constraints from the center of the uberCircle to the center of the top-level view, to keep the uberCircle centered when the top-level view changes size.

  3. For each button, set constraints from the center of the button to the center of the top-level view, to keep the button positioned properly when the top-level view changes size. These constraints need non-zero constants to offset the buttons from the center.

  4. Override supportedInterfaceOrientations to enable upside-down orientation (in addition to checking the “Upside Down” checkbox).

  5. Get rid of myView in viewDidLoad. You don't need it.

  6. Get rid of the centre property. You don't need it.

Thus:

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        createUberCircle()
        createButtons()
    }

    override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .all }

    private let radius: CGFloat = 96
    private let buttonCount = 10
    private let buttonSideLength: CGFloat = 60

    private func createUberCircle() {
        let circle = ShapeView()
        circle.translatesAutoresizingMaskIntoConstraints = false
        circle.shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: -radius, y: -radius, width: 2*radius, height: 2*radius)).cgPath
        circle.shapeLayer.fillColor = UIColor.cyan.cgColor
        view.addSubview(circle)
        circle.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        circle.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }

    private func createButtons() {
        for i in 1 ... buttonCount {
            createButton(number: i)
        }
    }

    private func createButton(number: Int) {
        let button = UIButton(type: .custom)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.backgroundColor = .white
        button.layer.cornerRadius = buttonSideLength / 2
        button.layer.borderWidth = 1
        button.layer.borderColor = UIColor.black.cgColor
        button.clipsToBounds = true
        button.titleLabel!.font = UIFont.systemFont(ofSize: buttonSideLength / 2)
        button.setTitleColor(.red, for: .normal)
        button.setTitle(String(number), for: .normal)
        view.addSubview(button)

        let radians = 2 * CGFloat.pi * CGFloat(number) / CGFloat(buttonCount) - CGFloat.pi / 2
        let xOffset = radius * cos(radians)
        let yOffset = radius * sin(radians)
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: xOffset),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: yOffset),
            button.widthAnchor.constraint(equalToConstant: buttonSideLength),
            button.heightAnchor.constraint(equalToConstant: buttonSideLength)
            ])
    }

}

class ShapeView: UIView {

    override class var layerClass: Swift.AnyClass { return CAShapeLayer.self }

    lazy var shapeLayer: CAShapeLayer = { self.layer as! CAShapeLayer }()

}


回答2:

I'd just constrain the parent UIView and constrain all of the buttons to the center of the parent view and override layoutSubviews. In layout subviews you can manipulate .constant of the centerX and centerY constraints for the buttons to reposition them. Alternatively you can just center all of them and use the .transform property of each button to move them into place.