How to Address the Current View vs Superview confusion (Swift) - ios

Using superview in the class paints the entire screen but I only want to paint the smaller subview (whose dimensions are defined by v.frame).
How do I have the class operate only on the "current view"?
#IBOutlet var storyboardView: UIView!
func selectView() {
var v = UIView()
switch viewchoice {
case false:
v = View1class()
case true:
v = View2class()
}
v.frame = CGRect(x: 20, y: 50, width: storyboardView.frame.width - 40, height: storyboardView.frame.height - 100)
storyboardView.addSubview(v)
}
class View1class: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
superview!.backgroundColor = .red
}
}
class View2class: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
superview!.backgroundColor = .blue
}
}
I tried using self instead of superview but that doesn't work.
EDIT: If I wanted to paint the entire screen, could I skip creating the subview and assign the class to storyboardView?

If you override draw(_ rect: CGRect) in a subclass, it is up to you to draw the view content. Setting .backgroundColor there will have no effect.
This is a common structure for a UIView subclass:
class View1Class: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.backgroundColor = .red
// do any other view setup here, such as
// adding subviews, adding sublayers, etc
}
}
Edit
Here is an example of two custom views - the first one uses shape layers, the second one overrides draw():
MyFirstView class
class MyFirstView: UIView {
let boxLayer = CAShapeLayer()
let circleLayer = CAShapeLayer()
let lineLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.backgroundColor = .systemYellow
layer.addSublayer(boxLayer)
layer.addSublayer(circleLayer)
layer.addSublayer(lineLayer)
boxLayer.fillColor = UIColor.red.cgColor
circleLayer.fillColor = UIColor.blue.cgColor
lineLayer.strokeColor = UIColor.green.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
// centered rectangle, half height and width
let halfWidth: CGFloat = bounds.width * 0.5
let halfHeight: CGFloat = bounds.height * 0.5
let boxRect: CGRect = CGRect(x: halfWidth * 0.5, y: halfHeight * 0.5, width: halfWidth, height: halfHeight)
let boxPath: UIBezierPath = UIBezierPath(rect: boxRect)
boxLayer.path = boxPath.cgPath
// circle centered in box, 3/4ths of the shorter of width or height
let wh: CGFloat = min(boxRect.width, boxRect.height) * 0.75
let circleRect: CGRect = CGRect(x: boxRect.midX - wh * 0.5, y: boxRect.midY - wh * 0.5, width: wh, height: wh)
let circlePath: UIBezierPath = UIBezierPath(ovalIn: circleRect)
circleLayer.path = circlePath.cgPath
// diagonal line from top-left to bottom-right
let linePath: UIBezierPath = UIBezierPath()
linePath.move(to: .zero)
linePath.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
lineLayer.path = linePath.cgPath
}
}
MySecondView class
class MySecondView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.backgroundColor = .systemTeal
}
override func draw(_ rect: CGRect) {
// centered rectangle, half height and width
let halfWidth: CGFloat = rect.width * 0.5
let halfHeight: CGFloat = rect.height * 0.5
let boxRect: CGRect = CGRect(x: halfWidth * 0.5, y: halfHeight * 0.5, width: halfWidth, height: halfHeight)
let boxPath: UIBezierPath = UIBezierPath(rect: boxRect)
UIColor.red.setFill()
boxPath.fill()
// circle centered in box, 3/4ths of the shorter of width or height
let wh: CGFloat = min(boxRect.width, boxRect.height) * 0.75
let circleRect: CGRect = CGRect(x: boxRect.midX - wh * 0.5, y: boxRect.midY - wh * 0.5, width: wh, height: wh)
let circlePath: UIBezierPath = UIBezierPath(ovalIn: circleRect)
UIColor.blue.setFill()
circlePath.fill()
// diagonal line from top-left to bottom-right
let linePath: UIBezierPath = UIBezierPath()
linePath.move(to: .zero)
linePath.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
UIColor.green.setStroke()
linePath.stroke()
}
override func layoutSubviews() {
setNeedsDisplay()
}
}
Example view controller
class CustomViewsViewController: UIViewController {
let firstView = MyFirstView()
let secondView = MySecondView()
override func viewDidLoad() {
super.viewDidLoad()
[firstView, secondView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// firstView Top / Leading / Trailing to view (safe-area)
firstView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
firstView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
firstView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// firstView Height
firstView.heightAnchor.constraint(equalToConstant: 150.0),
// secondView Leading / Trailing to view (safe-area)
secondView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
secondView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
// secondView Top 20-pts from firstView Bottom
secondView.topAnchor.constraint(equalTo: firstView.bottomAnchor, constant: 20.0),
// secondView Bottom to View Bottom (safe-area)
secondView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
}
}
Output - first view has .systemYellow background, second view has .systemTeal background:
and rotated:

Use self instead of superView.
self.backgroundColor = .red
       

Related

Mask a gradient layer with the intersection of two shape layers in Swift

I have two shapes of type CAShapeLayer (e.g. one box and a circle) and a gradient layer of type CAGradientLayer. How can I mask the gradient layer with the intersection of the two shapes like this picture in Swift?
Not exactly clear what you mean by "intersection of the two shapes" ... but maybe this is what you're going for:
To get that, we can create a CAShapeLayer with an oval (round) path, and use it as a mask on the gradient layer.
Here's some example code:
class GradientMaskingViewController: UIViewController {
let gradView = MaskedGradView()
override func viewDidLoad() {
super.viewDidLoad()
gradView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(gradView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
gradView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor),
gradView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
gradView.colorArray = [
.blue, .orange, .purple, .yellow
]
}
}
class MaskedGradView: UIView {
enum Direction {
case horizontal, vertical, diagnal
}
public var colorArray: [UIColor] = [] {
didSet {
setNeedsLayout()
}
}
public var locationsArray: [NSNumber] = [] {
didSet {
setNeedsLayout()
}
}
public var direction: Direction = .vertical {
didSet {
setNeedsLayout()
}
}
private let gLayer = CAGradientLayer()
private let maskLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// add gradient layer as a sublayer
layer.addSublayer(gLayer)
// mask it
gLayer.mask = maskLayer
// we'll use a 120-point diameter circle for the mask
maskLayer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 120.0, height: 120.0)).cgPath
// so we can see this view's frame
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
// update gradient layer
// frame
// colors
// locations
gLayer.frame = bounds
gLayer.colors = colorArray.map({ $0.cgColor })
if locationsArray.count > 0 {
gLayer.locations = locationsArray
}
switch direction {
case .horizontal:
gLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
case .vertical:
gLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
case .diagnal:
gLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
}
}
// touch code to drag the circular mask around
private var curPos: CGPoint = .zero
private var lPos: CGPoint = .zero
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
curPos = touch.location(in: self)
lPos = maskLayer.position
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let newPos = touch.location(in: self)
let diffX = newPos.x - curPos.x
let diffY = newPos.y - curPos.y
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.position = CGPoint(x: lPos.x + diffX, y: lPos.y + diffY)
CATransaction.commit() }
}
To help make it clear, I added touch handling code so you can drag the circle around inside the view:
Edit - after comment (but still missing details), let's try this again...
We can get the desired output by using multiple layers.
a white-filled gray-bordered rectangle CAShapeLayer
a white-filled NON-bordered oval CAShapeLayer
a CAGradientLayer masked with an oval CAShapeLayer
a NON-filled gray-bordered oval CAShapeLayer
So, we start with a view:
add a white-filled gray-bordered rectangle CAShapeLayer:
add a white-filled NON-bordered oval CAShapeLayer (red first, to show it clearly):
add a CAGradientLayer:
mask it with an oval CAShapeLayer:
finally, add a NON-filled gray-bordered oval CAShapeLayer:
Here's the example code:
class GradientMaskingViewController: UIViewController {
let gradView = MultiLayeredGradView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
gradView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(gradView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradView.widthAnchor.constraint(equalToConstant: 240.0),
gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor),
gradView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
gradView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}
class MultiLayeredGradView: UIView {
private let rectLayer = CAShapeLayer()
private let filledCircleLayer = CAShapeLayer()
private let gradLayer = CAGradientLayer()
private let maskLayer = CAShapeLayer()
private let outlineCircleLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// add filled-bordered rect layer as a sublayer
layer.addSublayer(rectLayer)
// add filled circle layer as a sublayer
layer.addSublayer(filledCircleLayer)
// add gradient layer as a sublayer
layer.addSublayer(gradLayer)
// mask it
gradLayer.mask = maskLayer
// add outline circle layer as a sublayer
layer.addSublayer(outlineCircleLayer)
let bColor: CGColor = UIColor.gray.cgColor
let fColor: CGColor = UIColor.white.cgColor
// filled-outlined
rectLayer.strokeColor = bColor
rectLayer.fillColor = fColor
rectLayer.lineWidth = 2
// filled
filledCircleLayer.fillColor = fColor
// clear-outlined
outlineCircleLayer.strokeColor = bColor
outlineCircleLayer.fillColor = UIColor.clear.cgColor
outlineCircleLayer.lineWidth = 2
// gradient layer properties
let colorArray: [UIColor] = [
.blue, .orange, .purple, .yellow
]
gradLayer.colors = colorArray.map({ $0.cgColor })
gradLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gradLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
}
override func layoutSubviews() {
super.layoutSubviews()
// circle diameter is 45% of the width of the view
let circleDiameter: CGFloat = bounds.width * 0.45
// circle Top is at vertical midpoint
// circle is moved Left by 25% of the circle diameter
let circleBounds: CGRect = CGRect(x: bounds.minX - circleDiameter * 0.25,
y: bounds.maxY * 0.5,
width: circleDiameter,
height: circleDiameter)
// gradient layer fills the bounds
gradLayer.frame = bounds
let rectPath = UIBezierPath(rect: bounds).cgPath
rectLayer.path = rectPath
let circlePath = UIBezierPath(ovalIn: circleBounds).cgPath
filledCircleLayer.path = circlePath
outlineCircleLayer.path = circlePath
maskLayer.path = circlePath
}
}

