How to blend multiple UIView in iOS? - ios

I want to blend multiple UIView in such a way that it can move, rotate and resize smoothly.
I already achieved this with the code below but it's not as smooth as other application like PicsArt & SnapSeed.
- (void)drawRect:(CGRect)rect {
// Drawing code
[super drawRect:rect];
UIImage *viewImage = [self captureView:self.superview withFrame:self.frame];
[viewImage drawInRect:rect];
[self.imageToBlend drawInRect:rect blendMode:self.blendMode alpha:self.alphaBlend];
// Other code....
}

You can't blend views this way; they have separate layer hierarchies that are resolved independently. Move the gradient and text on CALayer objects and blend those (or draw them by hand inside the same view). For example:
class MyView: UIView {
let gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.red.cgColor,
UIColor.blue.cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
return gradientLayer
}()
let textLayer: CATextLayer = {
let textLayer = CATextLayer()
let astring = NSAttributedString(string: "Text Example",
attributes: [.font: UIFont(name: "MarkerFelt-Wide", size: 60)!])
textLayer.string = astring
textLayer.alignmentMode = kCAAlignmentCenter
textLayer.bounds = astring.boundingRect(with: CGSize(width: .max, height: .max),
options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
return textLayer
}()
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
gradientLayer.bounds = bounds
gradientLayer.render(in: context)
context.saveGState()
context.setBlendMode(.softLight)
context.translateBy(x: center.x - textLayer.bounds.width / 2,
y: center.y - textLayer.bounds.height / 2)
textLayer.position = center
textLayer.render(in: context)
context.restoreGState()
}
}
}
you won't get a proper answer by searching try your own logic.
By using context, you can use a gesture and all other.

Capture and blend each time when drawRect: sounds too heavy.
It seems that you want to blend some image to its backend with various blend modes and alpha.
In that case, you'd better to blend entire view, and use view.layer.mask to show only masked area of the blend result.
So you can just move, rotate and resize the view.layer.mask, and no capturing or blending is needed. It should be much faster.
Something like this.
class MyView: UIImageView {
var backendImage: UIImage?
var blendImage: UIImage?
var blendMode: CGBlendMode = .normal
var alphaBlend: Float = 0.5
func setup(backend: UIView) {
self.backendImage = self.captureView(backend)
self.image = self.createBlendImage()
self.layer.mask = self.createMask()
}
func captureView(view: UIView) -> UIImage {
// capture view
}
func createBlendImage() -> UIImage {
// prepare ImageContext, draw two image, get image, and so on...
}
func createMask() -> CALayer {
let mask = CAShapeLayer()
// setup the shape of mask
}
func moveMask(origin: CGPoint, scale: Float, rotation: Float) {
// change mask
self.layer.mask.frame = ...
self.layer.mask.transform = ...
}
}

Related

How can i add Gradient colour for button tint in swift iOS

