Swift: Cant change CALayer background color in array - swift

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.

Related

UIView replicate CAAnimation of another view, in real time?

So I've got a background view with a gradient sublayer, animating continuously to change the colors slowly. I'm doing it with a CATransaction, because I need to animate other properties as well:
CATransaction.begin()
gradientLayer.add(colorAnimation, forKey: "colors")
// other animations
CATransaction.setCompletionBlock({
// start animation again, loop forever
}
CATransaction.commit()
Now I want to replicate this gradient animation, let's say, for the title of a button for instance.
Note 1: I can't just "make a hole" in the button, if such a thing is possible, because I might have other opaque views between the button and the background.
Note 2: The gradient position on the button is not important. I don't want the text gradient to replicate the exact colors underneath, but rather to mimic the "mood" of the background.
So when the button is created, I add its gradient sublayer to a list of registered layers, that the background manager will update as well:
func register(layer: CAGradientLayer) {
let pointer = Unmanaged.passUnretained(layer).toOpaque()
registeredLayers.addPointer(pointer)
}
So while it's easy to animate the text gradient at the next iteration of the animation, I would prefer that the button starts animating as soon as it's added, since the animation usually takes a few seconds. How can I copy the background animation, i.e. set the text gradient to the current state of the background animation, and animate it with the right duration left and timing function?
The solution was indeed to use the beginTime property, as suggested by #Shivam Gaur's comment. I implemented it as follows:
// The background layer, with the original animation
var backgroundLayer: CAGradientLayer!
// The animation
var colorAnimation: CABasicAnimation!
// Variable to store animation begin time
var animationBeginTime: CFTimeInterval!
// Registered layers replicating the animation
private var registeredLayers: NSPointerArray = NSPointerArray.weakObjects()
...
// Somewhere in our code, the setup function
func setup() {
colorAnimation = CABasicAnimation(keyPath: "colors")
// do the animation setup here
...
}
...
// Called by an external class when we add a view that should replicate the background animation
func register(layer: CAGradientLayer) {
// Store a pointer to the layer in our array
let pointer = Unmanaged.passUnretained(layer).toOpaque()
registeredLayers.addPointer(pointer)
layer.colors = colorAnimation.toValue as! [Any]?
// HERE'S THE KEY: We compute time elapsed since the beginning of the animation, and start the animation at that time, using 'beginTime'
let timeElapsed = CACurrentMediaTime() - animationBeginTime
colorAnimation.beginTime = -timeElapsed
layer.add(colorAnimation, forKey: "colors")
colorAnimation.beginTime = 0
}
// The function called recursively for an endless animation
func animate() {
// Destination layer
let toLayer = newGradient() // some function to create a new color gradient
toLayer.frame = UIScreen.main.bounds
// Setup animation
colorAnimation.fromValue = backgroundLayer.colors;
colorAnimation.toValue = toLayer.colors;
// Update background layer
backgroundLayer.colors = toLayer.colors
// Update registered layers (iterate is a custom function I declared as an extension of NSPointerArray)
registeredLayers.iterate() { obj in
guard let layer = obj as? CAGradientLayer else { return }
layer.colors = toLayer.colors
}
CATransaction.begin()
CATransaction.setCompletionBlock({
animate()
})
// Add animation to background
backgroundLayer.add(colorAnimation, forKey: "colors")
// Store starting time
animationBeginTime = CACurrentMediaTime();
// Add animation to registered layers
registeredLayers.iterate() { obj in
guard let layer = obj as? CAGradientLayer else { return }
layer.add(colorAnimation, forKey: "colors")
}
CATransaction.commit()
}

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

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()
}
}

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 do I make a UIScrollView scrollable only when touches are inside a custom shape?

