How do I remove a CAShapeLayer and CABasicAnimation from my UIView? - swift

This is my code:
#objc func drawForm() {
i = Int(arc4random_uniform(UInt32(formNames.count)))
var drawPath = actualFormNamesFromFormClass[i]
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 6
shapeLayer.frame = CGRect(x: -115, y: 280, width: 350, height: 350)
var paths: [UIBezierPath] = drawPath()
let shapeBounds = shapeLayer.bounds
let mirror = CGAffineTransform(scaleX: 1,
y: -1)
let translate = CGAffineTransform(translationX: 0,
y: shapeBounds.size.height)
let concatenated = mirror.concatenating(translate)
for path in paths {
path.apply(concatenated)
}
guard let path = paths.first else {
return
}
paths.dropFirst()
.forEach {
path.append($0)
}
shapeLayer.transform = CATransform3DMakeScale(0.6, 0.6, 0)
shapeLayer.path = path.cgPath
self.view.layer.addSublayer(shapeLayer)
strokeEndAnimation.duration = 30.0
strokeEndAnimation.fromValue = 0.0
strokeEndAnimation.toValue = 1.0
shapeLayer.add(strokeEndAnimation, forKey: nil)
}
This code animates the drawing of the shapeLayer path, however I can't find anything online about removing this layer and stopping this basic animation or removing the cgPath that gets drawn... Any help would be greatly appreciated!

You said:
I can't find anything online about removing this layer ...
It is removeFromSuperlayer().
shapeLayer.removeFromSuperlayer()
You go on to say:
... and stopping this basic animation ...
It is removeAllAnimations:
shapeLayer.removeAllAnimations()
Note, this will immediately change the strokeEnd (or whatever property you were animating) back to its previous value. If you want to "freeze" it where you stopped it, you have to grab the presentation layer (which captures the layer's properties as they are mid-animation), save the appropriate property, and then update the property of the layer upon which you are stopping the animation:
if let strokeEnd = shapeLayer.presentation()?.strokeEnd {
shapeLayer.removeAllAnimations()
shapeLayer.strokeEnd = strokeEnd
}
Finally, you go on to say:
... or removing the cgPath that gets drawn.
Just set it to nil:
shapeLayer.path = nil
By the way, when you're browsing for the documentation for CAShapeLayer and CABasicAnimation, don't forget to check out the documentation for their superclasses, namely and CALayer and CAAnimation ยป CAPropertyAnimation, respectively. Bottom line, when digging around looking for documentation on properties or methods for some particular class, you often will have to dig into the superclasses to find the relevant information.
Finally, the Core Animation Programming Guide is good intro and while its examples are in Objective-C, all of the concepts are applicable to Swift.

You can use an animation delegate CAAnimationDelegate to execute additional logic when an animation starts or ends. For example, you may want to remove a layer from its parent once a fade out animation has completed.
Below code taken from a class that implements CAAnimationDelegate on the layer, when you call The fadeOut function animates the opacity of that layer and, once the animation has completed, animationDidStop(_:finished:) removes it from its superlayer.
extension CALayer : CAAnimationDelegate {
func fadeOut() {
let fadeOutAnimation = CABasicAnimation()
fadeOutAnimation.keyPath = "opacity"
fadeOutAnimation.fromValue = 1
fadeOutAnimation.toValue = 0
fadeOutAnimation.duration = 0.25
fadeOutAnimation.delegate = self
self.add(fadeOutAnimation,
forKey: "fade")
}
public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
self.removeFromSuperlayer()
}
}

Related

Entity-Component in Swift