is there any solution to apply tint colour as a gradient in swift
my button has a simple white icon I want to change it to a gradient colour
I updated my answer according to Duncan C comment.
You can modify your image before setting it to button.
extension UIImage {
func drawLinearGradient(colors: [CGColor], startingPoint: CGPoint, endPoint: CGPoint) -> UIImage? {
let renderer = UIGraphicsImageRenderer(size: self.size)
var shouldReturnNil = false
let gradientImage = renderer.image { context in
context.cgContext.translateBy(x: 0, y: self.size.height)
context.cgContext.scaleBy(x: 1.0, y: -1.0)
context.cgContext.setBlendMode(.normal)
let rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)
// Create gradient
let colors = colors as CFArray
let colorsSpace = CGColorSpaceCreateDeviceRGB()
guard let gradient = CGGradient(colorsSpace: colorsSpace, colors: colors, locations: nil) else {
shouldReturnNil = true
return
}
// Apply gradient
guard let cgImage = self.cgImage else {
shouldReturnNil = true
print("Couldn't get cgImage of UIImage.")
return
}
context.cgContext.clip(to: rect, mask: cgImage)
context.cgContext.drawLinearGradient(
gradient,
start: endPoint,
end: startingPoint,
options: .init(rawValue: 0)
)
}
return shouldReturnNil ? nil : gradientImage
}
}
You can use it like that:
guard let image = UIImage(named: "<your_image_name>") else { return }
v.image = image.drawLinearGradient(
colors: [UIColor.blue.cgColor, UIColor.red.cgColor],
startingPoint: .init(x: image.size.width / 2, y: 0),
endPoint: .init(x: image.size.width / 2, y: image.size.height)
)
button.setImage(gradientImage, for: .normal)
This code produces result like this:
I have the same problem too
but for now
if you wanna change the text color it works.
like you convert image into pattern color by this code
/*
class gradiunTitle : UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
let gradient = getGradientLayer(bounds: self.bounds)
self.setTitleColor(gradientColor(bounds: self.bounds, gradientLayer: gradient), for: .normal)
self.tintColor = gradientColor(bounds: self.frame, gradientLayer: gradient)
}
func getGradientLayer(bounds : CGRect) -> CAGradientLayer{
let gradient = CAGradientLayer()
gradient.frame = bounds
//order of gradient colors
gradient.colors = [UIColor.red.cgColor,UIColor.blue.cgColor, UIColor.green.cgColor]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
return gradient
}
func gradientColor(bounds: CGRect, gradientLayer :CAGradientLayer) -> UIColor? {
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIColor(patternImage: image!)
}
}
*/

UIView to UIImage using UIGraphicsImageRenderer wrongly renderer image with alpha

I'm converting a UIView to UIImage, to set it as navigationBar background.
Thats view has an gradient layer, and when i use setBackgroundImage(_ backgroundImage: UIImage?, for barMetrics: UIBarMetrics) using the converted view, navigation gets a little transparent, something like 0.8 alpha
Here's the code that i'm using:
let navBackground = UIView(frame: CGRect(x: UIApplication.shared.statusBarFrame.origin.x,
y: UIApplication.shared.statusBarFrame.origin.y,
width: UIApplication.shared.statusBarFrame.width,
height: UIApplication.shared.statusBarFrame.height+self.navigationController!.navigationBar.frame.size.height))
let gradient: CAGradientLayer = CAGradientLayer()
gradient.colors = [UIColor(red:0.16, green:0.22, blue:0.49, alpha:1.0).cgColor, UIColor(red:0.31, green:0.53, blue:0.78, alpha:1.0).cgColor]
gradient.locations = [0.0, 1.0]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 0)
gradient.frame = navBackground.bounds
navBackground.layer.insertSublayer(gradient, above: nil)
let imgBackground = navBackground.asImage()
self.navigationController?.navigationBar.setBackgroundImage(imgBackground, for: UIBarMetrics.default)
asImage() it's a UIView extension that convert UIView to UIImage:
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}!
Result: sample
Turn off the translucent property of navigation bar. Actually the default navigation background has a blur effect but your converted image has none.

equivalent way drawing method to UIGraphicsGetCurrentContext

