I have been able to get my custom control basically the way I want it, but I'd like it to work with autolayout. I've done a lot of trail and error but getting no where.
The control lays out as expected when I set a frame.
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)
However, I'm struggling to get the control to play nice with Autolayout.
i.e using something like
NSLayoutConstraint.activate([
circleContol.centerYAnchor.constraint(equalTo: view.centerYAnchor),
circleContol.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
results in
I've played around with trying to set up autolayout in my the UIControl
subclass, but unable to get it right. For example, I've tried the following, but i'm not sure if this is the correct approach, or best place to set this constraints - especially with the circle shapeLayer which seems to be a whole other issue.
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()
}
Here is the playground liveView code, hopefully someone can provide some guidance. Its a lot of code, but I felt this was the best way for someone to see the issues
Thanks
Each example above can seen using these methods in the playGround
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 1. Lays out as expected
createControl()
// 2. Views all out of place
// createUsingAutoLayout()
}
PlayGround Code
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($0) }
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()
I can't run your playground but I may have some ideas.
In your component set the position of your element with frames, that's why autoLayout will not affect it, you should add constraints.
And you shouldn't set your contraints in updateConstraints method, constraints have to be set only one time for autoLayout to work.
Try editing:
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)
])
}
Hope it helps
The solution was trivial in the end
All I needed to do ensure the imageView was in the correct location. The curved text and circle is based on the bounds size (which is equal to the imageView size). So, both the circle and the drawn text were automatically were fixed once imageView was in its rightful place.
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)
])
}