Subtract UIView from another UIView in Swift - ios

I'm sure this is a very simple thing to do, but I can't seem to wrap my head around the logic.
I have two UIViews. One black, semi-transparent and "full-screen" ("overlayView"), another one on top, smaller and resizeable ("cropView"). It's pretty much a crop-view setup, where I want to "dim" out the areas of an underlying image that are not being cropped.
My question is: How do I go about this? I'm sure my approach should be with CALayers and masks, but no matter what I try, I can't get behind the logic.
This is what I have right now:
This is what I would want it to look like:
How do I achieve this result in Swift?

Although you won't find a method such as subtract(...), you can easily build a screen with an overlay and a transparent cut with the following code:
Swift 4.2
private func addOverlayView() {
let overlayView = UIView(frame: self.bounds)
let targetMaskLayer = CAShapeLayer()
let squareSide = frame.width / 1.6
let squareSize = CGSize(width: squareSide, height: squareSide)
let squareOrigin = CGPoint(x: CGFloat(center.x) - (squareSide / 2),
y: CGFloat(center.y) - (squareSide / 2))
let square = UIBezierPath(roundedRect: CGRect(origin: squareOrigin, size: squareSize), cornerRadius: 16)
let path = UIBezierPath(rect: self.bounds)
path.append(square)
targetMaskLayer.path = path.cgPath
// Exclude intersected paths
targetMaskLayer.fillRule = CAShapeLayerFillRule.evenOdd
overlayView.layer.mask = targetMaskLayer
overlayView.clipsToBounds = true
overlayView.alpha = 0.6
overlayView.backgroundColor = UIColor.black
addSubview(overlayView)
}
Just call this method inside your custom view's constructor or inside your ViewController's viewDidLoad().
Walkthrough
First I create a raw overlayView, then a CAShapeLayer which I called "targetMaskLayer". The ultimate goal is to draw a square with the help of UIBezierPath inside that overlayView. After defining the square's dimensions, I set its cgPath as the targetMaskLayer's path.
Now comes an important part:
targetMaskLayer.fillRule = CAShapeLayerFillRule.evenOdd
Here I basically configure the fill rule to exclude the intersection.
Finally, I provide some styling to the overlayView and add it as a subview.
ps.: don't forget to import UIKit

There might be another drawing solution but basically you have 4 areas that need to be handled. Take the square area above and below the space with full width and add the right and left side between them with constraints to eachother.

Related

draw rounded lines iOS Swift

Im trying to make an app which draws a fractal tree. I managed to make the code that generates all the start and end points from all the lines. I also managed to draw the lines but right now they are really boxy and want them to have rounded corners.
I using a UIView and using UIBezierPaths to draw the lines inside the view draw function. To retrieve the points I have an array of Branch objects inside a sigleton class. A branch object has among other things a startingPoint and a ending point which are both tuples( (x: Double, y: Double) ).
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.blue.setStroke()
for branch in Tree.shared.branches{
let path = UIBezierPath()
print(branch.startingPoint)
print(branch.endingPoint)
path.move(to: CGPoint(x: branch.startingPoint.x, y: branch.startingPoint.y))
path.addLine(to: CGPoint(x: branch.endingPoint.x, y: branch.endingPoint.y))
path.lineWidth = 3
path.stroke()
}
}
How could i make the corners rounded?
Also if someone knows a free library that could facilitate this Im also interested.
edit: Im not interested in how to generate the tree, I have done already done this part of the code I need help with drawing the lines.
You don't need a library, you just need to spend a little time learning how to draw curves with UIBezierPath, and curves are one of the things that that class is best at. A key to drawing curves is understanding how control points work. Here's an answer I wrote some time ago about how to smoothly connect curved lines, which I think will help. Play around with -addCurveToPoint:controlPoint1:controlPoint2:.
If you don't actually want curves, but really just want the corners to be rounded rather than pointy, then all you need to do is to set the lineJoinStyle to kCGLineJoinRound.
About rounded corners of some view, is just set the parameters below:
func adjustView(_ view: UIView){
view.layer.borderWidth = 2.0
view.layer.borderColor = UIColor.red
view.layer.cornerRadius = 10
view.clipsToBounds = true
view.backgroundColor = UIColor.white
}
If you wish more information check the current documentation about layer property:
https://developer.apple.com/documentation/uikit/uiview/1622436-layer

