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.
Related
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.
I have a UIView with a drawing on it that is composed of CALayers being created from a hardware device then me generating UIBezierPaths and added like so:
self.layer.addSublayer(currentLayer)
If my drawing is done after a device rotation, let's say, 90˚, the image is appropriate. However, after rotating back, the image is skewed and doesn't appear as it should. In essence, I want to redraw my CALayer with a new frame every time the device is rotated.
I've tried a few things, for starters, I tried rotating the UIView itself. Then I tried duplicating the layer that was being drawn on and rotating it, then removing all sublayers from my UIView and drawing the rotated layer, not working.
let layerZrotation = self.layer
self.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
if(doRotate == nil) { doRotate = 0}
layerZrotation.transform = CATransform3DMakeRotation(degree2radian(a:doRotate), 0.0, 0.0, 1.0)
self.layer.addSublayer(layerZrotation)
I expected a complete rotation when the phone is switched from portrait to landscape based on doRotate being set as follows:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
if UIDevice.current.orientation.isLandscape {
drawingViewTablet.doRotate = 90.0
buttonsShowing.doRotate = 90.0
} else {
drawingViewTablet.doRotate = -90.0
buttonsShowing.doRotate = -90.0
}
}
I have a horizontal UIScrollview in my app which has 1 really long UIImageView to start with. I have a timer and animation to create an illusion that the image under scroll view is automatically scrolling. Once the image comes to an end i dynamically add similar image to the scroll view so it should look like the image is repeating itself.
This is how i want them to be displayed under scroll view : image1|image2|image3|image4...... and these images will be scrolling from right to left. Exactly how it works in Behance's iphone app before you login.
Here's the code i have (in storyboard i have the scroll view and one UIIMageview already added).
override func viewDidAppear(_ animated: Bool) {
Timer.scheduledTimer(timeInterval: 0.6, target: self, selector: #selector(scrollImage), userInfo: nil, repeats: true)
}
#objc func scrollImage() {
offSet.x = offSet.x + CGFloat(20)
UIView.animate(withDuration: 1.0, animations: {
self.behanceView.setContentOffset(self.offSet, animated: false)
})
}
func addImagetoScrollView() {
let imageView = UIImageView(image:UIImage(named:"Landing_Scrollable"))
print(imageCount*Int(self.behanceView.contentSize.width)+100)
imageView.frame = CGRect(x:imageCount*Int(self.behanceView.contentSize.width), y: 0, width: 875, height: 502)
self.behanceView.contentSize = CGSize(width: imageView.bounds.size.width * CGFloat(imageCount), height: imageView.bounds.size.height)
self.behanceView.addSubview(imageView)
imageCount+=1
}
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollViewWidth = scrollView.frame.size.width;
let scrollOffset = scrollView.contentOffset.x;
print(imageCount*Int(self.behanceView.contentSize.width - scrollViewWidth))
if scrollOffset >= CGFloat(imageCount*Int(self.behanceView.contentSize.width - scrollViewWidth)) {
self.addImagetoScrollView()
}
}
}
But when i see it in action, it does something wierd and animation is all off.
Can someone please help.
Thanks,
I've never seen the “Behance” app, but I guess you're asking how to animate a seamlessly tiled background image across the screen indefinitely, like this:
(Pattern image by Evan Eckard.)
I used an animation duration of 1 second for the demo, but you probably want a much longer duration in a real app.
You shouldn't use a timer for this. Core Animation can perform the animation for you, and letting it perform the animation smoother and more efficient. (You might think Core Animation is performing your animation since you're using UIView animation, but I believe animating a scroll view's contentOffset does not use Core Animation because the scroll view has to call its delegate's scrollViewDidScroll on every animation step.)
You also shouldn't use a scroll view for this. UIScrollView exists to allow the user to scroll. Since you're not letting the user scroll, you shouldn't use UIScrollView.
Here's how you should set up your background:
Create two identical image views (numbered 0 and 1), showing the same image. Make sure the image views are each big enough to fill the screen.
Put the left edge of image view 0 at the left edge of your root view. Put the left edge of image view 1 at the right edge of image view 0. Since each image view is big enough to fill the screen, image view 1 will start out entirely off the right edge of the screen.
Animate image view 0's transform.translation.x from 0 to -imageView.bounds.size.width. This will make it slide to the left by precisely its own width, so when the animation reaches its end, image view 0's right edge is at the left edge of the screen (and thus image view 0 is entirely off the left edge of the screen). Set the animation's repeatCount to .infinity.
Add the same animation to image view 1. Thus image view 1 comes onto the screen as image view 0 is leaving it, exactly covering the pixels revealed by image view 0's animation.
The two animations end at exactly the same time. When they end, image view 1 is exactly where image view 0 was at the start. Since both animations are set to repeat infinitely, they both immediately start over. When image view 0's animation starts over, image view 0 instantly jumps back to its starting position, which is where image view 1 ended up. Since both image views show the same image, the pixels on screen don't change. This makes the animation loop seamless.
Here's my code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for imageView in imageViews {
imageView.image = patternImage
imageView.contentMode = .scaleToFill
view.addSubview(imageView)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let bounds = view.bounds
let patternSize = patternImage.size
// Scale the image up if necessary to be at least as big as the screen on both axes.
// But make sure scale is at least 1 so I don't shrink the image if it's larger than the screen.
let scale = max(1 as CGFloat, bounds.size.width / patternSize.width, bounds.size.height / patternSize.height)
let imageFrame = CGRect(x: 0, y: 0, width: scale * patternSize.width, height: scale * patternSize.height)
for (i, imageView) in imageViews.enumerated() {
imageView.frame = imageFrame.offsetBy(dx: CGFloat(i) * imageFrame.size.width, dy: 0)
let animation = CABasicAnimation(keyPath: "transform.translation.x")
animation.fromValue = 0
animation.toValue = -imageFrame.size.width
animation.duration = 1
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction(name: .linear)
// The following line prevents iOS from removing the animation when the app goes to the background.
animation.isRemovedOnCompletion = false
imageView.layer.add(animation, forKey: animation.keyPath)
}
}
private let imageViews: [UIImageView] = [.init(), .init()]
private let patternImage = #imageLiteral(resourceName: "pattern")
}
I have a custom UIView and I would like to animate its backgroundColor property. This is an animatable property of a UIView.
This is the code:
class ETTimerUIView: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// other methods
func flashBg() {
UIView.animateWithDuration( 1.0, animations: {
self.backgroundColor = UIColor.colorYellow()
})
}
override func drawRect() {
// Something related to a timer I'm rendering
}
This code causes causes the animation to skip and the color to change immediately:
self.backgroundColor = UIColor.colorYellow() // Changes immediately to yellow
If I animate alpha, this animates from 1 to 0 over one second as expected:
self.alpha = 0 // animates
How do I animate a background color change in this situation?
Implementing drawRect blocks backgroundColor animation, but no answer is provided yet.
Maybe this is why you can't combine drawRect and animateWithDuration, but I don't understand it much.
I guess I need to make a separate view--should this go in the storyboard in the same view controller? programmatically created?
Sorry, I'm really new to iOS and Swift.
It is indeed not working when I try it, I had a related question where putting the layoutIfNeeded() method inside the animation worked and made the view smoothly animating (move button towards target using constraints, no reaction?). But in this case, with the backgroundColor, it does not work. If someone knows the answer I will be interested to know.
But if you need a solution right now, you could create a UIView (programmatically or via the storyboard) that is used only as a container. Then you add 2 views inside : one on top, and one below, with the same frame as the container. And you only change the alpha of the top view, which let the user see the view behind :
class MyView : UIView {
var top : UIView!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.blueColor()
top = UIView(frame: CGRectMake(0,0, self.frame.width, self.frame.height))
top.backgroundColor = UIColor.yellowColor()
self.addSubview(top)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let sub = UIView(frame: CGRectMake(0,0, self.frame.width, self.frame.height))
sub.backgroundColor = UIColor.purpleColor()
self.sendSubviewToBack(sub)
UIView.animateWithDuration(1, animations: { () -> Void in
self.top.alpha = 0
}) { (success) -> Void in
println("anim finished")
}
}
}
The answer is that you cannot animate backgroundColor of a view that implements drawRect. I do not see docs for this anywhere (please comment if you know of one).
You can't animate it with animateWithDuration, nor with Core Animation.
This thread has the best explanation I've found yet:
When you implement -drawRect:, the background color of your view is then drawn into the associated CALayer, rather than just being set on the CALayer as a style property... thus prevents you from getting a contents crossfade
The solution, as #Paul points out, is to add another view above, behind, or wherever, and animate that. This animates just fine.
Would love a good understanding of why it is this way and why it silently swallows the animation instead of hollering.
Not sure if this will work for you, but to animate the background color of a UIView I add this to a UIView extension:
extension UIView {
/// Pulsates the color of the view background to white.
///
/// Set the number of times the animation should repeat, or pass
/// in `Float.greatestFiniteMagnitude` to pulsate endlessly.
/// For endless animations, you need to manually remove the animation.
///
/// - Parameter count: The number of times to repeat the animation.
///
func pulsate(withRepeatCount count: Float = 1) {
let pulseAnimation = CABasicAnimation(keyPath: "backgroundColor")
pulseAnimation.fromValue = <#source UIColor#>.cgColor
pulseAnimation.toValue = <#target UIColor#>.cgcolor
pulseAnimation.duration = 0.4
pulseAnimation.autoreverses = true
pulseAnimation.repeatCount = count
pulseAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
self.layer.add(pulseAnimation, forKey: "Pulsate")
CATransaction.commit()
}
}
When pasting this in to a source file in Xcode, replace the placeholders with your two desired colors. Or you can replace the entire lines with something like these values:
pulseAnimation.fromValue = backgroundColor?.cgColor
pulseAnimation.toValue = UIColor.white.cgColor
I have a zooming UIScrollView, and a non-zooming overlay view on which I animate markers. These markers need to track the location of some of the content of the UIScrollView (similar to the way a dropped pin needs to track a spot on the map as you pan and zoom).
I do so by triggering an update of the overlay view in response to the UIScrollView's layoutSubviews. This works, and the overlay tracks perfectly when zooming and panning.
But when the pinch gesture ends the UIScrollView automatically performs a final animation, and the overlay view is out of sync for the duration of this animation.
I made a simplified project to isolate this problem. The UIScrollView contains an orange square, and the overlay view displays a 2-pixel red outline around the frame of this orange square. As you can see below, the red outline always moves to where it should be, except for a short period of time after touch ends, when it visibly jumps ahead to the final position of the orange square.
The full Xcode project for this test is available here: https://github.com/Clafou/ScrollviewZoomTrackTest but all the code is in the two files shown below:
TrackedScrollView.swift:
class TrackedScrollView: UIScrollView {
#IBOutlet var overlaysView: UIView?
let square: UIView
required init(coder aDecoder: NSCoder) {
square = UIView(frame: CGRect(x: 50, y: 300, width: 300, height: 300))
square.backgroundColor = UIColor.orangeColor()
super.init(coder: aDecoder)
self.addSubview(square)
self.maximumZoomScale = 1
self.minimumZoomScale = 0.5
self.contentSize = CGSize(width: 500, height: 900)
self.delegate = self
}
override func layoutSubviews() {
super.layoutSubviews()
overlaysView?.setNeedsLayout()
}
}
extension TrackedScrollView: UIScrollViewDelegate {
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return square
}
}
OverlaysView.swift:
class OverlaysView: UIView {
#IBOutlet var trackedScrollView: TrackedScrollView?
let outline: CALayer
required init(coder aDecoder: NSCoder) {
outline = CALayer()
outline.borderColor = UIColor.redColor().CGColor
outline.borderWidth = 2
super.init(coder: aDecoder)
self.layer.addSublayer(outline)
}
override func layoutSubviews() {
super.layoutSubviews()
if let trackedScrollView = self.trackedScrollView {
CATransaction.begin()
CATransaction.setDisableActions(true)
let frame = trackedScrollView.convertRect(trackedScrollView.square.frame, toView: self)
outline.frame = CGRectIntegral(CGRectInset(frame, -3, -3))
CATransaction.commit()
}
}
}
Among the things I tried was using a CADisplayLink and presentationLayer and this allowed me to animate the overlay, but the coordinates that I obtained from presentationLayer lagged slightly behind the actual UIScrollView, so this still didn't look right. I think the right approach would be to tie my overlay update to the system-created UIScrollView animation, but I haven't had success hacking this so far.
How can I update this code to always track the UIScrollView's zooming content?
UIScrollView sends scrollViewDidZoom: to its delegate in its animation block, if it's “bouncing” back to its minimum or maximum zoom when the pinch ends. Update the frames of your overlays in scrollViewDidZoom: if zoomBouncing is true. If you're using Auto Layout call layoutIfNeeded.
scrollViewDidZoom: is only called once during the zoom bounce animation but adjusting your frames or calling layoutIfNeeded will ensure these changes are animated along with the zoom bounce, thus keeping them perfectly in sync.
Demo:
Fixed sample project: https://github.com/mayoff/ScrollviewZoomTrackTest