I am trying to build a simple iOS game using entity-component architecture similar to what is described here.
What I would like to achieve in my game is when a user touches the screen, detect where the touch occurred and move all entities of one type towards a specific direction (direction depends on where the user touched, right of screen = up, left of screen = down).
So far, the game is really simple and I am only getting started, but I am stuck in this simple functionality:
My issue is that an SKAction is supposed to run on all entities of a type, but happens at all.
Before I redesigned my game to an ECS approach, this worked fine.
Here is the GKEntity subclass that I declared in Lines.swift:
class Lines: GKEntity {
override init() {
super.init()
let LineSprite = SpriteComponent(color: UIColor.white, size: CGSize(width: 10.0, height: 300))
addComponent(LineSprite)
// Set physics body
if let sprite = component(ofType: SpriteComponent.self)?.node {
sprite.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: sprite.size.width, height: sprite.size.height))
sprite.physicsBody?.isDynamic = false
sprite.physicsBody?.restitution = 1.0
sprite.physicsBody?.friction = 0.0
sprite.physicsBody?.linearDamping = 0.0
sprite.physicsBody?.angularDamping = 0.0
sprite.physicsBody?.mass = 0.00
sprite.physicsBody?.affectedByGravity = false
sprite.physicsBody?.usesPreciseCollisionDetection = true
sprite.physicsBody?.categoryBitMask = 0b1
sprite.zPosition = 10
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In TouchesBegan I am calling the function Move(XAxisPoint: t.location(in: self)) which is declared in GameScene and here is what Move() does:
///Determines direction of movement based on touch location, calls MoveUpOrDown for movement
func move(XAxisPoint: CGPoint){
let Direction: SKAction
let Key: String
if XAxisPoint.x >= 0 {
Direction = SKAction.moveBy(x: 0, y: 3, duration: 0.01)
Key = "MovingUp"
} else {
Direction = SKAction.moveBy(x: 0, y: -3, duration: 0.01)
Key = "MovingDown"
}
moveUpOrDown(ActionDirection: Direction, ActionKey: Key)
}
///Moves sprite on touch
func moveUpOrDown(ActionDirection: SKAction, ActionKey: String) {
let Line = Lines()
if let sprite = Line.component(ofType: SpriteComponent.self)?.node {
if sprite.action(forKey: ActionKey) == nil {
stopMoving()
let repeatAction = SKAction.repeatForever(ActionDirection)
sprite.run(repeatAction, withKey: ActionKey)
}
}
}
///Stops movement
func stopMoving() {
let Line = Lines()
if let sprite = Line.component(ofType: SpriteComponent.self)?.node {
sprite.removeAllActions()
}
}
I am guessing there is some issue with this line of code Line.component(ofType: SpriteComponent.self)?.node but the compiler doesn't throw any errors and I am not sure where my mistake is.
Any help/guidance will be greatly appreciated!
The issue is the following line in MoveUpOrDown and StopMoving
let Line = Lines()
It's creating a new Lines object then telling it to run an action. Since it's new, it hasn't been added to the scene so it isn't drawn or acted on.
You should be getting an existing Lines object and modifying that instead of creating a new one.
As a side note, the common convention for naming methods and variables is to use camelCase which means MoveUpOrDown should be moveUpOrDown. On the other hand SnakeCase is used For classes structs and protocols so SpriteComponent is current. That allows you to know at a glance whether your working with a type or a variable.

Drawing a CAShapeLayer using autolayout

I am trying to draw and animate a circular button using CAShapeLayer but just the drawing gives me a lot of headache - I can't seem to figure out how to pass data into my class.
This is my setup:
- a class of type UIView which will draw the CAShapeLayer
- the view is rendered in my view controller and built using auto layout constraints
I have tried using layoutIfNeeded but seem to be passing the data too late for the view to be drawn. I have also tried redrawing the view in vieWillLayoutSubviews() but nothing. Example code below. What am I doing wrong?
Am I passing the data too early/too late?
Am I drawing the bezierPath too late?
I'd highly appreciate pointers.
And maybe a second follow up question: is there a simpler way to draw a circular path that is bound to it's views size?
In my View Controller:
import UIKit
class ViewController: UIViewController {
let buttonView: CircleButton = {
let view = CircleButton()
view.backgroundColor = .black
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewWillLayoutSubviews() {
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(buttonView)
buttonView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
buttonView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
buttonView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.75).isActive = true
buttonView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.25).isActive = true
buttonView.layoutIfNeeded()
buttonView.arcCenter = buttonView.center
buttonView.radius = buttonView.frame.width/2
}
override func viewDidAppear(_ animated: Bool) {
print(buttonView.arcCenter)
print(buttonView.radius)
}
}
And the class for the buttonView:
class CircleButton: UIView {
//Casting outer circular layers
let trackLayer = CAShapeLayer()
var arcCenter = CGPoint()
var radius = CGFloat()
//UIView Init
override init(frame: CGRect) {
super.init(frame: frame)
}
//UIView post init
override func layoutSubviews() {
super.layoutSubviews()
print("StudyButtonView arcCenter \(arcCenter)")
print("StudyButtonView radius \(radius)")
layer.addSublayer(trackLayer)
let outerCircularPath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
trackLayer.path = outerCircularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 5
trackLayer.strokeStart = 0
trackLayer.strokeEnd = 1
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.transform = CATransform3DMakeRotation(-CGFloat.pi/2, 0, 0, 1)
}
//Required for subclass
required init?(coder aDecoder: NSCoder) {
fatalError("has not been implemented")
}
}
There really isn't any correlation between auto-layout and the proper implementation of your CircleButton class. Your CircleButton class doesn't know or care whether it's being configured via auto-layout or whether it has some fixed size.
Your auto-layout code looks OK (other than points 5 and 6 below). Most of the issues in your code snippet rest in your CircleButton class. A couple of observations:
If you're going to rotate the shape layer, you have to set its frame, too, otherwise the size is .zero and it's going to end up rotating it about the origin of the view (and rotate outside of the bounds of the view, especially problematic if you're clipping subviews). Make sure to set the frame of the CAShapeLayer to be the bounds of the view before trying to rotate it. Frankly, I'd remove the transform, but given that you're playing around with strokeStart and strokeEnd, I'm guessing you may want to change these values later and have it start at 12 o'clock, in which case the transform makes sense.
Bottom line, if rotating, set the frame first. If not, setting the layer's frame is optional.
If you're going to change the properties of the view in order to update the shape layer, you'll want to make sure that the didSet observers do the appropriate updating of the shape layer (or call setNeedsLayout). You don't want your view controller from having to mess around with the internals of the shape layer, but you also want to make sure that these changes do get reflected in the shape layer.
It's a minor observation, but I'd suggest adding the shape layer during init and only configuring and adding it to the view hierarchy once. This is more efficient. So, have the various init methods call your own configure method. Then, do size-related stuff (like updating the path) in layoutSubviews. Finally, have properties observers that update the shape layer directly. This division of labor is more efficient.
If you want, you can make this #IBDesignable and put it in its own target in your project. Then you can add it right in IB and see what it will look like. You can also make all the various properties #IBInspectable, and you'll be able to set them right in IB, too. You then don't have to do anything in the code of your view controller if you don't want to. (But if you want to, feel free.)
A minor issue, but when you add your view programmatically, you don't need to call buttonView.layoutIfNeeded(). You only need to do that if you're animating constraints, which you're not doing here. Once you add the constraints (and fix the above issues), the button will be laid out correctly, with no explicit layoutIfNeeded required.
Your view controller has a line of code that says:
buttonView.arcCenter = buttonView.center
That is conflating arcCenter (which is a coordinate within the buttonView's coordinate space) and buttonView.center (which is the coordinate for the button's center within the view controller's root view's coordinate space). One has nothing to do with the other. Personally, I'd get rid of this manual setting of arcCenter, and instead have layoutSubviews in ButtonView take care of this dynamically, using bounds.midX and bounds.midY.
Pulling that all together, you get something like:
#IBDesignable
class CircleButton: UIView {
private let trackLayer = CAShapeLayer()
#IBInspectable var lineWidth: CGFloat = 5 { didSet { updatePath() } }
#IBInspectable var fillColor: UIColor = .clear { didSet { trackLayer.fillColor = fillColor.cgColor } }
#IBInspectable var strokeColor: UIColor = .lightGray { didSet { trackLayer.strokeColor = strokeColor.cgColor } }
#IBInspectable var strokeStart: CGFloat = 0 { didSet { trackLayer.strokeStart = strokeStart } }
#IBInspectable var strokeEnd: CGFloat = 1 { didSet { trackLayer.strokeEnd = strokeEnd } }
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
trackLayer.fillColor = fillColor.cgColor
trackLayer.strokeColor = strokeColor.cgColor
trackLayer.strokeStart = strokeStart
trackLayer.strokeEnd = strokeEnd
layer.addSublayer(trackLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
private func updatePath() {
let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = (min(bounds.width, bounds.height) - lineWidth) / 2
trackLayer.lineWidth = lineWidth
trackLayer.path = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
// There's no need to rotate it if you're drawing a complete circle.
// But if you're going to transform, set the `frame`, too.
trackLayer.transform = CATransform3DIdentity
trackLayer.frame = bounds
trackLayer.transform = CATransform3DMakeRotation(-.pi / 2, 0, 0, 1)
}
}
That yields:
Or you can tweak the settings right in IB, and you'll see it take effect:
And having made sure that all of the didSet observers for the properties of ButtonView either update the path or directly update some shape layer, the view controller can now update these properties and they'll automatically be rendered in the ButtonView.
The main issue that I see in your code is that you are adding the layer inside -layoutSubviews, this method is called multiple times during a view lifecycle.
If you don't want to make the view hosted layer a CAShapeLayer by using the layerClass property, you need to override the 2 init methods (frame and coder) and call a commonInit where you instantiate and add your CAShape layer as a sublayer.
In -layoutSubviews just set the frame property of it and the path according to the new view size.

Swift: Cant change CALayer background color in array

I am simply trying to change the background color of the last element of a CALayer array. Here is my entire View Class, however its only 2-3 lines that I actually try to access the last element of the CALayer.
Here is my progressViewClass and I put comments to where exactly my problem is:
class ProgressBarView: UIView {
//Variables for progress bar
var holdGesture = UILongPressGestureRecognizer()
let animation = CABasicAnimation(keyPath: "bounds.size.width")
var layerHolder = [CALayer]()
var widthIndex = CGPoint(x: 0, y: 0)
var nextXOffset = CGFloat(0.0)
var checkIfFull = CGFloat()
var newLayer : CALayer?
var progressBarAnimationDuration : CFTimeInterval = (MainController.sharedInstance.totalMiliSeconsToRecord / 10)
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
}
func startProgressBar(){
if(RecordingViewController().currentCameraMode == .recordingMode || RecordingViewController().currentCameraMode == .record1stClipMode) {
newLayer = CALayer()
newLayer?.frame = CGRect(x: nextXOffset, y: 0, width: 0, height: self.bounds.height)
newLayer?.backgroundColor = UIColor(red:0.82, green:0.01, blue:0.11, alpha:1.0).cgColor
//print("before \(nextXOffset)")
newLayer?.anchorPoint = widthIndex
animation.fromValue = 0
animation.toValue = self.bounds.width - nextXOffset
animation.duration = progressBarAnimationDuration - ((MainController.sharedInstance.miliSecondsPassed) / 10)
self.layer.addSublayer(newLayer!)
//print("Long Press Began")
newLayer?.add(animation, forKey: "bounds.size.width")
}
else{
stopProgressBar()
}
}
func stopProgressBar(){
if(RecordingViewController().currentCameraMode != .recordingMode){
pauseLayer(layer: newLayer!)
newLayer?.frame = (newLayer?.presentation()!.frame)!
nextXOffset = (newLayer?.frame.maxX)!
layerHolder.append(newLayer!)
print("Layerholder has elements : \(layerHolder.count)")
}
}
// HERE IS MY PROBLEM
func highlightLastLayer(){
print("in highlight last layer Layerholder has elements : \(layerHolder.count)")
// I CAN HIDE THE CALAYER SO I BELIEVE IM ACCESSING THE CORRECT LAYER
// layerHolder.last?.isHidden = true
// This is suppose to change the last element background color to blue but doesnt
layerHolder.last?.backgroundColor = UIColor.blue.cgColor
}
// ALSO MY PROBLEM
func unhighlightLastLayer(){
print("inside unhighlight last layer")
// I CAN HIDE THE CALAYER SO I BELIEVE IM ACCESSING THE CORRECT LAYER
//layerHolder.last?.isHidden = false
// Changes CALayer back to red
layerHolder.last?.backgroundColor = UIColor(red:0.82, green:0.01, blue:0.11, alpha:1.0).cgColor
}
//Function to pause the Progress Bar
func pauseLayer(layer : CALayer){
let pausedTime : CFTimeInterval = layer.convertTime(CACurrentMediaTime(), from: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
}
}
Simply put, I create a progressView object in my viewController and then call those functions based on certain button input. This view is essentially a progress bar that you'd see in many video recording applications to show how much you have recorded. In the highlightLastLayer, I am trying to grab the last element of the "layerHolder" array and change its color to blue. Simple right? Doesn't work. Any ideas?
Where are you calling highlight and unhighlight. I am pretty sure you are doing this when you are "stopped" or "paused" because thats the only time you add anything to the layerHolder Array. You can only do this when you are not animating because when you are animating the presentation layer is shown instead of the "real" layer. Instead of setting the speed to zero, setup your layer to look like the current animation state and call layer. removeAllAnimations to kill the presentation layer and show the actual layer for your view instead. Now you can make all the changes that you want and they will actually show up.

UIView custom transition snaps back on completion

I have implemented a class BubbleAnimator, that should create a bubble-like transition between views and added it through the UIViewControllerTransitioningDelegate-protocol. The presenting animation works fine so far (that's why I haven't added all the code for this part).
But on dismissing the view, the 'fromViewController' flashes up at the very end of the animation. After this very short flash, the correct toViewController is displayed again, but this glitch is very annoying.
The following is the relevant animateTransition-method:
//Get all the necessary views from the context
let containerView = transitionContext.containerView()
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
//Presenting
if self.reverse == false {
//Add the destinationvc as subview
containerView!.addSubview(fromViewController!.view)
containerView!.addSubview(toViewController!.view)
/*...Animating the layer goes here... */
//Dismissing
} else {
containerView!.addSubview(toViewController!.view)
containerView!.addSubview(fromViewController!.view)
//Init the paths
let circleMaskPathInitial = UIBezierPath(ovalInRect: self.originFrame)
let extremePoint = CGPoint(x: originFrame.origin.x , y: originFrame.origin.y - CGRectGetHeight(toViewController!.view.bounds) )
let radius = sqrt((extremePoint.x*extremePoint.x) + (extremePoint.y*extremePoint.y))
let circleMaskPathFinal = UIBezierPath(ovalInRect: CGRectInset(originFrame, -radius, -radius))
//Create a layer
let maskLayer = CAShapeLayer()
maskLayer.path = circleMaskPathFinal.CGPath
fromViewController!.view.layer.mask = maskLayer
//Create and add the animation
let animation = CABasicAnimation(keyPath: "path")
animation.toValue = circleMaskPathInitial.CGPath
animation.fromValue = circleMaskPathFinal.CGPath
animation.duration = self.transitionDuration(transitionContext)
animation.delegate = self
maskLayer.addAnimation(animation, forKey: "path")
}
The cleanup takes place in the delegate method:
override public func animationDidStop(anim: CAAnimation, finished flag: Bool) {
self.transitionContext?.completeTransition(!(self.transitionContext?.transitionWasCancelled())!)
}
I guess, that I am doing something wrong with adding the views to the containerView, but I couldn't figure it out. Another possibility is, that the view's layer mask gets reset, when the function completeTransition is called.
Thanks to this blogpost I have finally been able to solve this problem. Short explanation:
The CAAnimation only manipulates the presentation-layer of the view, but does not change the model-layer. When the animation now finishes, it's value snaps back to the original and unchanged value of the model-layer.
Short and simply workaround:
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
The better solution, as it doesn't prevent the animation from being removed is to manually set the final value of the layer's position before the animation starts. This way, the model-layer is assigned the correct value:
maskLayer.path = circleMaskPathInitial.CGPath
//Create and add the animation below..

How to set the BlurRadius of UIBlurEffectStyle.Light

I was wondering how to set the radius/blur factor of iOS new UIBlurEffectStyle.Light? I could not find anything in the documentation. But I want it to look similar to the classic UIImage+ImageEffects.h blur effect.
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let blur = UIBlurEffect(style: UIBlurEffectStyle.Light)
let effectView = UIVisualEffectView(effect: blur)
effectView.frame = frame
addSubview(effectView)
}
Changing alpha is not a perfect solution. It does not affect blur intensity. You can setup an animation from nil to target blur effect and manually set time offset to get desired blur intensity. Unfortunately iOS will reset the animation offset when app returns from background.
Thankfully there is a simple solution that works on iOS >= 10. You can use UIViewPropertyAnimator. I didn't notice any issues with using it. I keeps custom blur intensity when app returns from background. Here is how you can implement it:
class CustomIntensityVisualEffectView: UIVisualEffectView {
/// Create visual effect view with given effect and its intensity
///
/// - Parameters:
/// - effect: visual effect, eg UIBlurEffect(style: .dark)
/// - intensity: custom intensity from 0.0 (no effect) to 1.0 (full effect) using linear scale
init(effect: UIVisualEffect, intensity: CGFloat) {
super.init(effect: nil)
animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in self.effect = effect }
animator.fractionComplete = intensity
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
// MARK: Private
private var animator: UIViewPropertyAnimator!
}
I also created a gist: https://gist.github.com/darrarski/29a2a4515508e385c90b3ffe6f975df7
You can change the alpha of the UIVisualEffectView that you add your blur effect to.
let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.Light)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.alpha = 0.5
blurEffectView.frame = self.view.bounds
self.view.addSubview(blurEffectView)
This is not a true solution, as it doesn't actually change the radius of the blur, but I have found that it gets the job done with very little work.
Although it is a hack and probably it won't be accepted in the app store, it is still possible. You have to subclass the UIBlurEffect like this:
#import <objc/runtime.h>
#interface UIBlurEffect (Protected)
#property (nonatomic, readonly) id effectSettings;
#end
#interface MyBlurEffect : UIBlurEffect
#end
#implementation MyBlurEffect
+ (instancetype)effectWithStyle:(UIBlurEffectStyle)style
{
id result = [super effectWithStyle:style];
object_setClass(result, self);
return result;
}
- (id)effectSettings
{
id settings = [super effectSettings];
[settings setValue:#50 forKey:#"blurRadius"];
return settings;
}
- (id)copyWithZone:(NSZone*)zone
{
id result = [super copyWithZone:zone];
object_setClass(result, [self class]);
return result;
}
#end
Here blur radius is set to 50. You can change 50 to any value you need.
Then just use MyBlurEffect class instead of UIBlurEffect when creating your effect for UIVisualEffectView.
Recently developed Bluuur library to dynamically change blur radius of UIVisualEffectsView without usage any of private APIs: https://github.com/ML-Works/Bluuur
It uses paused animation of setting effect to achieve changing radius of blur. Solution based on this gist: https://gist.github.com/n00neimp0rtant/27829d87118d984232a4
And the main idea is:
// Freeze animation
blurView.layer.speed = 0;
blurView.effect = nil;
[UIView animateWithDuration:1.0 animations:^{
blurView.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
}];
// Set animation progress from 0 to 1
blurView.layer.timeOffset = 0.5;
UPDATE:
Apple introduced UIViewPropertyAnimator class in iOS 10. Thats what we need exactly to animate .effect property of UIVisualEffectsView. Hope community will be able to back-port this functionality to previous iOS version.
This is totally doable. Use CIFilter in CoreImage module to customize blur radius. In fact, you can even achieve a blur effect with continuous varying (aka gradient) blur radius (https://stackoverflow.com/a/51603339/3808183)
import CoreImage
let ciContext = CIContext(options: nil)
guard let inputImage = CIImage(image: yourUIImage),
let mask = CIFilter(name: "CIGaussianBlur") else { return }
mask.setValue(inputImage, forKey: kCIInputImageKey)
mask.setValue(10, forKey: kCIInputRadiusKey) // Set your blur radius here
guard let output = mask.outputImage,
let cgImage = ciContext.createCGImage(output, from: inputImage.extent) else { return }
outUIImage = UIImage(cgImage: cgImage)
I'm afraid there's no such api currently. According to Apple's way of doing things, new functionality was always brought with restricts, and capabilities will bring out gradually. Maybe that will be possible on iOS 9 or maybe 10...
I have ultimate solution for this question:
fileprivate final class UIVisualEffectViewInterface {
func setIntensity(effectView: UIVisualEffectView, intensity: CGFloat){
let effect = effectView.effect
effectView.effect = nil
animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [weak effectView] in effectView?.effect = effect }
animator.fractionComplete = intensity
}
private var animator: UIViewPropertyAnimator! }
extension UIVisualEffectView{
private var key: UnsafeRawPointer? { UnsafeRawPointer(bitPattern: 16) }
private var interface: UIVisualEffectViewInterface{
if let key = key, let visualEffectViewInterface = objc_getAssociatedObject(self, key) as? UIVisualEffectViewInterface{
return visualEffectViewInterface
}
let visualEffectViewInterface = UIVisualEffectViewInterface()
if let key = key{
objc_setAssociatedObject(self, key, visualEffectViewInterface, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
return visualEffectViewInterface
}
func intensity(_ value: CGFloat){
interface.setIntensity(effectView: self, intensity: value)
}}
This idea hits me after tried the above solutions, a little hacky but I got it working. Since we cannot modify the default radius which is set as "50", we can just enlarge it and scale it back down.
previewView.snp.makeConstraints { (make) in
make.centerX.centerY.equalTo(self.view)
make.width.height.equalTo(self.view).multipliedBy(4)
}
previewBlur.snp.makeConstraints { (make) in
make.edges.equalTo(previewView)
}
And then,
previewView.transform = CGAffineTransform(scaleX: 0.25, y: 0.25)
previewBlur.transform = CGAffineTransform(scaleX: 0.25, y: 0.25)
I got a 12.5 blur radius. Hope this will help :-)
Currently I didn't find any solution.
By the way you can add a little hack in order to let blur mask less "blurry", in this way:
let blurView = .. // here create blur view as usually
if let blurSubviews = self.blurView?.subviews {
for subview in blurSubviews {
if let filterView = NSClassFromString("_UIVisualEffectFilterView") {
if subview.isKindOfClass(filterView) {
subview.hidden = true
}
}
}
}
for iOS 11.*
in viewDidLoad()
let blurEffect = UIBlurEffect(style: .dark)
let blurEffectView = UIVisualEffectView()
view.addSubview(blurEffectView)
//always fill the view
blurEffectView.frame = self.view.bounds
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
UIView.animate(withDuration: 1) {
blurEffectView.effect = blurEffect
}
blurEffectView.pauseAnimation(delay: 0.5)
There is an undocumented way to do this. Not necessarily recommended, as it may get your app rejected by Apple. But it does work.
if let blurEffectType = NSClassFromString("_UICustomBlurEffect") as? UIBlurEffect.Type {
let blurEffectInstance = blurEffectType.init()
// set any value you want here. 40 is quite blurred
blurEffectInstance.setValue(40, forKey: "blurRadius")
let effectView: UIVisualEffectView = UIVisualEffectView(effect: blurEffectInstance)
// Now you have your blurred visual effect view
}
This works for me.
I put UIVisualEffectView in an UIView before add to my view.
I make this function to use easier. You can use this function to make blur any area in your view.
func addBlurArea(area: CGRect) {
let effect = UIBlurEffect(style: UIBlurEffectStyle.Dark)
let blurView = UIVisualEffectView(effect: effect)
blurView.frame = CGRect(x: 0, y: 0, width: area.width, height: area.height)
let container = UIView(frame: area)
container.alpha = 0.8
container.addSubview(blurView)
self.view.insertSubview(container, atIndex: 1)
}
For example, you can make blur all of your view by calling:
addBlurArea(self.view.frame)
You can change Dark to your desired blur style and 0.8 to your desired alpha value
If you want to accomplish the same behaviour as iOS spotlight search you just need to change the alpha value of the UIVisualEffectView (tested on iOS9 simulator)