circle collectionview image according to device size

Hello in my horizontal collectionview i want to circle image , if i set static height and width it works but it does not work if i set constraint
iam using this method to circle my image
public static func circularImageWhite(photoImageView: UIImageView?)
{
photoImageView!.layer.frame = photoImageView!.layer.frame.insetBy(dx: 0, dy: 0)
photoImageView!.layer.borderColor = UIColor.white.cgColor
photoImageView!.layer.cornerRadius = photoImageView!.frame.height/2
photoImageView!.layer.masksToBounds = false
photoImageView!.clipsToBounds = true
photoImageView!.layer.borderWidth = 1
photoImageView!.contentMode = UIViewContentMode.scaleAspectFill
}
i want to circle image on every device
Everything about your code is wrong.
photoImageView!.layer.frame = photoImageView!.layer.frame.insetBy(dx: 0, dy: 0)
That line is meaningless. If you inset the frame by zero you are not changing it. So that line does nothing at all.
photoImageView!.layer.masksToBounds = false
photoImageView!.clipsToBounds = true
A layer's masksToBounds and a view's clipsToBounds are actually the very same property. So you are setting the same property to false and then back to true in the very next line. Thus the first of those two lines does nothing at all.
photoImageView!.layer.cornerRadius = photoImageView!.frame.height/2
That is actually the heart of the matter. The problem is that you are setting the corner radius according to the frame height. But that assumes you know what the frame is. You don't. As you yourself said, this doesn't work if you set autolayout constraints on your view. Why? Because of the order in which things happen:
First, you use the current frame height to set the corner radius.
Then, the constraints kick in and change the frame. So now the corner radius that you set before doesn't "fit" the image view any more.
Moreover, setting the corner radius is a lousy way to clip a view to a circle. The correct way is to mask the view to an actual circle.
So, to sum up: You should use a UIImageView subclass that overrides its own layoutSubviews to set its own mask to a circle that fits the current size. As the size changes due to constraints, layoutSubviews will be called and your code will change the mask to fit properly.
(The white circular border can be yet another layer or subview that draws the circle.)
The matter comes up quite often, and I often see the cornerRadius misused in this same way, so here's an actual implementation:
class CircleImageView : UIImageView {
override func layoutSubviews() {
super.layoutSubviews()
self.layer.sublayers = nil
let radius = min(self.bounds.height, self.bounds.width)/2
let cen = CGPoint(x:self.bounds.width/2, y:self.bounds.height/2)
let r = UIGraphicsImageRenderer(size:self.bounds.size)
var im : UIImage?
var outline : UIImage?
r.image { ctx in
let con = ctx.cgContext
UIColor.black.setFill()
con.addArc(center: cen, radius: radius,
startAngle: 0, endAngle: .pi*2, clockwise: true)
let p = con.path
con.fillPath()
im = ctx.currentImage
con.clear(CGRect(origin:.zero, size:self.bounds.size))
con.addPath(p!)
UIColor.clear.setFill()
UIColor.white.setStroke() // border color, change as desired
con.setLineWidth(4) // border width, change as desired
con.strokePath()
outline = ctx.currentImage
}
// the circle mask
let iv = UIImageView(image:im)
iv.contentMode = .center
iv.frame = self.bounds
self.mask = iv
// the border
let iv2 = UIImageView(image:outline)
iv2.contentMode = .center
iv2.frame = self.bounds
self.addSubview(iv2)
}
}
Result:
Use CircleImageView as your image view and you'll get the right result. I repeat: the important thing is that this will continue to work no matter how the CircleImageView itself is subsequently resized.