I have a swift class that outputs pie chart, I want to take out drawing logic out of drawRect method, since UIGraphicsGetCurrentContext returns nil outside of the draw method I need to change way of drawing my pie chart view,
What is the appropriate way to do this? My PieChart class looks like;
override func draw(_ rect: CGRect) {
let context: CGContext = UIGraphicsGetCurrentContext()!
drawLinearGradient(context)
drawCenterCircle(context)
let circle: CAShapeLayer = drawStroke()
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = percentage
pathAnimation.toValue = percentage
pathAnimation.isRemovedOnCompletion = rendered ? false : (percentageLabel > 20 ? false : true)
pathAnimation.isAdditive = true
pathAnimation.fillMode = kCAFillModeForwards
circle.add(pathAnimation, forKey: "strokeEnd")
}
private func drawLinearGradient(_ context: CGContext) {
let circlePoint: CGRect = CGRect(x: 0, y: 0,
width: bounds.size.width, height: bounds.size.height)
context.addEllipse(in: circlePoint)
context.clip()
let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()
let colorArray = [colors.0.cgColor, colors.1.cgColor]
let colorLocations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorArray as CFArray, locations: colorLocations)
let startPoint = CGPoint.zero
let endPoint = CGPoint(x: 0, y: self.bounds.height)
context.drawLinearGradient(gradient!,
start: startPoint,
end: endPoint,
options: CGGradientDrawingOptions.drawsAfterEndLocation)
}
private func drawCenterCircle(_ context: CGContext) {
let circlePoint: CGRect = CGRect(x: arcWidth, y: arcWidth,
width: bounds.size.width-arcWidth*2,
height: bounds.size.height-arcWidth*2)
context.addEllipse(in: circlePoint)
UIColor.white.setFill()
context.fillPath()
}
private func drawStroke() -> CAShapeLayer {
let circle = CAShapeLayer()
self.layer.addSublayer(circle)
return circle
}
You can change your code to produce a UIImage containing the pie chart, and then place that image inside an image view:
UIGraphicsBeginImageContext(size)
let context = UIGraphicsGetCurrentContext()
// ... here goes your drawing code, just as before
let pieImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let pieView = UIImageView(image: pieImage)
Note that if you need to update the chart very frequently (multiple times per second), this solution might bring a performance penalty because the image creation takes a bit more time then the pure rendering.

iOS UIProgressView with gradient