I am working on creating an image collage app. And I am going to have multiple UIScrollView's. The scroll views will have boundaries with custom shapes and the user will be able to dynamically change the corners of the shapes where they intersect. The scroll views have UIImageView's as subviews.
The scroll views are subviews of other UIView's. I applied a CAShapeLayer mask to each of these UIView's. That way I can mask the scroll views with no problem.
But the problem is that, I can only scroll the contents of the last scroll view added. Also, I can pan and zoom beyond the boundaries of the masks. I should only able to pan or zoom when I am touching inside the boundaries of the polygons that I have as masks.
I tried;
scrollView.clipsToBounds = true
scrollView.layer.masksToBounds = true
But the result is the same.
Unfortunately I'm not able to post screenshots but, here is the code that I use to create masks for the UIViews:
func createMask(v: UIView, viewsToMask: [UIView], anchorPoint: CGPoint)
{
let frame = v.bounds
var shapeLayer = [CAShapeLayer]()
var path = [CGMutablePathRef]()
for i in 0...3 {
path.append(CGPathCreateMutable())
shapeLayer.append(CAShapeLayer())
}
//define frame constants
let center = CGPointMake(frame.origin.x + frame.size.width / 2, frame.origin.y + frame.size.height / 2)
let bottomLeft = CGPointMake(frame.origin.x, frame.origin.y + frame.size.height)
let bottomRight = CGPointMake(frame.origin.x + frame.size.width, frame.origin.y + frame.size.height)
switch frameType {
case 1:
// First view for Frame Type 1
CGPathMoveToPoint(path[0], nil, 0, 0)
CGPathAddLineToPoint(path[0], nil, bottomLeft.x, bottomLeft.y)
CGPathAddLineToPoint(path[0], nil, anchorPoint.x, bottomLeft.y)
CGPathAddLineToPoint(path[0], nil, anchorPoint.x, anchorPoint.y)
CGPathCloseSubpath(path[0])
// Second view for Frame Type 1
CGPathMoveToPoint(path[1], nil, anchorPoint.x, anchorPoint.y)
CGPathAddLineToPoint(path[1], nil, anchorPoint.x, bottomLeft.y)
CGPathAddLineToPoint(path[1], nil, bottomRight.x, bottomRight.y)
CGPathAddLineToPoint(path[1], nil, bottomRight.x, anchorPoint.y)
CGPathCloseSubpath(path[1])
// Third view for Frame Type 1
CGPathMoveToPoint(path[2], nil, 0, 0)
CGPathAddLineToPoint(path[2], nil, anchorPoint.x, anchorPoint.y)
CGPathAddLineToPoint(path[2], nil, bottomRight.x, anchorPoint.y)
CGPathAddLineToPoint(path[2], nil, bottomRight.x, 0)
CGPathCloseSubpath(path[2])
default:
break
}
for (key, view) in enumerate(viewsToMask) {
shapeLayer[key].path = path[key]
view.layer.mask = shapeLayer[key]
}
}
So, how can I make the scroll views behave in such a way that they will only scroll or zoom content when touches happen inside their corresponding mask boundaries?
EDIT:
According to the answer to this question: UIView's masked-off area still touchable? the masks only modify what you can see, not the area that you can touch. So I subclassed the UIScrollView and tried to override the hitTest:withEvent: method like so,
protocol CoolScrollViewDelegate: class {
var scrollViewPaths: [CGMutablePathRef] { get set }
}
class CoolScrollView: UIScrollView
{
weak var coolDelegate: CoolScrollViewDelegate?
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView?
{
if CGPathContainsPoint(coolDelegate?.scrollViewPaths[tag], nil, point, true) {
return self
} else {
return nil
}
}
}
But with this implementation, I can only check against the last scroll view and path boundaries change when I zoom in. For example if I zoom in on the image the hitTest:withEvent: method returns nil.
I would agree with #Kendel in the comments - to start with it might be an easier approach to create a UIScrollView subclass that knows how to mask itself with a particular shape. Keeping the shape logic within a scroll view subclass will keep things tidy, and allow you to easily restrict touches to within the shape (I'll come to that in a minute).
It's a little hard to tell from your description exactly how your shaped views should behave, but as a brief example your ShapedScrollView might look like something like this:
import UIKit
class ShapedScrollView: UIScrollView {
// MARK: Types
enum Shape {
case First // Choose a better name!
}
// MARK: Properties
private let shapeLayer = CAShapeLayer()
var shape: Shape = .First {
didSet { setNeedsLayout() }
}
// MARK: Initializers
init(frame: CGRect, shape: Shape = .First) {
self.shape = shape
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// MARK: Layout
override func layoutSubviews() {
super.layoutSubviews()
updateShape()
}
// MARK: Updating the Shape
private func updateShape() {
// Disable core animation actions to prevent changes to the shape layer animating implicitly
CATransaction.begin()
CATransaction.setDisableActions(true)
if bounds.size != shapeLayer.bounds.size {
// Bounds size has changed, completely update the shape
shapeLayer.frame = CGRect(origin: contentOffset, size: bounds.size)
shapeLayer.path = pathForShape(shape).CGPath
layer.mask = shapeLayer
} else {
// Bounds size has NOT changed, just update origin of shape path to
// match content offset - makes it appear stationary as we scroll
var shapeFrame = shapeLayer.frame
shapeFrame.origin = contentOffset
shapeLayer.frame = shapeFrame
}
CATransaction.commit()
}
private func pathForShape(shape: Shape) -> UIBezierPath {
let path = UIBezierPath()
switch shape {
case .First:
// Build the shape path, whatever that might be...
// path.moveToPoint(...)
// ...
}
return path
}
}
So making the touches only work inside the specified shape is the easy part. We already have a reference to a shape layer that describes the shape we want to restrict touches to. UIView provides a helpful hit-testing method that lets you specify whether or not a particular point should be considered to be "inside" that view: pointInside(_:withEvent:). Simply add the following override to ShapedScrollView:
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
return CGPathContainsPoint(shapeLayer.path, nil, layer.convertPoint(point, toLayer: shapeLayer), false)
}
This just says: "If point (converted to the shape layer's coordinate system) is inside the shape's path, consider it to be inside the view; otherwise consider it outside the view."
If a scroll view that masks itself isn't appropriate, you can still adopt this technique by using a ShapedScrollContainerView: UIView with a scrollView property. Then, apply the shape mask to the container as above, and again use pointInside(_:withEvent:) to test whether it should respond to particular touch points.

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)