让自动布局与 UIControl 子类一起工作
Getting autolayout to work with a UIControl subclass
我已经能够基本上按照我想要的方式获得我的自定义控件,但我希望它能与自动版式一起使用。我做了很多尝试和错误,但没有找到任何地方。
当我设置框架时控件按预期布局。
circleContol = CircleControl(size: 80, title: "My Button", circleStrokeSize: 4, image: testImage, imageColour: .yellow, circleColour: .red, textColour: .red)
circleContol.center = CGPoint(x: 150, y: 150)
view.addSubview(circleContol)
但是,我正在努力让控件与 Autolayout 配合使用。
即使用
NSLayoutConstraint.activate([
circleContol.centerYAnchor.constraint(equalTo: view.centerYAnchor),
circleContol.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
结果
我试过尝试在我的 UIControl
子类中设置自动版式,但无法正确设置。例如,我尝试了以下方法,但我不确定这是否是正确的方法,或者是设置此约束的最佳位置 - 特别是圆形 shapeLayer 似乎是另一个问题。
override func updateConstraints() {
NSLayoutConstraint.activate([
imageOverlay.centerYAnchor.constraint(equalTo: centerYAnchor),
imageOverlay.centerXAnchor.constraint(equalTo: centerXAnchor),
imageOverlay.heightAnchor.constraint(equalToConstant: radius),
imageOverlay.widthAnchor.constraint(equalToConstant: radius),
])
super.updateConstraints()
}
这里是playground liveView代码,希望有人能提供一些指导。它有很多代码,但我觉得这是让别人看到问题的最佳方式
谢谢
上面的每个例子都可以在playGround中看到使用这些方法
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 1. Lays out as expected
createControl()
// 2. Views all out of place
// createUsingAutoLayout()
}
PlayGround 代码
import UIKit
import PlaygroundSupport
class CircleControl: UIControl {
private var shaperLayer: CAShapeLayer!
private var stroke: CGFloat!
private var padding: CGFloat!
private var circleColour: UIColor!
private var title: String!
private var imageColour: UIColor!
private var textColour: UIColor!
private var imageOverlay: UIImageView!
private let fontSize: CGFloat = 20
private var actualRadius: CGFloat {
let halfSquare: CGFloat = max(bounds.size.width, bounds.size.height) / 2
let temp: CGFloat = halfSquare * halfSquare + halfSquare * halfSquare
let radius: CGFloat = temp.squareRoot()
return radius
}
private var radius: CGFloat {
return actualRadius + padding + (stroke * 0.5)
}
private var textRadius: CGFloat {
return actualRadius + padding + stroke + (fontSize * 0.5)
}
init(size: Int, title: String, circleStrokeSize: CGFloat, circlePadding: CGFloat = 0, image: UIImage, imageColour: UIColor, circleColour: UIColor, textColour: UIColor) {
let f = CGRect(x: 0, y: 0, width: size, height: size)
self.stroke = circleStrokeSize
self.padding = circlePadding
self.circleColour = circleColour
self.imageColour = imageColour
self.textColour = textColour
self.title = title
super.init(frame: f)
// add subviews
updateView()
// add image to button
let tinted = image.withRenderingMode(.alwaysTemplate)
imageOverlay.image = tinted
}
private func addTextToView() {
let label = CurvedTextView(frame: CGRect(x: 0, y: 0, width: 300, height: 300), title: title, r: textRadius, fontSize: fontSize, fontColour: textColour, location: .bottom)
label.center = imageOverlay.center
label.isUserInteractionEnabled = false
label.backgroundColor = UIColor.clear
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
func updateView() {
addImageOverlay()
addCircle()
addTextToView()
}
func addImageOverlay() {
imageOverlay = UIImageView(frame: bounds)
imageOverlay.tintColor = imageColour
imageOverlay.contentMode = .scaleAspectFill
addSubview(imageOverlay)
imageOverlay.isUserInteractionEnabled = false
imageOverlay.translatesAutoresizingMaskIntoConstraints = false
}
func addCircle() {
if let s = shaperLayer {
s.removeFromSuperlayer()
}
shaperLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: UIView(frame: bounds).center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
shaperLayer.path = circularPath.cgPath
shaperLayer.strokeColor = circleColour.cgColor
shaperLayer.fillColor = UIColor.clear.cgColor
shaperLayer.lineWidth = stroke
layer.addSublayer(shaperLayer)
}
}
class CurvedTextView: UIView {
var radius: CGFloat!
var fontSize: CGFloat!
var fontColour: UIColor!
var labelPost: LabelPostition = .bottom
var title: String!
init(frame: CGRect, title: String, r: CGFloat, fontSize: CGFloat, fontColour: UIColor, location: LabelPostition) {
self.radius = r
self.fontSize = fontSize
self.fontColour = fontColour
self.labelPost = location
self.title = title
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
enum LabelPostition {
case bottom
}
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
let characters: [String] = str.map { String([=14=]) }
let l = characters.count
let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font, NSAttributedString.Key.kern: 0.8]
var arcs: [CGFloat] = []
var totalArc: CGFloat = 0
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
thetaI += direction * arcs[i] / 2
}
}
func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
let attributes = [NSAttributedString.Key.foregroundColor: c, NSAttributedString.Key.font: font]
context.saveGState()
context.scaleBy(x: 1, y: -1)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
context.rotate(by: -slantAngle)
let offset = str.size(withAttributes: attributes)
context.translateBy (x: -offset.width / 2, y: -offset.height / 2)
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
context.restoreGState()
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
return 2 * asin(chord / (2 * radius))
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let size = self.bounds.size
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy (x: 1, y: -1)
let f = UIFont.systemFont(ofSize: fontSize, weight: UIFont.Weight.semibold)
var startAngle: CGFloat = .pi
var clock = true
switch labelPost {
case .bottom:
startAngle = -(.pi / 2)
clock = false
}
centreArcPerpendicular(text: title, context: context, radius: radius, angle: startAngle, colour: fontColour, font: f, clockwise: clock)
}
}
class MyViewController : UIViewController {
var circleContol: CircleControl!
private let testImage = UIImage(named: "faceLike.png")!
override func loadView() {
let view = UIView()
view.backgroundColor = .lightGray
self.view = view
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 1. Lays out as expected
createControl()
// 2. Views all out of place
// createUsingAutoLayout()
}
func createControl() {
circleContol = CircleControl(size: 80, title: "My Button", circleStrokeSize: 4, image: testImage, imageColour: .yellow, circleColour: .red, textColour: .red)
circleContol.center = CGPoint(x: 150, y: 150)
view.addSubview(circleContol)
}
func createUsingAutoLayout() {
circleContol = CircleControl(size: 150, title: "My Button", circleStrokeSize: 4, image: testImage, imageColour: .yellow, circleColour: .red, textColour: .red)
view.addSubview(circleContol)
circleContol.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
circleContol.centerYAnchor.constraint(equalTo: view.centerYAnchor),
circleContol.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
}
PlaygroundPage.current.liveView = MyViewController()
我不能 运行 你的游乐场,但我可能有一些想法。
在您的组件中设置带有框架的元素的位置,这就是为什么 autoLayout 不会影响它,您应该添加约束。
而且你不应该在 updateConstraints 方法中设置你的约束,约束只需要设置一次,autoLayout 就可以工作。
尝试编辑:
private func addTextToView() {
let label = CurvedTextView(frame: CGRect(x: 0, y: 0, width: 300, height: 300), title: title, r: textRadius, fontSize: fontSize, fontColour: textColour, location: .bottom)
label.isUserInteractionEnabled = false
label.backgroundColor = UIColor.clear
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: topAnchor),
label.bottomAnchor.constraint(equalTo: bottomAnchor),
label.trailingAnchor.constraint(equalTo: trailingAnchor),
label.leadingAnchor.constraint(equalTo: leadingAnchor)
])
}
func addImageOverlay() {
imageOverlay = UIImageView(frame: bounds)
imageOverlay.tintColor = imageColour
imageOverlay.contentMode = .scaleAspectFill
addSubview(imageOverlay)
imageOverlay.isUserInteractionEnabled = false
imageOverlay.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageOverlay.topAnchor.constraint(equalTo: topAnchor),
imageOverlay.bottomAnchor.constraint(equalTo: bottomAnchor),
imageOverlay.trailingAnchor.constraint(equalTo: trailingAnchor),
imageOverlay.leadingAnchor.constraint(equalTo: leadingAnchor)
])
}
希望对您有所帮助
最终解决方案很简单
我需要做的就是确保 imageView 位于正确的位置。弯曲的文本和圆圈基于边界大小(等于 imageView 大小)。因此,一旦 imageView 位于其正确位置,圆圈和绘制的文本都会自动修复。
func addImageOverlay() {
imageOverlay = UIImageView(frame: bounds)
imageOverlay.tintColor = imageColour
imageOverlay.contentMode = .scaleAspectFill
addSubview(imageOverlay)
imageOverlay.isUserInteractionEnabled = false
imageOverlay.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageOverlay.topAnchor.constraint(equalTo: topAnchor),
imageOverlay.bottomAnchor.constraint(equalTo: bottomAnchor),
imageOverlay.trailingAnchor.constraint(equalTo: trailingAnchor),
imageOverlay.leadingAnchor.constraint(equalTo: leadingAnchor),
imageOverlay.heightAnchor.constraint(equalToConstant: bounds.size.width),
imageOverlay.widthAnchor.constraint(equalToConstant: bounds.size.width)
])
}
我已经能够基本上按照我想要的方式获得我的自定义控件,但我希望它能与自动版式一起使用。我做了很多尝试和错误,但没有找到任何地方。
当我设置框架时控件按预期布局。
circleContol = CircleControl(size: 80, title: "My Button", circleStrokeSize: 4, image: testImage, imageColour: .yellow, circleColour: .red, textColour: .red)
circleContol.center = CGPoint(x: 150, y: 150)
view.addSubview(circleContol)
但是,我正在努力让控件与 Autolayout 配合使用。
即使用
NSLayoutConstraint.activate([
circleContol.centerYAnchor.constraint(equalTo: view.centerYAnchor),
circleContol.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
结果
我试过尝试在我的 UIControl
子类中设置自动版式,但无法正确设置。例如,我尝试了以下方法,但我不确定这是否是正确的方法,或者是设置此约束的最佳位置 - 特别是圆形 shapeLayer 似乎是另一个问题。
override func updateConstraints() {
NSLayoutConstraint.activate([
imageOverlay.centerYAnchor.constraint(equalTo: centerYAnchor),
imageOverlay.centerXAnchor.constraint(equalTo: centerXAnchor),
imageOverlay.heightAnchor.constraint(equalToConstant: radius),
imageOverlay.widthAnchor.constraint(equalToConstant: radius),
])
super.updateConstraints()
}
这里是playground liveView代码,希望有人能提供一些指导。它有很多代码,但我觉得这是让别人看到问题的最佳方式
谢谢
上面的每个例子都可以在playGround中看到使用这些方法
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 1. Lays out as expected
createControl()
// 2. Views all out of place
// createUsingAutoLayout()
}
PlayGround 代码
import UIKit
import PlaygroundSupport
class CircleControl: UIControl {
private var shaperLayer: CAShapeLayer!
private var stroke: CGFloat!
private var padding: CGFloat!
private var circleColour: UIColor!
private var title: String!
private var imageColour: UIColor!
private var textColour: UIColor!
private var imageOverlay: UIImageView!
private let fontSize: CGFloat = 20
private var actualRadius: CGFloat {
let halfSquare: CGFloat = max(bounds.size.width, bounds.size.height) / 2
let temp: CGFloat = halfSquare * halfSquare + halfSquare * halfSquare
let radius: CGFloat = temp.squareRoot()
return radius
}
private var radius: CGFloat {
return actualRadius + padding + (stroke * 0.5)
}
private var textRadius: CGFloat {
return actualRadius + padding + stroke + (fontSize * 0.5)
}
init(size: Int, title: String, circleStrokeSize: CGFloat, circlePadding: CGFloat = 0, image: UIImage, imageColour: UIColor, circleColour: UIColor, textColour: UIColor) {
let f = CGRect(x: 0, y: 0, width: size, height: size)
self.stroke = circleStrokeSize
self.padding = circlePadding
self.circleColour = circleColour
self.imageColour = imageColour
self.textColour = textColour
self.title = title
super.init(frame: f)
// add subviews
updateView()
// add image to button
let tinted = image.withRenderingMode(.alwaysTemplate)
imageOverlay.image = tinted
}
private func addTextToView() {
let label = CurvedTextView(frame: CGRect(x: 0, y: 0, width: 300, height: 300), title: title, r: textRadius, fontSize: fontSize, fontColour: textColour, location: .bottom)
label.center = imageOverlay.center
label.isUserInteractionEnabled = false
label.backgroundColor = UIColor.clear
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
func updateView() {
addImageOverlay()
addCircle()
addTextToView()
}
func addImageOverlay() {
imageOverlay = UIImageView(frame: bounds)
imageOverlay.tintColor = imageColour
imageOverlay.contentMode = .scaleAspectFill
addSubview(imageOverlay)
imageOverlay.isUserInteractionEnabled = false
imageOverlay.translatesAutoresizingMaskIntoConstraints = false
}
func addCircle() {
if let s = shaperLayer {
s.removeFromSuperlayer()
}
shaperLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: UIView(frame: bounds).center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
shaperLayer.path = circularPath.cgPath
shaperLayer.strokeColor = circleColour.cgColor
shaperLayer.fillColor = UIColor.clear.cgColor
shaperLayer.lineWidth = stroke
layer.addSublayer(shaperLayer)
}
}
class CurvedTextView: UIView {
var radius: CGFloat!
var fontSize: CGFloat!
var fontColour: UIColor!
var labelPost: LabelPostition = .bottom
var title: String!
init(frame: CGRect, title: String, r: CGFloat, fontSize: CGFloat, fontColour: UIColor, location: LabelPostition) {
self.radius = r
self.fontSize = fontSize
self.fontColour = fontColour
self.labelPost = location
self.title = title
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
enum LabelPostition {
case bottom
}
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
let characters: [String] = str.map { String([=14=]) }
let l = characters.count
let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font, NSAttributedString.Key.kern: 0.8]
var arcs: [CGFloat] = []
var totalArc: CGFloat = 0
for i in 0 ..< l {
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
totalArc += arcs[i]
}
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2
var thetaI = theta - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
thetaI += direction * arcs[i] / 2
}
}
func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
let attributes = [NSAttributedString.Key.foregroundColor: c, NSAttributedString.Key.font: font]
context.saveGState()
context.scaleBy(x: 1, y: -1)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
context.rotate(by: -slantAngle)
let offset = str.size(withAttributes: attributes)
context.translateBy (x: -offset.width / 2, y: -offset.height / 2)
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
context.restoreGState()
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
return 2 * asin(chord / (2 * radius))
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let size = self.bounds.size
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy (x: 1, y: -1)
let f = UIFont.systemFont(ofSize: fontSize, weight: UIFont.Weight.semibold)
var startAngle: CGFloat = .pi
var clock = true
switch labelPost {
case .bottom:
startAngle = -(.pi / 2)
clock = false
}
centreArcPerpendicular(text: title, context: context, radius: radius, angle: startAngle, colour: fontColour, font: f, clockwise: clock)
}
}
class MyViewController : UIViewController {
var circleContol: CircleControl!
private let testImage = UIImage(named: "faceLike.png")!
override func loadView() {
let view = UIView()
view.backgroundColor = .lightGray
self.view = view
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 1. Lays out as expected
createControl()
// 2. Views all out of place
// createUsingAutoLayout()
}
func createControl() {
circleContol = CircleControl(size: 80, title: "My Button", circleStrokeSize: 4, image: testImage, imageColour: .yellow, circleColour: .red, textColour: .red)
circleContol.center = CGPoint(x: 150, y: 150)
view.addSubview(circleContol)
}
func createUsingAutoLayout() {
circleContol = CircleControl(size: 150, title: "My Button", circleStrokeSize: 4, image: testImage, imageColour: .yellow, circleColour: .red, textColour: .red)
view.addSubview(circleContol)
circleContol.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
circleContol.centerYAnchor.constraint(equalTo: view.centerYAnchor),
circleContol.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
}
PlaygroundPage.current.liveView = MyViewController()
我不能 运行 你的游乐场,但我可能有一些想法。
在您的组件中设置带有框架的元素的位置,这就是为什么 autoLayout 不会影响它,您应该添加约束。
而且你不应该在 updateConstraints 方法中设置你的约束,约束只需要设置一次,autoLayout 就可以工作。
尝试编辑:
private func addTextToView() {
let label = CurvedTextView(frame: CGRect(x: 0, y: 0, width: 300, height: 300), title: title, r: textRadius, fontSize: fontSize, fontColour: textColour, location: .bottom)
label.isUserInteractionEnabled = false
label.backgroundColor = UIColor.clear
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: topAnchor),
label.bottomAnchor.constraint(equalTo: bottomAnchor),
label.trailingAnchor.constraint(equalTo: trailingAnchor),
label.leadingAnchor.constraint(equalTo: leadingAnchor)
])
}
func addImageOverlay() {
imageOverlay = UIImageView(frame: bounds)
imageOverlay.tintColor = imageColour
imageOverlay.contentMode = .scaleAspectFill
addSubview(imageOverlay)
imageOverlay.isUserInteractionEnabled = false
imageOverlay.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageOverlay.topAnchor.constraint(equalTo: topAnchor),
imageOverlay.bottomAnchor.constraint(equalTo: bottomAnchor),
imageOverlay.trailingAnchor.constraint(equalTo: trailingAnchor),
imageOverlay.leadingAnchor.constraint(equalTo: leadingAnchor)
])
}
希望对您有所帮助
最终解决方案很简单
我需要做的就是确保 imageView 位于正确的位置。弯曲的文本和圆圈基于边界大小(等于 imageView 大小)。因此,一旦 imageView 位于其正确位置,圆圈和绘制的文本都会自动修复。
func addImageOverlay() {
imageOverlay = UIImageView(frame: bounds)
imageOverlay.tintColor = imageColour
imageOverlay.contentMode = .scaleAspectFill
addSubview(imageOverlay)
imageOverlay.isUserInteractionEnabled = false
imageOverlay.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageOverlay.topAnchor.constraint(equalTo: topAnchor),
imageOverlay.bottomAnchor.constraint(equalTo: bottomAnchor),
imageOverlay.trailingAnchor.constraint(equalTo: trailingAnchor),
imageOverlay.leadingAnchor.constraint(equalTo: leadingAnchor),
imageOverlay.heightAnchor.constraint(equalToConstant: bounds.size.width),
imageOverlay.widthAnchor.constraint(equalToConstant: bounds.size.width)
])
}