iOS animate drawing of multiple circles, only one displays

Based on the answer to this question: Animate drawing of a circle
I now want to display two of these circles simultaneously on the same screen but in two different views. If I just want to animate one circle there are no problems. However if I try to add a second, only the second one is visible.
This is the Circle class:
import UIKit
var circleLayer: CAShapeLayer!
class Circle: UIView {
init(frame: CGRect, viewLayer: CALayer) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.green.cgColor
circleLayer.lineWidth = 8.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
viewLayer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e The speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// Right value when the animation ends
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
func removeCircle() {
circleLayer.strokeEnd = 0.0
}
}
And here is how I call it from my ViewController:
var rythmTimer: Circle?
var adrenalineTimer: Circle?
override func viewDidLoad() {
// Create two timers as circles
self.rythmTimer = Circle(frame: CGRect(x: 0, y: 0, width: 100, height: 100), viewLayer: view1.layer)
if let rt = rythmTimer {
view1.addSubview(rt)
rt.center = CGPoint(x: self.view1.bounds.midX, y: self.view1.bounds.midY);
}
self.adrenalineTimer = Circle(frame: CGRect(x: 0, y: 0, width: 100, height: 100), viewLayer: view2.layer)
if let at = adrenalineTimer {
view2.addSubview(at)
at.center = CGPoint(x: self.view2.bounds.midX, y: self.view2.bounds.midY)
}
}
If I remove the code for the adrenalineTimer I can see the circle drawn by the rythmTimer. If I keep it the rythmTimer will be displayed in view2 instead of view1 and will have the duration/color of the rythmTimer
It seems you set the circle path with wrong origin points. In addition, you do it before you place the circle on the view.
Add this code to the animate function instead:
func animateCircle(duration: TimeInterval) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: self.frame.origin.x + self.frame.size.width / 2, y: self.frame.origin.y + self.frame.size.height / 2), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)
circleLayer.path = circlePath.cgPath
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
...
The main problem is that you have declared circleLayer outside of the class:
import UIKit
var circleLayer: CAShapeLayer!
class Circle: UIView {
init(frame: CGRect, viewLayer: CALayer) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
...
}
}
The result is that you only ever have ONE circleLayer object (instance).
If you move it inside the class, then each instance of Circle will have its own instance of circleLayer:
import UIKit
class Circle: UIView {
var circleLayer: CAShapeLayer!
init(frame: CGRect, viewLayer: CALayer) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
...
}
}
There are a number of other odd things you're doing, but that is why you only get one animated circle.
Edit: Here's a modified version of your code that will allow you to use auto-layout instead of fixed / hard-coded sizes and positions. You can run this directly in a Playground page:
import UIKit
import PlaygroundSupport
class Circle: UIView {
var circleLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Setup the CAShapeLayer with colors and line width
circleLayer = CAShapeLayer()
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.green.cgColor
circleLayer.lineWidth = 8.0;
// We haven't set the path yet, so don't draw initially
circleLayer.strokeEnd = 0.0
// Add the layer to the self's layer
self.layer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
// this will update whenever the frame size changes (makes it easy to use with auto-layout)
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)
circleLayer.path = circlePath.cgPath
}
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e The speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// Right value when the animation ends
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
func removeCircle() {
circleLayer.strokeEnd = 0.0
}
}
class MyViewController : UIViewController {
var rythmTimer: Circle?
var adrenalineTimer: Circle?
var theButton: UIButton = {
let b = UIButton()
b.setTitle("Tap Me", for: .normal)
b.translatesAutoresizingMaskIntoConstraints = false
b.backgroundColor = .red
return b
}()
var view1: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
return v
}()
var view2: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .orange
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
view.addSubview(theButton)
// constrain button to Top: 32 and centerX
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 32.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
// add an action for the button tap
theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
view.addSubview(view1)
view.addSubview(view2)
NSLayoutConstraint.activate([
view1.widthAnchor.constraint(equalToConstant: 120.0),
view1.heightAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
view1.centerXAnchor.constraint(equalTo: view.centerXAnchor),
view1.topAnchor.constraint(equalTo: theButton.bottomAnchor, constant: 40.0),
view2.widthAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
view2.heightAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
view2.centerXAnchor.constraint(equalTo: view.centerXAnchor),
view2.topAnchor.constraint(equalTo: view1.bottomAnchor, constant: 40.0),
])
rythmTimer = Circle(frame: CGRect.zero)
adrenalineTimer = Circle(frame: CGRect.zero)
if let rt = rythmTimer,
let at = adrenalineTimer {
view1.addSubview(rt)
view2.addSubview(at)
rt.translatesAutoresizingMaskIntoConstraints = false
at.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
rt.widthAnchor.constraint(equalToConstant: 100.0),
rt.heightAnchor.constraint(equalToConstant: 100.0),
rt.centerXAnchor.constraint(equalTo: view1.centerXAnchor),
rt.centerYAnchor.constraint(equalTo: view1.centerYAnchor),
at.widthAnchor.constraint(equalToConstant: 100.0),
at.heightAnchor.constraint(equalToConstant: 100.0),
at.centerXAnchor.constraint(equalTo: view2.centerXAnchor),
at.centerYAnchor.constraint(equalTo: view2.centerYAnchor),
])
}
}
// on button tap, change the text in the label(s)
#objc func didTap(_ sender: Any?) -> Void {
if let rt = rythmTimer,
let at = adrenalineTimer {
rt.removeCircle()
at.removeCircle()
rt.animateCircle(duration: 2.0)
at.animateCircle(duration: 2.0)
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

CATransform3DMakeRotation frame

I have a custom view MyView (red) that contains one subview MySubView (blue). I want to rotate the subview around its center.
My problem is that when I setup the MySubview using autolayout, the rotation works fine:
but when I set the frame of the subview (without autolayout) the rotation distorts the view:
In the MySubview class I expose a property that sets the rotation as follows
var rotateAngle: Int = 0 {
didSet {
let measurement = Measurement(value: Double(rotateAngle), unit: UnitAngle.degrees)
let radians = measurement.converted(to: UnitAngle.radians).value
layer.transform = CATransform3DMakeRotation(CGFloat(radians), 0.0, 0.0, 1.0)
}
}
In the MyView, the autolayout is set using
subview.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
subview.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5),
subview.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.5),
subview.centerXAnchor.constraint(equalTo: centerXAnchor),
subview.centerYAnchor.constraint(equalTo: centerYAnchor),
])
When I set it without using autolayout, I simply set the subview frame
override func layoutSubviews() {
super.layoutSubviews()
subview.frame = CGRect(origin: CGPoint(x: frame.width / 4, y: frame.height / 4), size: CGSize(width: frame.width / 2, height: frame.height / 2))
}
The reason I don't want to use autolayout is because in the real app, I have hundreds of view refreshed quite often which performs better without autolayout.
Here is a complete listing that
class RotateViewController: UIViewController {
#IBOutlet var myView: MyView!
#IBOutlet var slider: UISlider!
override func viewDidLoad() {
myView = MyView()
view.addSubview(myView)
myView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
myView.widthAnchor.constraint(equalToConstant: 60),
myView.heightAnchor.constraint(equalToConstant: 60)
])
}
#IBAction func sliderValueChanged() {
myView.subview.rotateAngle = Int(slider.value)
}
}
class MySubView: UIView {
var rotateAngle: Int = 0 {
didSet {
let measurement = Measurement(value: Double(rotateAngle), unit: UnitAngle.degrees)
let radians = measurement.converted(to: UnitAngle.radians).value
layer.transform = CATransform3DMakeRotation(CGFloat(radians), 0.0, 0.0, 1.0)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .blue
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = .blue;
}
}
class MyView: UIView {
let useAutoLayout = false
var subview = MySubView()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(subview)
backgroundColor = .red
setupConsraints()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addSubview(subview)
backgroundColor = .red
setupConsraints()
}
override func layoutSubviews() {
super.layoutSubviews()
if useAutoLayout {
return
}
subview.frame = CGRect(origin: CGPoint(x: frame.width / 4, y: frame.height / 4), size: CGSize(width: frame.width / 2, height: frame.height / 2))
}
private func setupConsraints() {
if !useAutoLayout {
return
}
subview.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
subview.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5),
subview.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.5),
subview.centerXAnchor.constraint(equalTo: centerXAnchor),
subview.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
This because of re-run of this
override func layoutSubviews() {
super.layoutSubviews()
if(once)
{
subview.frame = CGRect(origin: CGPoint(x: frame.width / 4, y: frame.height / 4), size: CGSize(width: frame.width / 2, height: frame.height / 2))
print(subview.frame)
once = false
}
}
The problem is that you're constantly resetting the frame of the view, which is throwing off the transform. In MyView update setupConsraints() i.e.
private func setupConsraints() {
if !useAutoLayout {
subview.frame = CGRect(origin: CGPoint(x: frame.width / 4, y: frame.height / 4), size: CGSize(width: frame.width / 2, height: frame.height / 2))
return
}
subview.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
...
])
}
and then you can delete the layoutSubviews() function altogether.
After reading the docs of UIView I believe that the root cause is that the frame becomes invalid after setting the transform property to something else that identity:
If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.
So instead of using the frame of the rotated view, I can use its bounds to set it up:
override func layoutSubviews() {
super.layoutSubviews()
subview.bounds.size = CGSize(width: frame.width / 2, height: frame.height / 2)
subview.center = CGPoint(x: frame.width / 2, y: frame.height / 2)
}