is it possible to create a custom ui progress view with a gradient from left to right?
I've tried it with the following code:
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.frame
gradientLayer.anchorPoint = CGPoint(x: 0, y: 0)
gradientLayer.position = CGPoint(x: 0, y: 0)
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0);
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0);
gradientLayer.colors = [
UIColor.red,
UIColor.green
]
// Convert to UIImage
self.layer.insertSublayer(gradientLayer, at: 0)
self.progressTintColor = UIColor.clear
self.trackTintColor = UIColor.black
But unfortunately the gradient is not visible. Any other ideas?
Looking at UIProgressView documentation, there's this property:
progressImage
If you provide a custom image, the progressTintColor property is ignored.
With that in mind, the laziest way to do this would be to create your gradient image and set it as the progressImage
I adapted this extension to make it a little cleaner, scaleable, and safer.
fileprivate extension UIImage {
static func gradientImage(with bounds: CGRect,
colors: [CGColor],
locations: [NSNumber]?) -> UIImage? {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colors
// This makes it horizontal
gradientLayer.startPoint = CGPoint(x: 0.0,
y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0,
y: 0.5)
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
UIGraphicsEndImageContext()
return image`
}
}
Now that we've got a way to create a gradient image "on the fly", here's how to use it:
let gradientImage = UIImage.gradientImage(with: progressView.frame,
colors: [UIColor.red.cgColor, UIColor.green.cgColor],
locations: nil)
From there, you'd just set your progressView's progressImage, like so:
// I'm lazy...don't force unwrap this
progressView.progressImage = gradientImage!
progressView.setProgress(0.75, animated: true)
I had the same problem and solved it by creating a gradient custom view which I then convert to an image and assign it as the progress view track image.
I then flip the progress horizontally so that the progress bar becomes the background and the track image becomes the foreground.
This has the visual effect of revealing the gradient image underneath.
You just have to remember to invert your percentages which is really simple, see example buttons and code below:
SWIFT 3 Example:
class ViewController: UIViewController {
#IBOutlet weak var progressView: UIProgressView!
#IBAction func lessButton(_ sender: UIButton) {
let percentage = 20
let invertedValue = Float(100 - percentage) / 100
progressView.setProgress(invertedValue, animated: true)
}
#IBAction func moreButton(_ sender: UIButton) {
let percentage = 80
let invertedValue = Float(100 - percentage) / 100
progressView.setProgress(invertedValue, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
//create gradient view the size of the progress view
let gradientView = GradientView(frame: progressView.bounds)
//convert gradient view to image , flip horizontally and assign as the track image
progressView.trackImage = UIImage(view: gradientView).withHorizontallyFlippedOrientation()
//invert the progress view
progressView.transform = CGAffineTransform(scaleX: -1.0, y: -1.0)
progressView.progressTintColor = UIColor.black
progressView.progress = 1
}
}
extension UIImage{
convenience init(view: UIView) {
UIGraphicsBeginImageContext(view.frame.size)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: (image?.cgImage)!)
}
}
#IBDesignable
class GradientView: UIView {
private var gradientLayer = CAGradientLayer()
private var vertical: Bool = false
override func draw(_ rect: CGRect) {
super.draw(rect)
// Drawing code
//fill view with gradient layer
gradientLayer.frame = self.bounds
//style and insert layer if not already inserted
if gradientLayer.superlayer == nil {
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = vertical ? CGPoint(x: 0, y: 1) : CGPoint(x: 1, y: 0)
gradientLayer.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
gradientLayer.locations = [0.0, 1.0]
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
}
George figured out a very clever method. If you want a more easy solution, open UIProgressView document, there is a property named progressImage.
so, i just make it work like this:
progressView.progressImage = UIImage(named: "your_gradient_progress_icon")
progressView.trackTintColor = UIColor.clear
after that:
progressView.setProgress(currentProgress, animated: true)

ios: how to blur a image with CGPath?

I create a CGPath area as shown green circle. The CGPath area need to be clear, and the rest of image will be applied blurry or translucent effect, I can clip image inside CGPath with following code:
UIGraphicsBeginImageContext(view.frame.size);
CGContextAddPath(ctx, path);
CGContextClip(ctx);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *clipImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageWriteToSavedPhotosAlbum(clipImage, nil, nil, nil);
CGPathRelease(path);
but I don't know how to apply blurry or translucent effect with CGPath simultaneously. I think I can blur origin image and merge it with clip image, but I don't know how to implement it.
You required 2 concept to achieved end result :-
i) CGBlendMode
ii) CIFilter
CGBlendMode is Used to remove the desired portion from image. Compositing operations on paths residing one another in the Current Context .
Clear mode whose rawValue is 16 help to clear the desired portion.
CIFilter help to blur the final image.
class ConvertToBlurryImage:UIView {
var originalImage:UIImage!
var finalImage:UIImage!
override func draw(_ rect: CGRect) {
super.draw(rect)
//Original Image
originalImage = UIImage(named: "originalImage.png")
//Intermediate Image
let intermediateImage = UIImage().returnBlurImage(image: originalImage)
//Final Result Image
finalImage = blendImage(image: intermediateImage)
let attachedImage = UIImageView(image: finalImage)
addSubview(attachedImage)
}
func blurryImage(image:UIImage) -> UIImage {
UIGraphicsBeginImageContext(frame.size)
image.draw(in: CGRect(origin: frame.origin, size: frame.size) )
// 16 === clear
let mode = CGBlendMode(rawValue: 16)
UIGraphicsGetCurrentContext()!.setBlendMode(mode!)
//Path that need to crop
pathToCrop()
let mode2 = CGBlendMode(rawValue: 16)
UIGraphicsGetCurrentContext()!.setBlendMode(mode2!)
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
return finalImage!
}
func pathToCrop() {
let path = UIBezierPath(ovalIn: CGRect(x: frame.width/2 - 50, y: frame.height/2 - 100 , width: 150, height: 150) )
path.fill()
path.stroke()
}
}
extension UIImage {
func returnBlurImage(image:UIImage) -> UIImage {
let beginImage = CIImage(cgImage: image.cgImage!)
let blurfilter = CIFilter(name: "CIGaussianBlur")
blurfilter?.setValue(beginImage, forKey: "inputImage")
let resultImage = blurfilter?.value(forKey: "outputImage") as! CIImage
let blurredImage = UIImage(ciImage: resultImage)
return blurredImage
}
}
Mission Achived
Final Github Demo

Resources