Masking A UIView With UILabel Using AutoLayout - swift

I am working on a project that would result in this desired effect:
I am successfully drawing my circle using the following code:
let circle = CircleView()
circle.translatesAutoresizingMaskIntoConstraints = false
circle.backgroundColor = UIColor.clear
self.addSubview(circle)
// Setup constraints
circle.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
circle.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
circle.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.6).isActive = true
circle.heightAnchor.constraint(equalTo: circle.widthAnchor, multiplier: 1.0/1.0).isActive = true
For reference, this is my CircleView class:
class CircleView: UIView {
override func draw(_ rect: CGRect) {
// Set the path
let path = UIBezierPath(ovalIn: rect)
// Set the fill color
UIColor.black.setFill()
// Fill
path.fill()
}
}
Subsequently, I am then creating my UILabel, as such:
let myLabel = UILabel()
myLabel.translatesAutoresizingMaskIntoConstraints = false
myLabel.text = "HELLO"
myLabel.textColor = UIColor.white
circle.addSubview(myLabel)
// Setup constraints
myLabel.centerXAnchor.constraint(equalTo: circle.centerXAnchor).isActive = true
myLabel.centerYAnchor.constraint(equalTo: circle.centerYAnchor).isActive = true
I have attempted to mask my circle by using:
circle.mask = myLabel
This, however, results in the opposite effect I am after (my text is not a cut-out in the UIView, but rather, my text is now black). Where might I be going wrong to accomplish this effect?

Masks work in an opposite fashion, when the color on your mask is not transparent then it will show the content in that pixels position and when the pixels are transparent then it will hide the content.
Since you can't achieve this using UILabel you need to use something else as a mask, such as a CALayer.
If you lookup "UILabel see through text" you should find results similar to this which basically also applies to you (with some changes).
Instantiate your CircleView, then instantiate a CATextLayer and use this as a mask for your UIView

Related

UIView shadow displaying incorrectly

I've run into this problem before, and usually been able to fix it by placing the code in viewWillLayoutSubviews(). This works in every other view controller I have and I just can't find a solution.
I have a UIView and I'm adding a shadow to it like this:
func setUpUI() {
for tile in roundedTiles {
tile.layer.cornerRadius = t.frame.height/2
tile.layer.shadowColor = UIColor.black.cgColor
tile.layer.shadowRadius = 10.0
tile.layer.shadowOpacity = 0.15
tile.layer.addSpread(spread: -10)
tile.layer.borderColor = GlobalConstants.Colors.lightGray.cgColor
tile.layer.borderWidth = 1
}
}
I then call it here:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
setUpUI()
}
However, this is what it looks like:
This is what it should look like, taken from another view controller where it's displaying correctly (and I have written the code there exactly the same):
After looking at other StackOverflow questions I've tried moving it to viewDidLayoutSubviews(), viewDidLoad(), and adding tile.layoutIfNeeded(). Nothing is working. Would really appreciate some help!
First, I would recommend subclassing UIView for your tiles and add the shadow and rounded corners in the init method, rather than adding it via the parent view controller.
The trick to getting rounded corners with a shadow is adding the shadow to the view itself, and then adding a subview for the rounded corners, which you can maskToBounds without affecting the parent.
So:
class roundedTile: UIView {
required init?(coder: NSCoder) {
super.init(coder: coder)
self.layer.cornerRadius = self.frame.height/2
// Add shadow to parent view
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 5.0, height: 0.0)
self.layer.shadowOpacity = 0.15
self.layer.shadowRadius = 10.0
// Create subview for rounded corners
let subView = UIView()
subView.frame.size = self.frame.size
subView.layer.cornerRadius = frame.height/2
subView.layer.borderColor = UIColor.lightGray.cgColor
subView.layer.borderWidth = 1
subView.layer.masksToBounds = true
self.addSubview(subView)
}
}
Season to taste the shadow offset/radius/opacity.

How to add corner radius to UIStackView and mask subviews

I use a simple subclass for UIStackView to add background color for it:
func backgroundColor(_ color: UIColor?) {
backgroundView.backgroundColor = color
}
func cornerRadius(_ radius: CGFloat) {
backgroundView.clipsToBounds = true
backgroundView.layer.cornerRadius = radius
}
The problem is that corner radius using custom view as a container, won't mask arrangedSubviews. I was trying to fix that by overriding addArrangedSubview method:
override func addArrangedSubview(_ view: UIView) {
super.addArrangedSubview(view)
view.mask = backgroundView
}
But it makes weird things and spamming to console:
- changing property mask in
transform-only layer, will have no effect
Instead of adding background to stack view, you can add stack view as a child to the background and mask background with corners.
let wrapper = UIView() // Creating background
wrapper.layer.cornerRadius = 10
wrapper.layer.masksToBounds = true
wrapper.backgroundColor = .yellow
let stack = UIStackView() // Creating stack
stack.frame = wrapper.bounds
stack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
wrapper.addSubview(stack)

