I am trying to custom annotate images on image view using CAShapeLayers. This include adding shapes(like rectangle, ellipse, line) over the image view, touch them, move them around and pinch-zoom them. I am able to recognise the touch of the layer over the image view, but unsuccessful when tried to move it around. As I try to move the layer, it flies to a corner.
Code and output as below:
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func drawRectangle(_ sender: UIButton) { // to initially place the shape at the centre of the image view.
drawRectangleWithRect(CGRect(x: imageView.bounds.midX - 100, y: imageView.bounds.midY - 100, width: 200, height: 200))
}
func drawRectangleWithRect(_ rect: CGRect) {
let path = UIBezierPath(rect: rect)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
//shapeLayer.bounds = (shapeLayer.path?.boundingBox)!
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = nil
shapeLayer.lineWidth = 1
imageView.layer.addSublayer(shapeLayer)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let point = touch!.location(in: imageView)
guard let sublayers = imageView.layer.sublayers else { return }
for sublayer in sublayers {
if let sublayer = sublayer as? CAShapeLayer, let path = sublayer.path {
//if path.contains(point) {
sublayer.position = point
//}
}
}
}
}
Tapping on Rectangle button creates the layer at the centre of image view as above. But when I start to touch and move the layer just a bit it goes off to the right bottom corner and disappears.
Now tweaked a bit - uncommented the line shapeLayer.bounds = (shapeLayer.path?.boundingBox)! inside the method drawRectangleWithRect(_ rect: CGRect). Then tapping on the Rectangle button placed the layer at the left top of the view and not at the centre :( . But moving it around now made the layer move around with my finger (though there is a delay). But also that this made the layer move even when image view is touched outside the layer. I understand that some if condition inside the touchesMoved method is needed that properly recognises the layer area but the commented one in the above code did not work properly.
What am I missing to properly place the layer at centre of the view and move it around without any delay ?
Can I actually pinch zoom the layers effectively ?
What is the best recommended way to annotate images like this ? Adding layers with touch recognition or adding shapes as subviews with pan gestures ?
Related
I have custom, layer-backed NSView as the document view of a NSScrollView. The scroll view is inside a NSSplitView, which itself uses constraints to ensure that it fills the entire window.
The bounds of the custom view are never explicitly set. At one point, however, the bounds of the backing layer are set to encompass every child layer. Setting the bounds explicitly doesn't help.
When I resize the window, the scroll view clips my custom view and I find myself unable to scroll:
When I debug my view, I find that resizing the window also resizes the custom view's frame. I don't really know if this is normal. My autoresize mask is .ViewMinYMargin, and translatesAutoresizingMaskIntoConstraints if off (but it doesn't appear to change anything if I turn it on).
Considering that there are basically no instructions on what to do to make scrolling work in the Apple guide, I must be missing something fairly simple. Does anyone have any idea?
This happened because of a misunderstanding of autolayout constraints. Each edge of the custom view was constrained to be at a distance of 0 to the edge of the clip view. This effectively set the size of the view to whatever the size of the clip view was, preventing scrolling because it made it look like the clipped size was the custom view's natural size.
The solution was to constrain only the left, top and right edges, and leave the bottom edge free. Instead, I made a height constraint on the custom view, and I programmatically set its constant value to the size of my view's contents (which is dynamically generated).
Sample code for LayerBackedView:
class LayerBackedView: NSView {
required init?(coder: NSCoder) {
super.init(coder: coder)
self.wantsLayer = true
}
override var flipped: Bool {
// hug top of scroll view if not high enough to fill it entirely
return true
}
private var heightConstraint: NSLayoutConstraint {
let firstConstraint = constraints[0] as! NSLayoutConstraint
assert(firstConstraint.firstAttribute == .Height)
return firstConstraint
}
override func awakeFromNib() {
let red = CALayer()
red.anchorPoint = CGPointMake(0, 0)
red.backgroundColor = CGColorCreateGenericRGB(1, 0, 0, 1)
red.borderWidth = 2
red.borderColor = CGColorCreateGenericGray(0, 1)
layer!.addSublayer(red)
// set height constraint to whatever you need it to be
let desiredHeight: CGFloat = 200
heightConstraint.constant = desiredHeight
red.frame = CGRectMake(0, 0, bounds.width, desiredHeight)
}
}
Constraints on the LayerBackedView (the height value doesn't matter because we change it programmatically, the constraint just needs to exist):
Size constraints are added to the view itself, but spacing constraints are added to the superview. This means that out of the four constraints, the height constraint will be the only one inside the view (and this is why we can do constraints[0] as! NSLayoutConstraint). If this is not your case, you can make an #IBOutlet to it instead.
I have set up a very simple Single View Application (in Swift) using iOS 8.1. I have added one UIImageView to the main view controller view. I am trying to use CAKeyframeAnimation to animate a sequence of images. I was originally using the UIImageView animationImages property which worked fine but I need to be able to know precisely when the animation finishes, hence the move to CAKeyframeAnimation.
My code is as follows:
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
var animation : CAKeyframeAnimation!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let animationImages:[AnyObject] = [UIImage(named: "image-1")!, UIImage(named: "image-2")!, UIImage(named: "image-3")!, UIImage(named: "image-4")!]
animation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = kCAAnimationDiscrete
animation.duration = 25
animation.values = animationImages
animation.repeatCount = 25
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeForwards
self.imageView.layer.addAnimation(animation, forKey: "contents")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
The problem is that the animation does not display any images and I just receive a blank screen. Is there something I am missing in the above code? How can I get the animation to display?
This line is never going to work:
animation.values = animationImages
Change it to:
animation.values = animationImages.map {$0.CGImage as AnyObject}
The reason is that you are trying to animate the "contents" key of this layer. But that is the contents property. But the contents property must be set to a CGImage, not a UIImage. Your animationImages, by contrast, contains UIImages, not CGImages.
Thus you need to transform your array of UIImage into an array of CGImage. Moreover, you're trying to pass this array to Objective-C, where an NSArray must contain objects only; and since CGImage is not an object in Objective-C's mind, you need to cast each of them as an AnyObject. And that is what my map call does.
I'm trying to implement the behaviour seen in the iOS lock screen when touching the camera icon, using a UIPageViewController
I have a UIPageViewController that vertically scrolls between 2 view controllers. I would like to do a small "bounce/bump" when the user taps the view.
Any ideas on how I can accomplish this?
Thank you!!
Thanks to #Szu for pointing me out to UIKit Dynamics :)
Here's a rough solution:
Call init with a controller's view (to be bounced) and a reference view (your current view controller's view property).
eg.
FSBounceAnimator(contentView: viewController.view, referenceView: self.view)
viewController is any viewController that you may instantiated and added as a childViewController
self.view is your current viewController view.
Call bounce(), to bounce :)
import UIKit
class FSBounceAnimator: NSObject {
var animator: UIDynamicAnimator
var gravityBehaviour: UIGravityBehavior
var pushBehavior: UIPushBehavior
var itemBehaviour: UIDynamicItemBehavior
init(contentView: UIView!, referenceView: UIView!) {
animator = UIDynamicAnimator(referenceView: referenceView)
var colisionBehaviour: UICollisionBehavior = UICollisionBehavior(items: [contentView])
colisionBehaviour.setTranslatesReferenceBoundsIntoBoundaryWithInsets(UIEdgeInsetsMake(-100, 0, 0, 0))
animator.addBehavior(colisionBehaviour)
gravityBehaviour = UIGravityBehavior(items: [contentView])
gravityBehaviour.gravityDirection = CGVectorMake(1, 1)
animator.addBehavior(gravityBehaviour)
pushBehavior = UIPushBehavior(items: [contentView], mode: UIPushBehaviorMode.Instantaneous)
pushBehavior.magnitude = 0.0
pushBehavior.angle = 0.0
animator.addBehavior(pushBehavior)
itemBehaviour = UIDynamicItemBehavior(items: [contentView])
itemBehaviour.elasticity = 0.45
animator.addBehavior(itemBehaviour)
super.init()
}
func bounce() {
self.pushBehavior.pushDirection = CGVectorMake(0.0, 100.0);
self.pushBehavior.active = true;
}
}
I have a UITableViewCell with constraints so that the cells layout correctly on iPhone 6 and iPhone 6+.
The cell contents correctly resize correctly. However, I need to draw a sublayer gradient over one of the views inside. In order to do so, I'm using awakeFromNib to draw the sublayer.
AwakeFromNib is giving the size of the frame prior to it autoresizing and therefore the gradient subview isn't the right size.
If I use layoutSubviews, the first time it's called, the size is also wrong and the cell needs to be scrolled before it has the right size; so that method also isn't working
What method should I be using to get the right frame size in order to draw correctly?
This code worked for me, is written in Swift.
import UIKit
import QuartzCore
class DemoTableViewCell: UITableViewCell {
#IBOutlet weak var gradientView: UIView!
private let gl = CAGradientLayer()
override func awakeFromNib() {
super.awakeFromNib()
// set gradient
let colors: [AnyObject] = [
UIColor.clearColor().CGColor,
UIColor(white: 0, alpha: 0.8).CGColor
]
gl.colors = colors
gradientView.layer.insertSublayer(gl, atIndex: 0)
}
override func layoutSubviews() {
super.layoutSubviews()
// update gradient
gl.frame = gradientView.bounds
}
}
during awakeFromNib the frame layouts do not appear. On your custom Cell you can either do this
-(void)drawRect:(CGRect)rect
{
[super drawRect:rect];
// Enter Custom Code
}
Or you can override applyLayoutAttributes method like this
-(void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
[super applyLayoutAttributes:layoutAttributes];
// Enter custom Code here
}
applyLayoutAttributes is called before drawRect method but still has the size parameteres of rect.
#angshuk answer is perfect where if anyone wants to modify UI inside UITableViewCell. Like in my case I was trying to add a different layer on runtime to an UIView, placed inside UITableViewCell using storyboard/autolayout. Tried the same thing inside awakeFromNib() & layoutSubviews() also. Nothing worked.
Here is the same code using Swift. By the way I am using Xcode 7.3.1 with Swift 2.2.
import UIKit
class InviteMainCellClass: UITableViewCell {
#IBOutlet weak var shareCodeLblSuperView: UIView! //Custom view added using IB/Autolayout
override func awakeFromNib() {
super.awakeFromNib()
//shareCodeLblSuperView.addDashedBorderToView(self.shareCodeLblSuperView, viewBorderColor: UIColor.whiteColor(), borderWidth: 1.0) //Not worked
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
shareCodeLblSuperView.addDashedBorderToView(self.shareCodeLblSuperView, viewBorderColor: UIColor.whiteColor(), borderWidth: 1.0) //Worked
}
override func layoutSubviews() {
super.layoutSubviews()
//shareCodeLblSuperView.addDashedBorderToView(self.shareCodeLblSuperView, viewBorderColor: UIColor.whiteColor(), borderWidth: 1.0)//Not worked
}
Hope this helped. Thanks.
I'm working on custom keyboard for iOS 8. By default it has grey background color.
If you are setting some color for its view background - all is OK, but how can I make it transparent? (UIColor.clearColor() doesn't work)
Are you using a xib file to load the view ?
Or did you do everything programmatically ?
I did mine using a xib file, I set the background color in the story to the default empty one.
Then before viewDidLoad I set it up to clearcolor, qnd it works. I got the same background than the default keyboard.
I haven't tried it with a keyboard, but can you set a background PNG? If so, create a 1px by 1px PNG that's clear (keep the transparency in your PNG). Use that as your background image. It appears there is currently a bug in the SDK that is causing this problem.
Unfortunately you can't display the keyboard's background as .clear (transparent) but you can change it to other colors. More on that:
Remember when using a nib, there are 2 views - the nib's view and the ViewController's view. You can change the nib's background to .clear, but that just show's the ViewController's view (in this case, KeyboardViewController)
override func viewDidLoad() {
super.viewDidLoad()
let keyboardNib = UINib(nibName: "KeyboardView", bundle: nil)
let keyboardView = keyboardNib.instantiate(withOwner: self, options: nil) [0] as? UIView
view.backgroundColor = .red
keyboardView.backgroundColor = .clear
}
Where the above really comes in handy is when you want the nib's view to match the ViewController's view (keyboardView.backgroundColor = view.backgroundColor also does the same).
If you're just wanting to change the nib's color, you can just set the nib's background color to whatever color you want since its in front of the keyboardViewController's view.
override func viewDidLoad() {
super.viewDidLoad()
let keyboardNib = UINib(nibName: "KeyboardView", bundle: nil)
let keyboardView = keyboardNib.instantiate(withOwner: self, options: nil) [0] as? UIView
keyboardView.backgroundColor = .red
}