How to add corner radius to UIStackView and mask subviews - swift

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)

Related

How do you move the uiimage inside the uiimageview to the right while retaining the aspect ratio of the original image?

I want to move an image to the right when a user imports it using uiimagepicker but when I set content mode = .right this occurs: The image enlarges for some reason and it looks like it moves to the left
Is there any way to keep the aspect ratio of the uiimageview and the aspect ratio of the imported image, and while also moving it to the right inside the image view.
This is how I want it to be
Here is one approach: custom view, using a sublayer with the content set to the image...
add a CALayer as a sublayer
calculate the aspect-scaled rectangle for the image inside the view's bounds
set the image layer's frame to that scaled rect
then set the layer's origin based on the desired alignment
A simple example:
class AspectAlignImageView: UIView {
enum AspectAlign {
case top, left, right, bottom, center
}
// this is an array so we can set two options
// if, for example, we don't know if the image will be
// taller or narrower
// for example:
// [.top, .right] will put a
// wide image aligned top
// narrow image aligned right
public var alignment: [AspectAlign] = [.center]
public var image: UIImage?
private let imgLayer: CALayer = CALayer()
override func layoutSubviews() {
super.layoutSubviews()
// make sure we have an image
if let img = image {
// only add the sublayer once
if imgLayer.superlayer == nil {
layer.addSublayer(imgLayer)
}
imgLayer.contentsGravity = .resize
imgLayer.contents = img.cgImage
// calculate the aspect-scaled rect inside our bounds
var scaledImageRect = CGRect.zero
let aspectWidth:CGFloat = bounds.width / img.size.width
let aspectHeight:CGFloat = bounds.height / img.size.height
let aspectRatio:CGFloat = min(aspectWidth, aspectHeight)
scaledImageRect.size.width = img.size.width * aspectRatio
scaledImageRect.size.height = img.size.height * aspectRatio
// set image layer frame to aspect-scaled rect
imgLayer.frame = scaledImageRect
// align as specified
if alignment.contains(.top) {
imgLayer.frame.origin.y = 0
}
if alignment.contains(.left) {
imgLayer.frame.origin.x = 0
}
if alignment.contains(.bottom) {
imgLayer.frame.origin.y = bounds.maxY - scaledImageRect.height
}
if alignment.contains(.right) {
imgLayer.frame.origin.x = bounds.maxX - scaledImageRect.width
}
}
}
}
class TestAlignViewController: UIViewController {
let testView = AspectAlignImageView()
override func viewDidLoad() {
super.viewDidLoad()
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
NSLayoutConstraint.activate([
// constrain test view 240x240 square
testView.widthAnchor.constraint(equalToConstant: 240.0),
testView.heightAnchor.constraint(equalTo: testView.widthAnchor),
// centered in view
testView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
testView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
if let img = UIImage(named: "bottle") {
testView.image = img
}
testView.alignment = [.right]
// so we can see the actual view frame
testView.backgroundColor = .green
}
}
Using this image:
in a 240x240 view (view background set green so we can see its frame), we get this result:
Set your UIImage Content mode to aspect fill or aspect fit. Then use auto layout.

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.

Masking A UIView With UILabel Using AutoLayout

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

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!

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?