How to set the BlurRadius of UIBlurEffectStyle.Light - swift

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)

Related

How do I update a CALayer with a CVPixelBuffer/IOSurface?

I have an IOSurface-backed CVPixelBuffer that is getting updated from an outside source at 30fps. I want to render a preview of the image data in an NSView -- what's the best way for me to do that?
I can directly set the .contents of a CALayer on the view, but that only updates the first time my view updates (or if, say, I resize the view). I've been poring over the docs but I can't figure out the correct invocation of needsDisplay on the layer or view to let the view infrastructure know to refresh itself, especially when updates are coming from outside the view.
Ideally I'd just bind the IOSurface to my layer and any changes I make to it would be propagated, but I'm not sure if that's possible.
class VideoPreviewController: NSViewController, VideoFeedConsumer {
let customLayer : CALayer = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
print("Loaded our video preview")
view.layer?.addSublayer(customLayer)
customLayer.frame = view.frame
// register our view with the browser service
VideoFeedBrowser.instance.registerConsumer(self)
}
override func viewWillDisappear() {
// deregister our view from the video feed
VideoFeedBrowser.instance.deregisterConsumer(self)
super.viewWillDisappear()
}
// This callback gets called at 30fps whenever the pixelbuffer is updated
#objc func updateFrame(pixelBuffer: CVPixelBuffer) {
guard let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else {
print("pixelbuffer isn't IOsurface backed! noooooo!")
return;
}
// Try and tell the view to redraw itself with new contents?
// These methods don't work
//self.view.setNeedsDisplay(self.view.visibleRect)
//self.customLayer.setNeedsDisplay()
self.customLayer.contents = surface
}
}
Here's my attempt of a scaling version that's NSView rather than NSViewController-based, that also doesn't update correctly (or scale correctly for that matter):
class VideoPreviewThumbnail: NSView, VideoFeedConsumer {
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
self.wantsLayer = true
// register our view with the browser service
VideoFeedBrowser.instance.registerConsumer(self)
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.wantsLayer = true
// register our view with the browser service
VideoFeedBrowser.instance.registerConsumer(self)
}
deinit{
VideoFeedBrowser.instance.deregisterConsumer(self)
}
override func updateLayer() {
// Do I need to put something here?
print("update layer")
}
#objc
func updateFrame(pixelBuffer: CVPixelBuffer) {
guard let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else {
print("pixelbuffer isn't IOsurface backed! noooooo!")
return;
}
self.layer?.contents = surface
self.layer?.transform = CATransform3DMakeScale(
self.frame.width / CGFloat(CVPixelBufferGetWidth(pixelBuffer)),
self.frame.height / CGFloat(CVPixelBufferGetHeight(pixelBuffer)),
CGFloat(1))
}
}
What am I missing?
Maybe I'm wrong, but I think you are you updating your NSView on a background thread. (I suppose that the callback to updateFrame is on a background thread)
If I'm right, when you want to update the NSView, convert your pixelBuffer to whatever you want (NSImage?), and then dispatch it on the main thread.
Pseudocode (I don't work often with CVPixelBuffer so I'm not sure this is the right way to convert to an NSImage)
let ciImage = CIImage(cvImageBuffer: pixelBuffer)
let context = CIContext(options: nil)
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let cgImage = context.createCGImage(ciImage, from: CGRect(x: 0, y: 0, width: width, height: height))
let nsImage = NSImage(cgImage: cgImage, size: CGSize(width: width, height: height))
DispatchQueue.main.async {
// assign the NSImage to your NSView here
}
Another catch: I did some tests, and it seems that you cannot assign an IOSurface directly to the contents of a CALayer.
I tried with this:
let textureImageWidth = 1024
let textureImageHeight = 1024
let macPixelFormatString = "ARGB"
var macPixelFormat: UInt32 = 0
for c in macPixelFormatString.utf8.reversed() {
macPixelFormat *= 256
macPixelFormat += UInt32(c)
}
let ioSurface = IOSurfaceCreate([kIOSurfaceWidth: textureImageWidth,
kIOSurfaceHeight: textureImageHeight,
kIOSurfaceBytesPerElement: 4,
kIOSurfaceBytesPerRow: textureImageWidth * 4,
kIOSurfaceAllocSize: textureImageWidth * textureImageHeight * 4,
kIOSurfacePixelFormat: macPixelFormat] as CFDictionary)!
IOSurfaceLock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
let test = CIImage(ioSurface: ioSurface)
IOSurfaceUnlock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
v1?.layer?.contents = ioSurface
Where v1 is my view. No effect
Even with a CIImage no effect (just last few lines)
IOSurfaceLock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
let test = CIImage(ioSurface: ioSurface)
IOSurfaceUnlock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
v1?.layer?.contents = test
If I create a CGImage it works
IOSurfaceLock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
let test = CIImage(ioSurface: ioSurface)
IOSurfaceUnlock(ioSurface, IOSurfaceLockOptions.readOnly, nil)
let context = CIContext.init()
let img = context.createCGImage(test, from: test.extent)
v1?.layer?.contents = img
I encountered this problem myself and the solution is to double buffer the IOSurface source: use two IOSurface objects instead of one and render to the current surface, set the surface to the layer contents and then on the next rendering pass use the alternate (back/front) surface and then swap.
It would appear that setting the CALayer.contents twice to the same CVPixelBufferRef has no effect. However, if you alternate between two IOSurfaceRef it works wonderfully.
It maybe also possible to invalidate the layer contents by setting it to nil and then reset. I did not try that case but am using the double buffer technique.
If you have some IBActions that update it then create an observed variable with the didSet block and whenever the IBAction is triggered, change its value. Also remember to write the code you want to run when updated in that block.
I'd suggest making the variable an Int, set its default value to 0 and add 1 to it every time it updates.
And you can cast the NSView into an NSImageView for the part where you ask about showing the IMAGE data on an NSView so that does the job.
You need to convert the pixel buffer to CGImage and convert it to a layer so that you can change the layer of the main view.
Please try this code
#objc
func updateFrame(pixelBuffer: CVPixelBuffer) {
guard let surface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else {
print("pixelbuffer isn't IOsurface backed! noooooo!")
return;
}
void *baseAddr = CVPixelBufferGetBaseAddress(pixelBuffer);
size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef cgContext = CGBitmapContextCreate(baseAddr, width, height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), colorSpace, kCGImageAlphaNoneSkipLast);
CGImageRef cgImage = CGBitmapContextCreateImage(cgContext);
CGContextRelease(cgContext);
let outputImage = UIImage(cgImage: outputCGImage, scale: 1, orientation: img.imageOrientation)
let newLayer:CGLayer = CGLayer.init(cgImage: outputImage)
self.layer = newLayer
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
CVPixelBufferRelease(pixelBuffer);
}

