Why is CALayer mask not centered and off by a few pixels? - ios

I have the following code:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
var gradient = new CAGradientLayer();
gradient.Frame = view.Bounds;
gradient.Colors = colors;
var mask = new CALayer();
mask.Frame = view.Bounds;
mask.Contents = view.Image.CGImage;
mask.ContentsGravity = CALayer.GravityCenter;
gradient.Mask = mask;
view.Layer.AddSublayer(gradient);
}
which produces the following result:
"view" is a UIImageView which is centered perfectly.
The number "1" is exactly in the middle. Here you can see that the mask is not centered perfectly.
The image here is the heart icon from the standard resources, it's not a custom image. (If e.g. used in a button, then it's centered perfectly).
Before posting I've tried all possible combinations when it comes to setting Frame and Bounds, the mask is always off-center by a few pixels.
Someone else reported a similar issue here, but there was no solution: Gradient Color over Template Image in Swift 4
This makes me believe that there's a bug with how masks are applied. Could anyone confirm?

There’s no bug with masking. This is crucial stuff, basic to all drawing. If there were a bug the whole interface would be broken. The bug is in your code.
It's all basically just a matter of timing. Do not talk about any frames or bounds until you know what they actually are.
To demonstrate, I'm going to work backwards: first I'll show you the result of my code, then I'll show you the code. Here's the result. I start with a storyboard that has a square centered by autolayout, so you know I'm not cheating:
Now I run the app and superimpose over the entire screen a gradient layer with a centered heart image mask:
As you can see, the heart is in exactly the right place. It is centered in precisely the same sense as the yellow square, and that is why it appears centered in the square.
Now here's the code. Notice that the assembly work is done just once, in viewDidLoad, but all the measurement work is done when we have actual measurements to work with, namely, in viewDidLayoutSubviews. This is Swift but you should have no difficulty translating (C# or whatever):
class ViewController: UIViewController {
let gradient : CAGradientLayer = {
let g = CAGradientLayer()
g.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
return g
}()
let mask = CALayer()
let image : UIImage = {
let im = UIImage(systemName: "heart.fill")!
return im
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.layer.addSublayer(gradient)
mask.contents = image.cgImage
mask.contentsGravity = .center
gradient.mask = mask
}
override func viewDidLayoutSubviews() {
gradient.frame = view.bounds
mask.frame = gradient.bounds
}
}
Go ye and do likewise.

Related

Trouble positioning a sublayer correctly during animation