UIImageview mask updating too slowly in response to gesture

I need to create a component that will let users choose from 2 choices in images. At first, you see 2 images side by side with a "handle" in the middle. If you move the handle to the left, you will see more of the image to right and less of the left image, as to reveal the right image, and vice versa.
Technically, I have 2 full size UIImageViews one on top of the other, and they are masked. I have a pan gesture and when the user slides the handle, the handle moves and the masks update themselves to adjust to "the new middle".
Here's the code responsible for adjusting the image mask. The constant is calculated in the method called by the gesture. I know my calculations of that constant are good because the "handle" and the masks are updated correctly.
BUT
the masks gets updated too late and when dragging, we see it being adjusted too late.
func adjustImagesMasks(to constant: CGFloat) {
choiceImageA.mask?.willChangeValue(forKey: "frame")
choiceImageB.mask?.willChangeValue(forKey: "frame")
let separationPoint: CGFloat = self.frame.width / 2.0 + constant
maskA.backgroundColor = UIColor.black.cgColor
maskA.frame = CGRect(origin: .zero, size: CGSize(width: separationPoint, height: self.frame.size.height))
maskB.backgroundColor = UIColor.black.cgColor
maskB.frame = CGRect(x: separationPoint, y: 0, width: self.frame.width - separationPoint, height: self.frame.size.height)
choiceImageA.mask?.didChangeValue(forKey: "frame")
choiceImageB.mask?.didChangeValue(forKey: "frame")
maskA.drawsAsynchronously = true
maskB.drawsAsynchronously = true
self.setNeedsDisplay()
maskA.setNeedsDisplay()
maskA.displayIfNeeded()
maskB.setNeedsDisplay()
maskB.displayIfNeeded()
}
The image views have their masks setup like this:
maskA = CALayer()
maskB = CALayer()
choiceImageA.layer.mask = maskA
choiceImageA.layer.masksToBounds = true
choiceImageB.layer.mask = maskB
choiceImageB.layer.masksToBounds = true
So to recap, my question is really about performance. The image views are being correctly adjusted, but too slowly. The "handle", which is positioned with constraints, get updated really quickly.
So apparently, CALayer tries to animate most of the changes to its properties. So the delay I was seeing was in fact due to an animation.
I resolved my issue by surrounding the call to adjustImagesMasks() with CATransaction.setValue(kCFBooleanTrue, forKey:kCATransactionDisableActions) and CATransaction.commit(). So for this transaction, I'm asking to not animate the changes. Because this is continuous (with the panning gesture), it is seemless.
Full code here:
CATransaction.setValue(kCFBooleanTrue, forKey:kCATransactionDisableActions)
adjustImagesMasks(to: newConstant)
CATransaction.commit()```.
This other post helped me a lot. There's a nice explanation too.
Hope this helps someone else.

Shape/mask on UIImage?

I was wondering how can this be achieved in Xcode? It's part of a profile, so the header is an actual UIImageView, but the curve below it, not sure how to achieve that. Any ideas?
Say that the grey area is built out of a bottom grey rectangle and on top another rectangle with your arc we could do something like this:
Create a UIBezierPath in the shape of a circle:
let path = UIBezierPath(ovalIn:CGRect(x: 0, y: view.bounds.height/2, width: view.bounds.width, height: view.bounds.height)).cgPath
Apply it to the top rectangle.
let overlay = CAShapeLayer()
overlay.path = path
overlay.fillColor = UIColor.gray.cgColor
overlay.shouldRasterize = true
view.layer.addSublayer(overlay)
This will create a perfect circle but you can tweak the CGRect's your liking in order to get the shape you want!
It can be achieved by giving header view UIBezierPath but if you don't want to do that stuff.
I found a cool way of doing.
Your Header view contain a image (Lets say it HeaderImage).
Make a Image of that shape (Lets say it MaskImage).
let path = UIImageView.init(image: #imageLiteral(resourceName: "MaskImage"))
Than apply this mask to Header Image.
HeaderImage.mask = path
Hope it work for you.

How to invert colors of a specific area of a UIView?

It's easy to blur a portion of the view, keeping in mind that if the contents of views behind change, the blur changes too in realtime.
My questions
How to make an invert effect, and you can put it over a view and the contents behind would have inverted colors
How to add an effect that would know the average color of the pixels behind?
In general, How to access the pixels and manipulate them?
My question is not about UIImageView, asking about UIView in general..
there are libraries that does something similar, but they are so slow and don't run as smooth as blur!
Thanks.
If you know how to code a CIColorKernel, you'll have what you need.
Core Image has several blur filters, all of which use the GPU, which will give you the performance you need.
The CIAreaAverage will give you the average color for a specified rectangular area.
Core Image Filters
Here is about the simplest CIColorKernel you can write. It swaps the red and green value for every pixel in an image (note the "grba" instead of "rgba"):
kernel vec4 swapRedAndGreenAmount(__sample s) {
return s.grba;
}
To put this into a CIColorKernel, just use this line of code:
let swapKernel = CIKernel(string:
"kernel vec4 swapRedAndGreenAmount(__sample s) {" +
"return s.grba;" +
"}"
#tww003 has good code to convert a view's layer into a UIImage. Assuming you call your image myUiImage, to execute this swapKernel, you can:
let myInputCi = CIImage(image: myUiImage)
let myOutputCi = swapKernel.apply(withExtent: myInputCi, arguments: myInputCi)
Let myNewImage = UIImage(ciImage: myOutputCi)
That's about it. You can do alot more (including using CoreGraphics, etc.) but this is a good start.
One last note, you can chain individual filters (including hand-written color, warp, and general kernels). If you want, you can chain your color average over the underlying view with a blur and do whatever kind of inversion you wish as a single filter/effect.
I don't think I can fully answer your question, but maybe I can point you in the right direction.
Apple has some documentation on accessing the pixels data from CGImages, but of course that requires that you have an image to work with in the first place. Fortunately, you can create an image from a UIView like this:
UIGraphicsBeginImageContext(view.frame.size)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
From this image you created, you'll be able to manipulate the pixel data how you want to. It may not be the cleanest way to solve your problem, but maybe it's something worth exploring.
Unfortunately, the link I provided is written in Objective-C and is a few years old, but maybe you can figure out how to make good use of it.
1st I will recommend you to extend UIImageView for this purpose. ref
Good ref by Joe
You have to override drawRect method
import UIKit
#IBDesignable class PortholeView: UIView {
#IBInspectable var innerCornerRadius: CGFloat = 10.0
#IBInspectable var inset: CGFloat = 20.0
#IBInspectable var fillColor: UIColor = UIColor.grayColor()
#IBInspectable var strokeWidth: CGFloat = 5.0
#IBInspectable var strokeColor: UIColor = UIColor.blackColor()
override func drawRect(rect: CGRect) {
super.drawRect(rect:rect)
// Prep constants
let roundRectWidth = rect.width - (2 * inset)
let roundRectHeight = rect.height - (2 * inset)
// Use EvenOdd rule to subtract portalRect from outerFill
// (See https://stackoverflow.com/questions/14141081/uiview-drawrect-draw-the-inverted-pixels-make-a-hole-a-window-negative-space)
let outterFill = UIBezierPath(rect: rect)
let portalRect = CGRectMake(
rect.origin.x + inset,
rect.origin.y + inset,
roundRectWidth,
roundRectHeight)
fillColor.setFill()
let portal = UIBezierPath(roundedRect: portalRect, cornerRadius: innerCornerRadius)
outterFill.appendPath(portal)
outterFill.usesEvenOddFillRule = true
outterFill.fill()
strokeColor.setStroke()
portal.lineWidth = strokeWidth
portal.stroke()
}
}
Your answer is here

Resources