Programmatically created shadow doesn't display - ios

I have iOS app where I have a couple views with a shadow (Called them shadowView for convenience). This is how they're made:
let shadowView = UIView(frame: .zero)
self.addSubview(shadowView)
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 0.5, height: 1.5)
shadowView.layer.shadowOpacity = 0.15
shadowView.layer.masksToBounds = false
shadowView.layer.cornerRadius = 8
shadowView.backgroundColor = .clear
shadowView.layer.shadowRadius = 5.0
shadowView.clipsToBounds = false
shadowView.layer.shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: 20).cgPath
The view is then placed in position with Snapkit, but I left out that part of the code due to it being irrelevant. Now the shadow that I made here is not displayed. However, if I set the backgroundColor property to e.g. UIColor.yellow then the view itself shows, but not the shadow.
I also checked if the shadow might be cut off by any parent view, which doesn't seem to be the case.
As you can see it's not the usual clipsToBounds / masksToBounds mistake, and I've been looking at this for the last couple hours. Am I missing a piece of code maybe? Or did I miss anything?

Your frame size is .zero. Assign some valid size to it.

Give the view a non-zero frame size
let viewWidth = //any
let viewHeight = //any
let shadowView = UIView(frame: CGRect.init(x: 0, y: 0, width: viewWidth, height: viewHeight))

To try and clarify...
You are setting .shadowPath, but based on you code you're setting it prior to .shadowView having its frame set. So, you are creating a UIBezierPath with a frame of .zero -- and it never changes.
You need to set the shadow path either in viewDidLayoutSubviews of a view controller, or by overriding layoutSubviews() inside your custom view.
Here is a simple example that you can run in a Playground page:
import UIKit
import PlaygroundSupport
class TestViewController : UIViewController {
let shadowView = UIView(frame: .zero)
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(shadowView)
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOpacity = 0.15
shadowView.layer.cornerRadius = 8
shadowView.backgroundColor = .clear
// this would be handled by your use of SnapKit
shadowView.translatesAutoresizingMaskIntoConstraints = false
shadowView.topAnchor.constraint(equalTo: view.topAnchor, constant: 40.0).isActive = true
shadowView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true
shadowView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0).isActive = true
shadowView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
shadowView.layer.shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: 20).cgPath
}
}
let vc = TestViewController()
vc.view.backgroundColor = .white
PlaygroundPage.current.liveView = vc

Like other colleagues said, the frame of your view is the problem.
This is a problem just because you set the layer's shadow path to be as big as the view's bounds that in that moment is .zero.
Still I understand that you want your view to react to its constraint when the auto-layout engine is ready to layout your views. There are then two things you can do to address this problem:
1) you can remove the explicit set of shadowView.layer.shadowPath and change the color of the view from clear to another color. This will cause the shadow to follow the shape of the view even if it changes, with a dynamic shadow path. While this will fix the issue, sometimes it results also in an impact on performance during animations and transitions.
2) if the view doesn't change its size during her life on screen you can force the superView to re-layout itself right after you add the shadowView as subview.
self.addSubview(shadowView)
self.setNeedsLayout()
self.layoutIfNeeded()
In this way when you'll create the shadow path bounds will not be zero because auto layout already did its magic changing the frame of your view.
Please note that with the second solution you will not see the view (that has clear color) but just its shadow.

Swift 4
Add this function in your view controller
func addShadow(myView: UIView?) {
myView?.layer.shadowColor = UIColor.black.cgColor
myView?.layer.shadowOffset = CGSize(width: 0, height:0)
myView?.layer.shadowOpacity = 0.3
myView?.layer.shadowRadius = 5
myView?.layer.masksToBounds = false
myView?.layer.cornerRadius = 12
}
and use it like:
let shadowView = UIView(frame: CGRect(x: 0, y: 0, width: yourWidth, height: yourHeight))
self.addShadow(shadowView)

Related

Problem with rounded corner in UITableView in Objective C [duplicate]

