I made a UIView subclass that implements custom drawing. Here is the code (please don't mind that this could be done by a UIImageView. I stripped the code of all extras just to show the problem)
#IBDesignable
class TPMSkinnedImageView: UIView {
private var originalImage:UIImage?
private var skinnedImage:UIImage?
#IBInspectable var image: UIImage? {
set {
originalImage = newValue
if(newValue == nil) {
skinnedImage = nil
return
}
skinnedImage = originalImage!
self.invalidateIntrinsicContentSize()
self.setNeedsDisplay()
}
get {
return originalImage
}
}
override func draw(_ rect: CGRect) {
let context:CGContext! = UIGraphicsGetCurrentContext()
context.saveGState()
context.translateBy(x: 0, y: rect.height)
context.scaleBy(x: 1, y: -1)
context.draw(skinnedImage!.cgImage!, in: rect)
context.restoreGState()
}
override var intrinsicContentSize: CGSize {
if(skinnedImage != nil) {
return skinnedImage!.size
}
return CGSize.zero
}
}
I instantiate this view in a viewcontroller nib file and show the viewcontroller modally.
What happens is is that the draw method only gets called when the parent view has been on screen for about 20 seconds.
I checked the intrinsicContentSize and it does not return .zero
This is what the stack looks like once it is called:
Any idea what could be causing this?
Try calling setNeedsDisplay() on your view in your view controller's viewWillAppear()
Related
I have a UI component (a loading spinner) that has an endless animation:
class MySpinner: UIView {
...
override var bounds: CGRect {
didSet {
updateLayers()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.animatedLayer)
self.updateLayers()
self.startAnimating()
}
private func updateLayers() {
//Set up path, stroke color and such
self.animatedLayer.frame = self.bounds
}
private func startAnimating() {
CATransaction.begin()
CATransaction.setDisableActions(true) //disable automatic animations by iOS
let endlessRotationAnimation = CAKeyframeAnimation(keyPath: "transform.rotation")
endlessRotationAnimation.values = [ 0.0, CGFloat(2.0 * .pi) ]
endlessRotationAnimation.keyTimes = [ 0.0, 1.0 ]
endlessRotationAnimation.repeatDuration = .infinity
endlessRotationAnimation.duration = 1.0
self.animatedLayer.add(endlessRotationAnimation, forKey: "rotationAnimation")
CATransaction.commit()
}
}
It turns out this is not working. I assume that if I call startAnimating() before the view is added to the window, iOS will immediately remove the animation. If I first add the view to the view hierarchy and then call startAnimating(), things start working.
Is there a way to prevent iOS removing my animation? Or is there some documentation about this behaviour, I couldn't find any?
I was able to work around this issue by overriding didMoveToWindow.
public override func didMoveToWindow() {
super.didMoveToWindow()
startAnimating()
}
I have a function in my controller I call
private func toggleLauncher() {
let launcher = CommentsLauncher()
launcher.showLauncher()
}
This essentially adds a view on top of the current view, with a semi transparent background.
I'd like to then render a custom inputAccessoryView at the bottom of the newly added view.
class CommentsLauncher: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0, alpha: 0.5)
}
func showLauncher() {
if let window = UIApplication.shared.keyWindow {
window.addSubview(view)
}
}
override var inputAccessoryView: UIView? {
get {
let containerView = UIView()
containerView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 50)
containerView.backgroundColor = .purple
return containerView
}
}
override var canBecomeFirstResponder: Bool {
return true
}
}
All that happens though is the semi transparent background is visible but I see no inputAccessoryView added to the view also and I am unsure why.
Your CommentsLauncher never become the first responder in the code you provided. A UIResponder's inputAccessoryView is displayed when the responder becomes first responder.
Change your showLauncher method to something like this:
func showLauncher() {
if let window = UIApplication.shared.keyWindow {
window.addSubview(view)
becomeFirstResponder()
}
}
And you should see the input accessory view.
I made a custom circular progress view by subclassing UIView, adding a CAShapeLayer sublayer to its layer and overriding drawRect() to update the shape layer's path property.
By making the view #IBDesignable and the progress property #IBInspectable, I was able to edit its value in Interface Builder and see the updated bezier path in real time. Non-essential, but really cool!
Next, I decided to make the path animated: Whenever you set a new value in code, the arc indicating the progress should "grow" from zero length to whatever percentage of the circle is achieved (think of arcs in the Activity app in apple Watch).
To achieve this, I swapped my CAShapeLayer sublayer by a custom CALayer subclass that has a #dynamic (#NSManaged) property, observed as key for ananimation (I implemented needsDisplayForKey(), actionForKey(), drawInContext() etc. ).
My View code (the relevant parts) looks something like this:
// Triggers path update (animated)
private var progress: CGFloat = 0.0 {
didSet {
updateArcLayer()
}
}
// Programmatic interface:
// (pass false to achieve immediate change)
func setValue(newValue: CGFloat, animated: Bool) {
if animated {
self.progress = newValue
} else {
arcLayer.animates = false
arcLayer.removeAllAnimations()
self.progress = newValue
arcLayer.animates = true
}
}
// Exposed to Interface Builder's inspector:
#IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: currentValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
private func updateArcLayer() {
arcLayer.frame = self.layer.bounds
arcLayer.progress = progress
}
And the layer code:
var animates: Bool = true
#NSManaged var progress: CGFloat
override class func needsDisplay(forKey key: String) -> Bool {
if key == "progress" {
return true
}
return super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> CAAction? {
if event == "progress" && animates == true {
return makeAnimation(forKey: event)
}
return super.action(forKey: event)
}
override func draw(in ctx: CGContext) {
ctx.beginPath()
// Define the arcs...
ctx.closePath()
ctx.setFillColor(fillColor.cgColor)
ctx.drawPath(using: CGPathDrawingMode.fill)
}
private func makeAnimation(forKey event: String) -> CABasicAnimation? {
let animation = CABasicAnimation(keyPath: event)
if let presentationLayer = self.presentation() {
animation.fromValue = presentationLayer.value(forKey: event)
}
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.duration = animationDuration
return animation
}
The animation works, but now I can't get my paths to show in Interface Builder.
I have tried implementing my view's prepareForInterfaceBuilder() like this:
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.topLabel.text = "Hello, Interface Builder!"
updateArcLayer()
}
...and the label text change is reflected in Interface Builder, but the path isn't rendered.
Am I missing something?
Well, isn't it funny... It turns out the declaration of my #Inspectable property had a very silly bug.
Can you spot it?
#IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: currentValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
It should be:
#IBInspectable var currentValue: CGFloat {
set(newValue) {
setValue(newValue: newValue, animated: false)
self.setNeedsLayout()
}
get {
return progress
}
}
That is, I was discarding the passed value (newValue) and using the current one (currentValue) to set the internal variable instead. "Logging" the value (always 0.0) to Interface Builder (via my label's text property!) gave me a clue.
Now it is working fine, no need for drawRect() etc.
There is a Main View with subview. I need to get these subviews.
For example, I created a view:
With the Swift Code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// Check if has view in rect area
let rect1 = CGRect(x: 0, y: 0, width: 100, height: 100)
if CGRectContainsPoint(rect1, rect1.origin) { // return true
print("TRUE")
} else {
print("FALSE")
}
}
}
I need to get the view you are "touching" the Rect in size or origin.
In the above example, I created three subviews with different colors between them.
I need to get the view and make a print() with the backgroundColor.
Can someone help me?
This func fix the problem:
func intersectingViewWithView(panView: UIView) -> UIView? {
for view in self.view.subviews {
if view != panView {
if CGRectIntersectsRect(panView.frame, view.frame) {
return view
}
}
}
return nil
}
I am trying to draw on an image from the camera roll using the code from this tutorial. After selecting a photo from the camera roll, imageView is set to the photo. When the user presses the draw button, tempImageView is added over the photo, so the user can draw onto it. However, when I attempt to draw on the image, nothing happens. Why?
Code:
class EditViewController: UIViewController{
var tempImageView: DrawableView!
var imageView: UIImageView = UIImageView(image: UIImage(named: "ClearImage"))
func draw(sender: UIButton!) {
tempImageView = DrawableView(frame: imageView.bounds)
tempImageView.backgroundColor = UIColor.clearColor()
self.view.addSubview(tempImageView)
}
}
class DrawableView: UIView {
let path=UIBezierPath()
var previousPoint:CGPoint
var lineWidth:CGFloat=10.0
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override init(frame: CGRect) {
previousPoint=CGPoint.zero
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
previousPoint=CGPoint.zero
super.init(coder: aDecoder)
let panGestureRecognizer=UIPanGestureRecognizer(target: self, action: "pan:")
panGestureRecognizer.maximumNumberOfTouches=1
self.addGestureRecognizer(panGestureRecognizer)
}
override func drawRect(rect: CGRect) {
// Drawing code
UIColor.greenColor().setStroke()
path.stroke()
path.lineWidth=lineWidth
}
func pan(panGestureRecognizer:UIPanGestureRecognizer)->Void
{
let currentPoint=panGestureRecognizer.locationInView(self)
let midPoint=self.midPoint(previousPoint, p1: currentPoint)
if panGestureRecognizer.state == .Began
{
path.moveToPoint(currentPoint)
}
else if panGestureRecognizer.state == .Changed
{
path.addQuadCurveToPoint(midPoint,controlPoint: previousPoint)
}
previousPoint=currentPoint
self.setNeedsDisplay()
}
func midPoint(p0:CGPoint,p1:CGPoint)->CGPoint
{
let x=(p0.x+p1.x)/2
let y=(p0.y+p1.y)/2
return CGPoint(x: x, y: y)
}
}
make sure that tempImageView.userIteractionEnabled = YES. by default this property for UIImageView is NO.
after put breakpoint to you gestureRecogniser method, see if it's being called.