The question is how should I define and set my shape layer's position and how should it be updated so that the layer appears where I'm expecting it to during the animation? Namely, the shape should be stuck on the end of the stick.
I have a CALayer instance called containerLayer, and it has a sublayer which is a CAShapeLayer instance called shape. containerLayer is supposed to place shape at a specific position unitLoc like this:
class ContainerLayer: CALayer, CALayerDelegate {
// ...
override func layoutSublayers() {
super.layoutSublayers()
if !self.didSetup {
self.setup()
self.didSetup = true
}
updateFigure()
setNeedsDisplay()
}
func updateFigure() {
figureCenter = self.bounds.center
figureDiameter = min(self.bounds.width, self.bounds.height)
figureRadius = figureDiameter/2
shapeDiameter = round(figureDiameter / 5)
shapeRadius = shapeDiameter/2
locRadius = figureRadius - shapeRadius
angle = -halfPi
unitLoc = CGPoint(x: self.figureCenter.x + cos(angle) * locRadius, y: self.figureCenter.y + sin(angle) * locRadius)
shape.bounds = CGRect(x: 0, y: 0, width: shapeDiameter, height: shapeDiameter)
shape.position = unitLoc
shape.updatePath()
}
// ...
}
I'm having trouble finding the right way to specify what this position should be before, and during a resize animation which changes containerLayer.bounds. I do understand that the problem I'm having is that I'm not setting the position in such a way that the animation will display it the way that I'm expecting it would.
I have tried using a CABasicAnimation(keyPath: "position") to animate the position, and it improved the result over what I had tried previously, but it's still off.
#objc func resize(sender: Any) {
// MARK:- animate containerLayer bounds & shape position
// capture bounds value before changing
let oldBounds = self.containerLayer.bounds
// capture shape position value before changing
let oldPos = self.containerLayer.shape.position
// update the constraints to change the bounds
isLarge.toggle()
updateConstraints()
self.view.layoutIfNeeded()
let newBounds = self.containerLayer.bounds
let newPos = self.containerLayer.unitLoc
// set up the bounds animation and add it to containerLayer
let baContainerBounds = CABasicAnimation(keyPath: "bounds")
baContainerBounds.fromValue = oldBounds
baContainerBounds.toValue = newBounds
containerLayer.add(baContainerBounds, forKey: "bounds")
// set up the position animation and add it to shape layer
let baShapePosition = CABasicAnimation(keyPath: "position")
baShapePosition.fromValue = oldPos
baShapePosition.toValue = newPos
containerLayer.shape.add(baShapePosition, forKey: "position")
containerLayer.setNeedsLayout()
self.view.layoutIfNeeded()
}
I also tried using the presentation layer like this to set the position, and it also seems to get it close, but it's still off.
class ViewController: UIViewController {
//...
override func loadView() {
super.loadView()
displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.default)
//...
}
#objc func animationDidUpdate(displayLink: CADisplayLink) {
let newCenter = self.containerLayer.presentation()!.bounds.center
let new = CGPoint(x: newCenter.x + cos(containerLayer.angle) * containerLayer.locRadius, y: newCenter.y + sin(containerLayer.angle) * containerLayer.locRadius)
containerLayer.shape.position = new
}
//...
}
class ContainerLayer: CALayer, CALayerDelegate {
// ...
func updateFigure() {
//...
//shape.position = unitLoc
//...
}
// ...
}
With some slight exaggeration, I was able to make it clearer what's happening in your code. In my example, the circle layer is supposed to remain 1/3 the height of the background view:
At the time the animation starts, the background view has already been set to its ultimate size at the end of the animation. You don't see that, because animation relies on portraying the layer's presentation layer, which is unchanged; but the view itself has changed. Therefore, when you position the shape of the shape layer, and you do it in terms of the view, you are sizing and positioning it at the place it will need to be when the animation ends. Thus it jumps to its final size and position, which makes sense only when we reach the end of the animation.
Okay, but now consider this:
Isn't that nicer? How is it done? Well, using the principles I have already described elsewhere, I've got a layer with a custom animatable property. The result is that on every frame of the animation, I get an event (the draw(in:) method for that layer). I respond to this by recalculating the path of the shape layer. Thus I am giving the shape layer a new path on every frame of the animation, and so it behaves smoothly. It stays in the right place, it resizes in smooth proportion to the size of the background view, and its stroke thickness remains constant throughout.

Swift: Insert text label onto round image

I'm trying to recreate something like this, where I pull the user's image, and then overlay the "Change" label on top- but I can't seem to figure out how.
(I also want to have some sort of action associated with this label (eg, segue to new page))
My issue: I cannot seem to figure out how to overlay the text label but still keep the image round and have the bottom part of the image have that opaque label.
Code+Details: I have a custom UIView, which contains an Imageview- When I want to add an image I call the following code:
self.userProfilePic.addImage((userImg).roundImage(), factorin: 0.95)
Within the custom view, this is how the image is added:
func addImage(imagein: UIImage, factorin:Float)
{
let img = imagein
imageScalingFactor = factorin
if imageView == nil
{
imageView = UIImageView(image: img)
imageView?.contentMode = .ScaleAspectFit
self.addSubview(imageView!)
self.sendSubviewToBack(imageView!)
imageView!.image = img
}
}
This is my code for the image rounding (which I do not want to touch):
extension UIImage
{
func roundImage() -> UIImage
{
let newImage = self.copy() as! UIImage
let cornerRadius = self.size.height/2
UIGraphicsBeginImageContextWithOptions(self.size, false, 1.0)
let bounds = CGRect(origin: CGPointZero, size: self.size)
UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).addClip()
newImage.drawInRect(bounds)
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return finalImage
}
}
Any help would be appreciated!
Try something like this view hierarchy
UIView
!
!--UIImageView
!--UIButton
so you take 'UIView', inside that first add the UIImageView than on the lower portion of the image view add a UIButton. Now set the clipToBounds to yes. Now set the desire corner radius of this parent view's layer as following
parentView.layer.cornerRadius = parentVirew.frame.size.width;
Remember you have to make the parent view of square size, means the height & width should be the same, for getting the circular masking. Adjust the button position a bit. You will definetly get the result.

Gradient at Top/Bot of Scrollview When There's Additional Content