Hello i have some views with rounded corners, and I'd like to apply a shadow to this views.
SO first I round the view :
view.layer.cornerRadius = 15
view.clipsToBounds = true
then I apply the shadow :
func dropShadow(scale: Bool = true) {
self.layer.masksToBounds = false
self.layer.cornerRadius = 15
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.layer.cornerRadius).cgPath
self.layer.shadowOffset = CGSize(width: 3, height: 3)
self.layer.shadowOpacity = 0.5
self.layer.shadowRadius = 5
}
view.dropShadow()
I got my rounded view with a shadow but the shadow is not rounded like my view. The shadow is not rounded at all
You cannot cast a shadow from a view whose clipsToBounds is true. If a view's masksToBounds is true, its clipsToBounds is true; they are the same thing.
If you want a shadow to appear to come from from a view that clips, you need to use two views: one that the user can see, with rounded corners and clipsToBounds set to true, and another that the user can't see because it's behind the first one, also with rounded corners, but with clipsToBounds set to false, to cast the shadow.
class ShadowView : UIView {
override init(frame: CGRect) {
super.init(frame:frame)
self.isOpaque = true
self.backgroundColor = .black
self.dropShadow()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func dropShadow() {
self.layer.masksToBounds = false
self.layer.cornerRadius = 15
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 3, height: 3)
self.layer.shadowOpacity = 0.5
self.layer.shadowRadius = 5
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let r = CGRect(x: 100, y: 100, width: 100, height: 100)
let v = UIImageView(frame:r)
v.image = UIImage(named:"marsSurface.jpg")
v.clipsToBounds = true
v.backgroundColor = .red
v.layer.cornerRadius = 15
self.view.addSubview(ShadowView(frame:r))
self.view.addSubview(v)
}
}
Note that neither view is a subview of the other, nor do they have a superview that clips, as that would clip the shadow.
Trying to have a single view handle both corner rounding and shadow logic is not recommended. The best way to create the effect you are looking for is to wrap your rounded view with another parent view that owns the shadow.
This can be best illustrated with a playground. Try this code out in a playground.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let shadowView = UIView()
shadowView.frame = CGRect(x: 50, y: 200, width: 200, height: 100)
shadowView.backgroundColor = nil
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 0, height: 4.0)
shadowView.layer.shadowRadius = 8.0
shadowView.layer.shadowOpacity = 1
let roundedView = UIView()
roundedView.frame = shadowView.bounds
roundedView.backgroundColor = .gray
roundedView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner,.layerMaxXMinYCorner]
roundedView.layer.cornerRadius = 10
roundedView.layer.masksToBounds = true
shadowView.addSubview(roundedView)
view.addSubview(shadowView)
view.backgroundColor = UIColor.white
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Don't forget to set the shadowPath property on your shadow view's layer in either viewWillLayoutSubviews or layoutSubviews. If this property is set to nil, UIKit has to perform off screen rendering to figure out how to draw the shadow rect.

autoresizingMask not working as expected for UITableView.tableHeaderView's subview

I'm adding a header view to my UITableView and want to add a subview to it having some margins.
class ViewController: UIViewController {
private let tablewView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
tablewView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tablewView)
[
tablewView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
tablewView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
tablewView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tablewView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
].forEach{ $0.isActive = true}
let headerView = UIView(frame: CGRect(x: 0, y: 120, width: 200, height: 100))
headerView.backgroundColor = .blue
let subView = UIView(frame: CGRect(x: 10, y: 10, width: 180, height: 80))
subView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
subView.backgroundColor = .yellow
headerView.addSubview(subView)
tablewView.tableHeaderView = headerView
}
}
The problem is that the right margin isn't preserved when the header is resized (when the table view is laid out). As you can see on the image, the right margin is missing:
If I'm using the same view without UITableView, then the margin is preserved as expected.
Is it a UIKit bug? Are there any workarounds?
I know that I can try AutoLayout solutions from here Is it possible to use AutoLayout with UITableView's tableHeaderView? but they're looking a bit hacky. autoresizingMask is supposed to work, after all.
In Cocoa programming as in comedy, timing is everything.
Add the subview in a one-time implementation of viewDidLayoutSubviews and all will be well. The subview will appear correctly, and will continue working if the table view is resized (e.g. due to rotation of the interface).
So, cut these four lines:
let subView = UIView(frame: CGRect(x: 10, y: 10, width: 180, height: 80))
subView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
subView.backgroundColor = .yellow
headerView.addSubview(subView)
And instead:
var didLayout = false
override func viewDidLayoutSubviews() {
guard !didLayout else { return }
didLayout.toggle()
if let h = tablewView.tableHeaderView {
let subView = UIView(frame: h.bounds.insetBy(dx: 10, dy: 10))
subView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
subView.backgroundColor = .yellow
h.addSubview(subView)
}
}