How to give an imageView in swift3.0.1 shadow at the same time with rounded corners

I want to give an imageView a shadow at the same time with rounded corners,but I failed.
Here is my solution
Basic idea :
Use an Extra view (say AView) as super view of image view (to those views on which you are willing to have shado) and assign that view class to DGShadoView
Pin Image view to AView (that super view)from left, right, top and bottom with constant 5
Set back ground color of the AView to clear color from storybosrd's Property inspector this is important
Inside idea: Here we are using a Bezier path on the Aview nearly on border and setting all rounded corner properties and shadow properties to that path and we are placing our target image view lie with in that path bound
#IBDesignable
class DGShadoView:UIView {
override func draw(_ rect: CGRect) {
self.rect = rect
decorate(rect: self.rect)
}
func decorate(rect:CGRect) {
//self.backgroundColor = UIColor.clear
//IMPORTANT: dont forgot to set bg color of your view to clear color from story board's property inspector
let ref = UIGraphicsGetCurrentContext()
let contentRect = rect.insetBy(dx: 5, dy: 5);
/*create the rounded oath and fill it*/
let roundedPath = UIBezierPath(roundedRect: contentRect, cornerRadius: 5)
ref!.setFillColor("your color for background".cgColor)
ref!.setShadow(offset: CGSize(width:0,height:0), blur: 5, color: "your color for shado".cgColor)
roundedPath.fill()
/*draw a subtle white line at the top of view*/
roundedPath.addClip()
ref!.setStrokeColor(UIColor.red.cgColor)
ref!.setBlendMode(CGBlendMode.overlay)
ref!.move(to: CGPoint(x:contentRect.minX,y:contentRect.minY+0.5))
ref!.addLine(to: CGPoint(x:contentRect.maxX,y:contentRect.minY+0.5))
}
}
Update
Extension Approach
There is another Approach. Just Make a class with empty and paste Following UIImageView Extension code, Assign this subclass to that ImageView on which you shadow.
import UIKit
class DGShadowView: UIImageView {
#IBInspectable var intensity:Float = 0.2{
didSet{
setShadow()
}
}
override func layoutSubviews()
{
super.layoutSubviews()
setShadow()
}
func setShadow(){
let shadowPath = UIBezierPath(rect: bounds)
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0.0, height: 0.3)
layer.shadowOpacity = intensity
layer.shadowPath = shadowPath.cgPath
}
}
The solution is to create two separate views. One for the shadow and one for the image itself. On the imageView you clipToBounds the layer so that the corner radius is properly added.
Put the imageView on top of the shadowView and you've got your solution!

Borders not covering background