Scaled up MTKView shows gaps when joining CIImages

I'm using a MTKView written by Simon Gladman that "exposes an image property type of 'CIImage' to simplify Metal based rendering of Core Image filters." It has been slightly altered for performance. I left out an additional scaling operation since it has nothing to do with the issue here.
Problem: When creating a composite of smaller CIImages into a larger one, they are aligned pixel perfect. MTKView's image property is set to this CIImage composite. However, there is a scale done to this image so it fits the entire MTKView which makes gaps between the joined images visible. This is done by dividing the drawableSize width/height by the CIImage's extent width/height.
This makes me wonder if something needs to be done the CIImage side to actually join those pixels. Saving that CIImage to the camera roll shows no separation between the joined images. It's only visible when the MTKView scales up. In addition, whatever needs to be done needs to have virtually no impact on performance since these image renders are being done in real time through the camera's output. (The MTKView is a preview of the effect being done)
Here is the MTKView that I'm using to render with:
class MetalImageView: MTKView
{
let colorSpace = CGColorSpaceCreateDeviceRGB()
var textureCache: CVMetalTextureCache?
var sourceTexture: MTLTexture!
lazy var commandQueue: MTLCommandQueue =
{
[unowned self] in
return self.device!.makeCommandQueue()
}()!
lazy var ciContext: CIContext =
{
[unowned self] in
//cacheIntermediates
return CIContext(mtlDevice: self.device!, options:[.cacheIntermediates:false])
//return CIContext(mtlDevice: self.device!)
}()
override init(frame frameRect: CGRect, device: MTLDevice?)
{
super.init(frame: frameRect,
device: device ?? MTLCreateSystemDefaultDevice())
if super.device == nil
{
fatalError("Device doesn't support Metal")
}
CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, self.device!, nil, &textureCache)
framebufferOnly = false
enableSetNeedsDisplay = true
isPaused = true
preferredFramesPerSecond = 30
}
required init(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
/// The image to display
var image: CIImage?
{
didSet
{
//renderImage()
//draw()
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect)
{
guard let
image = image,
let targetTexture = currentDrawable?.texture else
{
return
}
let commandBuffer = commandQueue.makeCommandBuffer()
let bounds = CGRect(origin: CGPoint.zero, size: drawableSize)
let originX = image.extent.origin.x
let originY = image.extent.origin.y
let scaleX = drawableSize.width / image.extent.width
let scaleY = drawableSize.height / image.extent.height
let scale = min(scaleX, scaleY)
let scaledImage = image
.transformed(by: CGAffineTransform(translationX: -originX, y: -originY))
.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
ciContext.render(scaledImage,
to: targetTexture,
commandBuffer: commandBuffer,
bounds: bounds,
colorSpace: colorSpace)
commandBuffer?.present(currentDrawable!)
commandBuffer?.commit()
}
}
When compositing the images, I have a full size camera image as the background just as a foundation for what the size should be, then I duplicate half of that halfway across the width or height of the image using the CISourceAtopCompositing CIFilter and translate it using a CGAffineTransform. I also give it a negative scale to add a mirror effect:
var scaledImageTransform = CGAffineTransform.identity
scaledImageTransform = scaledImageTransform.translatedBy(x:0, y:sourceCore.extent.height)
scaledImageTransform = scaledImageTransform.scaledBy(x:1.0, y:-1.0)
alphaMaskBlend2 = alphaMaskBlend2?.applyingFilter("CISourceAtopCompositing",
parameters: [kCIInputImageKey: alphaMaskBlend2!,
kCIInputBackgroundImageKey: sourceCore])
alphaMaskBlend2 = alphaMaskBlend2?.applyingFilter("CISourceAtopCompositing",
parameters: [kCIInputImageKey: (alphaMaskBlend2?.cropped(to: cropRect).transformed(by: scaledImageTransform))!,
kCIInputBackgroundImageKey: alphaMaskBlend2!])
sourceCore is the original image that came through the camera. alphaMaskBlend2 is the final CIImage that I assign the MTKView to. The cropRect correctly crops the mirrored part of the image. In the scaled up MTKView there is a visible gap between these two joined CIImages. What can be done to make this image display as continuous pixels no matter how scaled the MTKView is just like any other image does?

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

Get and change hue of SKSpriteNode's SKColor(HSBA)?

A SKSpriteNode's SKColor has a way to be created with Hue, Saturation, Brightness & Alpha:
let myColor = SKColor(hue: 0.5, saturation: 1, brightness: 1, alpha: 1)
mySprite.color = myColor
How do I get at the hue of a SKSpriteNode and make a change to it? eg, divide it by 2.
An SKSpriteNode is a node that draws a texture (optionally blended with a color), an image, a colored square. So, this is it's nature.
When you make an SKSpriteNode, you have an instance property that represent the texture used to draw the sprite called also texture
Since iOS 9.x, we are able to retrieve an image from a texture following the code below. In this example I call my SKSpriteNode as spriteBg:
let spriteBg = SKSpriteNode.init(texture: SKTexture.init(imageNamed: "myImage.png"))
if let txt = spriteBg.texture {
if #available(iOS 9.0, *) {
let image : UIImage = UIImage.init(cgImage:txt.cgImage())
} else {
// Fallback on earlier versions and forgot this code..
}
}
Following this interesting answer, we can translate it to a more confortable Swift 3.0 version:
func imageWith(source: UIImage, rotatedByHue: CGFloat) -> UIImage {
// Create a Core Image version of the image.
let sourceCore = CIImage(cgImage: source.cgImage!)
// Apply a CIHueAdjust filter
guard let hueAdjust = CIFilter(name: "CIHueAdjust") else { return source }
hueAdjust.setDefaults()
hueAdjust.setValue(sourceCore, forKey: "inputImage")
hueAdjust.setValue(CGFloat(rotatedByHue), forKey: "inputAngle")
let resultCore = hueAdjust.value(forKey: "outputImage") as! CIImage!
let context = CIContext(options: nil)
let resultRef = context.createCGImage(resultCore!, from: resultCore!.extent)
let result = UIImage(cgImage: resultRef!)
return result
}
So, finally with the previous code we can do:
if let txt = spriteBg.texture {
if #available(iOS 9.0, *) {
let image : UIImage = UIImage.init(cgImage:txt.cgImage())
let changedImage = imageWith(source: image, rotatedByHue: 0.5)
spriteBg.texture = SKTexture(image: changedImage)
} else {
// Fallback on earlier versions or bought a new iphone
}
}
I'm not in a place to be able to test this right now, but looking at the UIColor documentation (UIColor and SKColor are basically the same thing), you should be able to use the .getHue(...) function retrieve the color's components, make changes to it, then set the SKSpriteNode's color property to the new value. the .getHue(...) function "Returns the components that make up the color in the HSB color space."
https://developer.apple.com/reference/uikit/uicolor/1621949-gethue

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.