I have a scrollview>ContentView>TextLabel setup where the gradient is showing up but not working how I want it to. It's a clear background scrollView, so all I'm looking for is a clear gradient mask over the text. I found something similar to I'm looking for in Objective-C but that thread doesn't have a Swift solution.
My end goal seems like something most people might use, so I'm surprised there's not a Swift version yet. The functionality I'm trying to code is:
Sometimes the text will fit perfectly in the scrollView's fixed size so there should be no gradient.
When the text is longer than can fit and so some of it is below the scrollView's bottom cutoff, the bottom of the view should fade to clear.
Once the user scrolls down and the top should fade, indicating that there's more above.
I tried this code to handle bullet #2:
let gradient = CAGradientLayer()
gradient.frame = self.bio_ScrollView.superview!.bounds ?? CGRectNull
gradient.colors = [UIColor.whiteColor().CGColor, UIColor.clearColor().CGColor]
//gradient.locations = [0, 0.15, 0.25, 0.75, 0.85, 1.0]
gradient.locations = [0.6, 1.0]
self.bio_ScrollView.superview!.layer.mask = gradient
But it fades everything, including the button below it, which is clearly not in the scrollview:
If I remove the .superview and apply it directly to the scrollView, it just fades all the text below the initial part that was visible:
Does anyone know anything about how to implement this correctly?
Figured it out. First, make sure you've added the right view hierarchy. The scroll view needs to be embedded in a container view (which is what the gradient will be applied to):
Top/container view: Set this up however you want
Scrollview: Pin all edges to container (4 total constraints)
Scrollview content view: Pin edges + Center X (5 total constraints)
Label: Pin edges (4 total constraints)
Add "scrollViewDelegate" to the ViewController class:
class ViewController_WithScrollView: UIViewController, UIScrollViewDelegate {
....
Connect the four views listed above with IBOutlets. Then, set your scrollView delegate within the viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
yourScrollView.delegate = self
//+All your other viewDidLoad stuff
}
Then implement the scrollViewDidScroll func, which will run automatically thanks to the work you did above.
func scrollViewDidScroll (scrollView: UIScrollView) {
if scrollView.isAtTop {
self.applyGradient_To("Bot")
} else if scrollView.isAtBottom {
self.applyGradient_To("Top")
} else {
self.applyGradient_To("Both")
}
}
Then add this magical gradient code:
func applyGradient_To (state: String) {
let gradient = CAGradientLayer()
gradient.frame = self.yourScrollView.superview!.bounds ?? CGRectNull
switch state {
case "Top":
gradient.colors = [UIColor.clearColor().CGColor,UIColor.whiteColor().CGColor]
gradient.locations = [0.0,0.2]
case "Bot":
gradient.colors = [UIColor.whiteColor().CGColor, UIColor.clearColor().CGColor]
gradient.locations = [0.8,1.0]
default:
gradient.colors = [UIColor.clearColor().CGColor,UIColor.whiteColor().CGColor,UIColor.whiteColor().CGColor, UIColor.clearColor().CGColor]
gradient.locations = [0.0,0.2,0.8,1.0]
}
self.yourScrollView.superview!.layer.mask = nil
self.yourScrollView.superview!.layer.mask = gradient
}
That should do it!

Can a value of a var in #IBDesignable depend on external values, such as a frame of a view?

I want to draw a shape hole in an #IBDesignable view. For this I need the size of a view, which is above in the view hierarchy. This view changes it's size by constraints.
How to retrieve the frame? May this is not possible at all, because the view we are depending on is drawn later? But it's frame size may be retrievable early?
<MainView>
| <View>
| | <ViewWithShape>
| <ViewWeDependOn>
The shape is drawn in layoutSubviews:
override func layoutSubviews() {
super.layoutSubviews()
if !(holeLayer != nil) {
holeLayer = CAShapeLayer()
layer.mask = holeLayer
let rect = bounds // I need the frame HERE
let path = UIBezierPath(quadrantHoleInsideRect: rect)
holeLayer.path = path.CGPath
}
}
I put this question, because my first obvious solution would have been a outlet to the depending view from within the #IBDesignable view.
This gave me a crash at first, but it is now working as expected.

UIVisualEffectView doesn't blur it's SKView superview