I've got a UILabel is using a border the same color as a background which it is half obscuring, to create a nice visual effect. However the problem is that there is still a tiny, yet noticeable, sliver of the label's background color on the OUTSIDE of the border.
The border is not covering the whole label!
Changing the border width doesn't change anything either, sadly.
Here's a picture of what's going on, enlarged so you can see it:
And my code follows:
iconLbl.frame = CGRectMake(theWidth/2-20, bottomView.frame.minY-20, 40, 40)
iconLbl.font = UIFont.fontAwesomeOfSize(23)
iconLbl.text = String.fontAwesomeIconWithName(.Info)
iconLbl.layer.masksToBounds = true
iconLbl.layer.cornerRadius = iconLbl.frame.size.width/2
iconLbl.layer.borderWidth = 5
iconLbl.layer.borderColor = topBackgroundColor.CGColor
iconLbl.backgroundColor = UIColor.cyanColor()
iconLbl.textColor = UIColor.whiteColor()
Is there something I'm missing?
Or am I going to have to figure out another to achieve this effect?
Thanks!
EDIT:
List of things I've tried so far!
Changing layer.borderWidth
Fussing around with clipsToBounds/MasksToBounds
Playing around the the layer.frame
Playing around with an integral frame
EDIT 2:
No fix was found! I used a workaround by extending this method on to my UIViewController
func makeFakeBorder(inputView:UIView,width:CGFloat,color:UIColor) -> UIView {
let fakeBorder = UIView()
fakeBorder.frame = CGRectMake(inputView.frame.origin.x-width, inputView.frame.origin.y-width, inputView.frame.size.width+width*2, inputView.frame.size.height+width*2)
fakeBorder.backgroundColor = color
fakeBorder.clipsToBounds = true
fakeBorder.layer.cornerRadius = fakeBorder.frame.size.width/2
fakeBorder.addSubview(inputView)
inputView.center = CGPointMake(fakeBorder.frame.size.width/2, fakeBorder.frame.size.height/2)
return fakeBorder
}
I believe this is the way a border is drawn to a layer in iOS. In the document it says:
When this value is greater than 0.0, the layer draws a border using the current borderColor value. The border is drawn inset from the receiver’s bounds by the value specified in this property. It is composited above the receiver’s contents and sublayers and includes the effects of the cornerRadius property.
One way to fix this is to apply a mask to a view's layer, but I found out that even if so we still can see a teeny tiny line around the view when doing snapshot tests. So to fix it more, I put this code to layoutSubviews
class MyView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let maskInset: CGFloat = 1
// Extends the layer's frame.
layer.frame = layer.frame.inset(dx: -maskInset, dy: -maskInset)
// Increase the border width
layer.borderWidth = layer.borderWidth + maskInset
layer.cornerRadius = bounds.height / 2
layer.maskToBounds = true
// Create a circle shape layer with true bounds.
let mask = CAShapeLayer()
mask.path = UIBezierPath(ovalIn: bounds.inset(dx: maskInset, dy: maskInset)).cgPath
layer.mask = mask
}
}
CALayer's mask

Swift: Mask Alignment + Auto-Layout Constraints

I have this PNG file, which I'd like to use as a mask for a UIView.
The view must be:
20 pixels/points in from each side
A perfect square
Centered vertically
I set the following constraints to accomplish this:
However, it seems these constraints don't play well with masks. When these constraints and the mask property are set, I get the following:
but I'd like the view to look just like the mask above, except orange (The backgroundColor here is just for simplicity—I later add subviews that need to be masked.)
However, when no constraints are set, the mask seems to work properly and I get something like this (borderColor added for visual purposes only):
Here's my code (viewForLayer is a UIView I made in the storyboard):
viewForLayer.layer.borderColor = UIColor.redColor().CGColor
viewForLayer.layer.borderWidth = 10
var mask = CALayer()
mask.contents = UIImage(named: "TopBump")!.CGImage
mask.frame = CGRect(x: 0, y: 0, width: viewForLayer.bounds.width, height: viewForLayer.bounds.height)
mask.position = CGPoint(x: viewForLayer.bounds.width/2, y: viewForLayer.bounds.height/2)
viewForLayer.layer.mask = mask
viewForLayer.backgroundColor = UIColor.orangeColor()
The problem is though, that now the view isn't the right size or in the right position—it doesn't follow the rules above—"The view must be: ". How can I have the mask work properly, and the auto-layout constraints set at the same time?
I found a way around it. Not sure if this is the best way but here we go...
http://imgur.com/pUIZbNA
Just make sure you change the name of the UIView class in the storyboard inspector too. Apparently, the trick is to set the mask frame for each layoutSubviews call.
class MaskView : UIView {
override func layoutSubviews() {
super.layoutSubviews()
if let mask = self.layer.mask {
mask.frame = self.bounds
}
}
}
class ViewController: UIViewController {
#IBOutlet weak var viewForLayer: MaskView!
override func viewDidLoad() {
super.viewDidLoad()
let image = UIImage(named: "TopBump")!.CGImage!
let maskLayer = CALayer()
maskLayer.contents = image
maskLayer.frame = viewForLayer.bounds
viewForLayer.layer.mask = maskLayer
viewForLayer.backgroundColor = UIColor.orangeColor()
// Do any additional setup after loading the view, typically from a nib.
viewForLayer.layer.borderColor = UIColor.redColor().CGColor
viewForLayer.layer.borderWidth = 10
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I tried it for myself. Minus the nitpicking on 'let mask = CALayer()' (it's immutable reference to an updatable object), changing the autolayout constraints of the embedded view shows the mask is aligned correctly.
NSLog("\(viewForLayer.bounds.width), \(viewForLayer.bounds.height)")
returns 375.0, 667.0 on an iPhone 6 screen. What are you getting?