Can you add a UIView to a UIStackview?

I have an HStack of content like title, description, etc. Then I have some rectangles I want to add to the UIStackView programatically. But they do not appear.
In Playground in Swift 4
It works when appended to the overall view:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
// Add rects
let rectFrame: CGRect = CGRect(x:0, y:0, width:100, height:16)
let stepRect = UIView(frame: rectFrame)
stepRect.backgroundColor = UIColor.darkGray
let rectFrame2: CGRect = CGRect(x:50, y:16, width:100, height:16)
let stepRect2 = UIView(frame: rectFrame2)
stepRect2.backgroundColor = UIColor.red
self.view.addSubview(stepRect)
self.view.addSubview(stepRect2)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
This returns a white screen with two rectangles, one gray and one red, stacked on top of each other with some offset.
But is not working when appended to a UIStackView:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
// Make vstack
let vStack = UIStackView()
vStack.axis = .vertical
vStack.frame = CGRect(x: 0, y: 0, width: 300, height: 20)
// Add rects
let rectFrame: CGRect = CGRect(x:0, y:0, width:100, height:16)
let stepRect = UIView(frame: rectFrame)
stepRect.backgroundColor = UIColor.darkGray
let rectFrame2: CGRect = CGRect(x:50, y:16, width:100, height:16)
let stepRect2 = UIView(frame: rectFrame2)
stepRect2.backgroundColor = UIColor.red
vStack.addArrangedSubview(stepRect)
vStack.addArrangedSubview(stepRect2)
view.addSubview(vStack)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
This returns a white screen.
Yes, you can “add a UIView to a UIStackView.
But the whole point of a stack view is to let the placement of these “arranged” subviews. One supplies a distribution of the subviews (defaults to .fill) and an alignment of the subviews (again, defaults to .fill).
But you have specified ambiguous frames for the stack view and its subviews (which don’t happen to have any intrinsic size). Also, the notion of different x coordinates in a vertical stack view doesn't really make sense, either, as the alignment dictates the placement. If you run this in an actual app and then use the “view debugger”, it will report this ambiguity:
Unfortunately, playgrounds don't supply this sorts of diagnostic.
So, if using stack views, you either supply views with intrinsic sizes and let the stack view dictate the size and placement, or define supply the stack view subviews with size constraints and then ask it to manage the spacing between them (by using distribution of .equalSpacing or .equalCentering).
And if you really want to set the x and y values for your views manually, then it doesn't make sense to use a stack view at all.
A few examples: Perhaps you want define the two subviews to have the same size as each other, spaced 10 pt apart, for a total height of 50 pt:
let stackView = UIStackView(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.alignment = .fill
stackView.spacing = 10
Or perhaps you want your two subviews of specific size spaced within this 50 pt tall stack view:
let stackView = UIStackView(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
stackView.axis = .vertical
stackView.distribution = .equalSpacing
stackView.alignment = .center
let grayView = UIView()
grayView.translatesAutoresizingMaskIntoConstraints = false
grayView.backgroundColor = .darkGray
let redView = UIView()
redView.translatesAutoresizingMaskIntoConstraints = false
redView.backgroundColor = .red
stackView.addArrangedSubview(grayView)
stackView.addArrangedSubview(redView)
view.addSubview(stackView)
NSLayoutConstraint.activate([
redView.widthAnchor.constraint(equalToConstant: 100),
redView.heightAnchor.constraint(equalToConstant: 16),
grayView.widthAnchor.constraint(equalToConstant: 100),
grayView.heightAnchor.constraint(equalToConstant: 16)
])
There are lots of other examples we could give you, but, in short, it simply doesn’t make sense to set frames of stack view and arranged subviews (esp with different x values) and hoping that the stack view will be able to reconcile this solely with with .fill distribution.
From Apple's UIStackView documentation:
For all distributions except the UIStackView.Distribution.fillEqually
distribution, the stack view uses each arranged view’s
intrinsicContentSize property when calculating its size along the
stack’s axis. UIStackView.Distribution.fillEqually resizes all the
arranged views so they are the same size, filling the stack view along
its axis. If possible, the stack view stretches all the arranged views
to match the view with the longest intrinsic size along the stack’s
axis.
Your UIViews have no content and so when the stack views lay them out it will not see any intrinsicContentSize it needs to preserve. To see a different behavior that shows how a stack view handles its arranged subviews, try putting non-empty text views or image views in the stack. You can also try setting
vStack.distribution = .fillEqually
If you want to be able to have a generic UIView hold the size you set, one way is to create your own minimal subclass of UIView and override its intrinsicContentSize property, returning the view's self.bounds.size

Is it possible to use Auto Layout in a UITextField's leftView?

I want to customize a UITextField's leftView with a view that is automatically sized depending on its contents:
func set(leftImage image: UIImage) {
let imageView = UIImageView(image: image)
let paddingContainer = UIView()
// This is the crucial point:
paddingContainer.translatesAutoresizingMaskIntoConstraints = false
paddingContainer.addSubview(imageView)
imageView.pin(toMarginsOf: paddingContainer)
leftView = paddingContainer
leftViewMode = .always
}
where the pin method just pins the image view on all four sides to the margins of the paddingContainer:
func pin(toMarginsOf view: UIView) {
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor)
])
}
On iOS 12, everything works as expected, but on iOS versions < 12, the image is completely misplaced. It's not even within the bounds of the text field but in the upper left corner of my view controller's view.
To me it seems like older versions of iOS don't support using Auto Layout inside the view that you set as a text field's leftView. The documentation states:
The left overlay view is placed in the rectangle returned by the leftViewRect(forBounds:) method of the receiver.
but it doesn't state how it's placed there: By using constraints or by setting the frame directly.
Are there any reliable sources or educated guesses if using Auto Layout is supported at all for the leftView?
extension UITextField{
func setLeft(image: UIImage, withPadding padding: CGFloat = 0) {
let wrapperView = UIView.init(
frame: CGRect.init(
x: 0,
y: 0,
width: bounds.height,
height: bounds.height
)
)
let imageView = UIImageView()
imageView.image = image
imageView.contentMode = .scaleAspectFit
wrapperView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(
equalTo: wrapperView.leadingAnchor,
constant: padding
),
imageView.trailingAnchor.constraint(
equalTo: wrapperView.trailingAnchor,
constant: -padding
),
imageView.topAnchor.constraint(
equalTo: wrapperView.topAnchor,
constant: padding
),
imageView.bottomAnchor.constraint(
equalTo: wrapperView.bottomAnchor,
constant: -padding
)
])
leftView = wrapperView
leftViewMode = .always
}
}
hope this will help
You can set its frame on layout subviews function like this
override func layoutSubviews() {
super.layoutSubviews()
if let lv = self.leftView {
lv.frame = CGRect(x: 0, y: 0, width: self.bounds.height, height: self.bounds.height)
}
}

Creating a UIContainerView() programmatically and changing its height

I'm creating a container view programmatically. However, i'm not able to change its height. I'm setting it programmatically but without any effect.
let supportView: UIView = UIView()
let containerView = UIView()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
self.containerView.frame = CGRect(x: self.view.frame.size.width - 100, y: 200, width: 225, height: 70)
print(self.containerView.frame.height)
self.containerView.backgroundColor = UIColor.gray
self.containerView.layer.cornerRadius = 20
self.view.addSubview(self.containerView)
let controller = storyboard!.instantiateViewController(withIdentifier: "Storyboard2")
addChildViewController(controller)
containerView.addSubview(controller.view)
controller.didMove(toParentViewController: self)
}
I created the view controller with identifier "Storyboard2" in the storyboard. And i've set its height to 70 there too. But without any luck.
Any Help? Thanks
You have not set the clipsToBounds on containerView, and the default value of that property is false.
Add this line just under you are setting the containerView's frame:
containerView.clipsToBounds = true
Also, as some reading material, i would like to present to you this discussion about the clipsToBounds property.

Resources