Seeing the draw using IBDesignable+UIView(storyboard) but can't see on app

I'm trying to draw some dashed lines on an app but it only draws on main.storyboard with IBDesignable. When I run the app on iOS simulator, nothing shows. What's happening?
The code to draw:
#IBDesignable
class AnalogView: UIView {
fileprivate let thickHorizontalLayer = CAShapeLayer()
fileprivate let thinHorizontalLayer = CAShapeLayer()
#IBInspectable var thickYCoord = 50.0
#IBInspectable var thinYCoord = 52.5
override init(frame: CGRect) {
super.init(frame: frame)
let thickDashesPath = UIBezierPath()
thickDashesPath.move(to: CGPoint(x: 0, y: thickYCoord)) //left
thickDashesPath.addLine(to: CGPoint(x: 340, y: thickYCoord)) //right
//thickHorizontalLayer.frame = frame
thickHorizontalLayer.path = thickDashesPath.cgPath
thickHorizontalLayer.strokeColor = UIColor.black.cgColor //dashes color
thickHorizontalLayer.lineWidth = 20
thickHorizontalLayer.lineDashPattern = [ 1, 83.5 ]
//thickHorizontalLayer.lineDashPhase = 0.25
self.layer.addSublayer(thickHorizontalLayer)
let thinDashesPath = UIBezierPath()
thinDashesPath.move(to: CGPoint(x: 0, y: thinYCoord)) //esquerda
thinDashesPath.addLine(to: CGPoint(x: 340, y: thinYCoord)) //direita
//thinHorizontalLayer.frame = frame
thinHorizontalLayer.path = thinDashesPath.cgPath
thinHorizontalLayer.strokeColor = UIColor.black.cgColor
thinHorizontalLayer.lineWidth = 15.0
thinHorizontalLayer.fillColor = UIColor.clear.cgColor
thinHorizontalLayer.lineDashPattern = [ 0.5, 7.95]
//thinHorizontalLayer.lineDashPhase = 0.25
self.layer.addSublayer(thinHorizontalLayer)
You need to put the common code in a routine that is called by both init(frame:) and init(coder:):
#IBDesignable
class AnalogView: UIView {
fileprivate let thickHorizontalLayer = CAShapeLayer()
fileprivate let thinHorizontalLayer = CAShapeLayer()
#IBInspectable var thickYCoord: CGFloat = 50.0
#IBInspectable var thinYCoord: CGFloat = 52.5
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
let thickDashesPath = UIBezierPath()
thickDashesPath.move(to: CGPoint(x: 0, y: thickYCoord)) //left
thickDashesPath.addLine(to: CGPoint(x: 340, y: thickYCoord)) //right
thickHorizontalLayer.path = thickDashesPath.cgPath
thickHorizontalLayer.strokeColor = UIColor.black.cgColor //dashes color
thickHorizontalLayer.lineWidth = 20
thickHorizontalLayer.lineDashPattern = [ 1, 83.5 ]
self.layer.addSublayer(thickHorizontalLayer)
let thinDashesPath = UIBezierPath()
thinDashesPath.move(to: CGPoint(x: 0, y: thinYCoord)) //esquerda
thinDashesPath.addLine(to: CGPoint(x: 340, y: thinYCoord)) //direita
thinHorizontalLayer.path = thinDashesPath.cgPath
thinHorizontalLayer.strokeColor = UIColor.black.cgColor
thinHorizontalLayer.lineWidth = 15.0
thinHorizontalLayer.fillColor = UIColor.clear.cgColor
thinHorizontalLayer.lineDashPattern = [ 0.5, 7.95]
self.layer.addSublayer(thinHorizontalLayer)
}
}
I'd also suggest declaring an explicit type for your #IBInspectable types, or else you won't be able to adjust them in IB.
Personally, rather than hard coding the path width, I'd update it when the layout changes. Also, if you're going to make those properties #IBDesignable, you really want to update the paths if they change.
#IBDesignable
class AnalogView: UIView {
fileprivate let thickHorizontalLayer = CAShapeLayer()
fileprivate let thinHorizontalLayer = CAShapeLayer()
#IBInspectable var thickYCoord: CGFloat = 50.0 { didSet { updatePaths() } }
#IBInspectable var thinYCoord: CGFloat = 52.5 { didSet { updatePaths() } }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
layer.addSublayer(thickHorizontalLayer)
layer.addSublayer(thinHorizontalLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
updatePaths()
}
private func updatePaths() {
let thickDashesPath = UIBezierPath()
thickDashesPath.move(to: CGPoint(x: bounds.origin.x, y: thickYCoord)) //left
thickDashesPath.addLine(to: CGPoint(x: bounds.origin.x + bounds.size.width, y: thickYCoord)) //right
thickHorizontalLayer.path = thickDashesPath.cgPath
thickHorizontalLayer.strokeColor = UIColor.black.cgColor //dashes color
thickHorizontalLayer.lineWidth = 20
thickHorizontalLayer.lineDashPattern = [1.0, NSNumber(value: Double(bounds.size.width - 1) / 4 - 1.0) ]
let thinDashesPath = UIBezierPath()
thinDashesPath.move(to: CGPoint(x: bounds.origin.x, y: thinYCoord)) //esquerda
thinDashesPath.addLine(to: CGPoint(x: bounds.origin.x + bounds.size.width, y: thinYCoord)) //direita
thinHorizontalLayer.path = thinDashesPath.cgPath
thinHorizontalLayer.strokeColor = UIColor.black.cgColor
thinHorizontalLayer.lineWidth = 15.0
thinHorizontalLayer.fillColor = UIColor.clear.cgColor
thinHorizontalLayer.lineDashPattern = [0.5, NSNumber(value: Double(bounds.size.width - 1) / 40 - 0.5)]
}
}
You might want to adjust the dashing to span the width, too (I'm not sure if you wanted a consistent scale or for it to span the width). But hopefully this illustrates the idea.

Animation origin iOS

I've been trying to create an animation that increases the height of a view without changing the origin (the top left and right edges stay in the same position)
import UIKit
class SampRect: UIView {
var res : CGRect?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor(red: 0.91796875, green: 0.91796875, blue: 0.91796875, alpha: 1)
self.layer.cornerRadius = 5
let recognizer = UITapGestureRecognizer(target: self, action:#selector(SampRect.handleTap(_:)))
self.addGestureRecognizer(recognizer)
}
func handleTap(recognizer : UITapGestureRecognizer) {
let increaseSize = CABasicAnimation(keyPath: "bounds")
increaseSize.delegate = self
increaseSize.duration = 0.4
increaseSize.fromValue = NSValue(CGRect: frame)
res = CGRect(x: self.frame.minX, y: self.frame.minY, width: frame.width, height: self.frame.height + 10)
increaseSize.toValue = NSValue(CGRect: res!)
increaseSize.fillMode = kCAFillModeForwards
increaseSize.removedOnCompletion = false
layer.addAnimation(increaseSize, forKey: "bounds")
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
self.frame = res!
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
but the animation increase the size from the middle (push the top edge up and the bottom edge down)
what am I missing?
You can Try this.
let newheight:CGFloat = 100.0
UIView.animateWithDuration(0.5) {
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, newheight)
}

Resources