I'm writing a SpriteKit game and faced a problem with blurred view, which lies on the SKView. It is supposed to slide from the right when game is paused and it should blur the content of it's parent view (SKView) just like control center panel in iOS 7. Here is the desired appearance:
What I actually get is:
In fact the view on the left is not totally black, you can see how highlights from the superview are slightly struggling through almost opaque subview, but no blur is applied. Is it an iOS 8 bug/feature, or is it my mistake/misunderstanding
Here is my UIVisualEffectView subclass's essensials:
class OptionsView: UIVisualEffectView {
//...
init(size: CGSize) {
buttons = [UIButton]()
super.init(effect: UIBlurEffect(style: .Dark))
frame = CGRectMake(-size.width, 0, size.width, size.height)
addButtons()
clipsToBounds = true
}
func show() {
UIView.animateWithDuration(0.3, animations: {
self.frame.origin.x = 0
})
}
func hide() {
UIView.animateWithDuration(0.3, animations: {
self.frame.origin.x = -self.frame.size.width
})
}
Then in GameScene class:
in initializer:
optionsView = OptionsView(size: CGSizeMake(130, size.height))
in didMoveToView(view: SKView):
view.addSubview(optionsView)
on pressing pause button:
self.optionsView.show()
P.S. Though I know two another ways to implement blur view, I thought this one was the easiest, since my app is going to support iOS8 only
Render a blurred static image from superview ->
put UIImageView on the OptionsView, with clipsToBounds = true ->
animate UIImageView position while animating optionsView position, so that blur stays still relatively to the superview
Forget about UIView, UIVisualEffectView and UIBlurView and use SKEffectNode together with SKCropNode.
Ok, I have managed to get the desired effect using SKEffectNode instead of UIVisualEffectView.
Here is the code for someone facing the same issue
class BlurCropNode: SKCropNode {
var blurNode: BlurNode
var size: CGSize
init(size: CGSize) {
self.size = size
blurNode = BlurNode(radius: 10)
super.init()
addChild(blurNode)
let mask = SKSpriteNode (color: UIColor.blackColor(), size: size)
mask.anchorPoint = CGPoint.zeroPoint
maskNode = mask
}
}
class BlurNode: SKEffectNode {
var sprite: SKSpriteNode
var texture: SKTexture {
get { return sprite.texture }
set {
sprite.texture = newValue
let scale = UIScreen.mainScreen().scale
let textureSize = newValue.size()
sprite.size = CGSizeMake(textureSize.width/scale, textureSize.height/scale)
}
}
init(radius: CGFloat) {
sprite = SKSpriteNode()
super.init()
sprite.anchorPoint = CGPointMake(0, 0)
addChild(sprite)
filter = CIFilter(name: "CIGaussianBlur", withInputParameters: ["inputRadius": radius])
shouldEnableEffects = true
shouldRasterize = true
}
}
Result:
There are several issues though
Cropping doesn't work with SKEffectNode until it's shouldRasterize property is set to true. I get the whole screen blurred. So I still don't know how to properly implement realtime blur.
Animation on the BlurCropNode is not smooth enough. There is a lag at the beginning because of capturing texture and setting it to the effectNode's sprite child. Even dispatch_async doesn't help.
It would be very much appreciated if anyone could help to solve at least one of the problems
I know I'm probably a bit late but I was having the same problem and found a solution to creating a realtime blur on part of the screen. It's based on this tutorial: http://www.youtube.com/watch?v=eYHId0zgkdE where he used a shader to blur a static sprite. I extended his tutorial to capture of the part of the screen and then apply the blur to that. For your problem you could capture under that sidebar.
Firstly, you create an SKSpriteNode to hold the captured texture. Then in didMoveToView() you add your blur shader to that sprite. (You can find the blur.fsh file on GitHub, there's a link at the bottom of the youtube video.)
override func didMoveToView(view: SKView) {
blurNode.shader = SKShader(fileNamed: "blur")
self.addChild(blurNode)
}
Then you have to capture the section of the view you want to blur and apply the SKTexture to, in my case, blurNode.
override func update(currentTime: CFTimeInterval) {
// Hide the blurNode to avoid it be captured too.
blurNode.hidden = true
blurNode.texture = self.view!.textureFromNode(self, crop: blurNode.frame)
blurNode.hidden = false
}
And that should be it. On my iPad mini, with a blur of 1/3 of the width of the screen, the fps was 58-59. Blurring the whole screen the fps was down to about 22 so it's obviously not ideal for some things but hopefully it helps.

Resources