Constraints affecting shadow of UIView? - ios

I am trying to establish a shadow on all four sides of my UIView in my ViewController — which works perfectly fine until I add constraints to the UIView via Xcode. How can I make the shadow of the UIView display on all four sides with set constraints for all sides?
Essentially, what I've discovered is that whenever I apply constraints via Xcode to the UIView that I have set the shadow around, the shadow does not appear under all four sides of the UIView. Instead, it floats to the left, leaving the right and bottom sides completely without shadowing. This can be seen in the screenshots beneath.
https://imgur.com/a/bZgYPH8
class RegistrationViewController: UIViewController {
#IBOutlet weak var signUpView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
signUpView.clipsToBounds = true
signUpView.layer.cornerRadius = 20;
signUpView.layer.shadowPath = UIBezierPath(roundedRect: signUpView.bounds, cornerRadius: signUpView.layer.cornerRadius).cgPath
signUpView.layer.shouldRasterize = true
signUpView.layer.shadowColor = UIColor.black.cgColor
signUpView.layer.shadowOffset = .zero
signUpView.layer.shadowOpacity = 1
signUpView.layer.shadowRadius = 20
signUpView.layer.masksToBounds = false
// Do any additional setup after loading the view.
}
}
Why is this happening and how can I add constraints while keeping the desired result?

In viewDidLoad, your constraints have not taken affect yet. So when you create your UIBezierPath, the bounds are not updated to the auto-layout constraints. The bounds that are used are probably the ones in your .xib file.
Move your shadowMakingPath into viewDidLayoutSubviews
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubivews()
signUpView.layer.shadowPath = UIBezierPath(roundedRect: signUpView.bounds, cornerRadius: signUpView.layer.cornerRadius).cgPath
signUpView.layer.shouldRasterize = true
signUpView.layer.shadowColor = UIColor.black.cgColor
signUpView.layer.shadowOffset = .zero
signUpView.layer.shadowOpacity = 1
signUpView.layer.shadowRadius = 20
signUpView.layer.masksToBounds = false
}
More information .. Objective-C/Swift (iOS) When are the auto-constraints applied in the View/ViewController work flow?

Related

Add/ expand animation will cause unwanted UIScrollView scrolling

I notice that, if I perform add/ expand animation within an UIScrollView, it will cause unwanted scrolling behavior, when the UIScrollView fill with enough content to become scroll-able.
As you can see in the following animation, initially, the add/ expand animation works just fine.
When we have added enough item till the UIScrollView scrollable, whenever a new item is added, and UIScrollView will first perform scroll down, and then scroll up again!
My expectation is that, the UIScrollView should remain static, when add/ expand animation is performed.
Here's the code which performs add/ expand animation.
Add/ expand animation
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
// Clear off horizontal swipe in animation caused by addArrangedSubview
stackView.superview?.layoutIfNeeded()
customView.show()
// Perform expand animation.
UIView.animate(withDuration: 1) {
self.stackView.superview?.layoutIfNeeded()
}
}
Here's the constraint setup of the UIScrollView & added custom view item
Constraint setup
Custom view
class CustomView: UIView {
private var zeroHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.cornerRadius = stackView.frame.height / 2
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
zeroHeightConstraint = self.safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 0)
zeroHeightConstraint.isActive = false
}
func hide() {
zeroHeightConstraint.isActive = true
}
func show() {
zeroHeightConstraint.isActive = false
}
}
Here's the complete source code
https://github.com/yccheok/add-expand-animation-in-scroll-view
Do you have any idea why such problem occur, and we can fix such? Thanks.
Because of the way stack views arrange their subviews, animation can be problematic.
One approach that you may find works better is to embed the stack view in a "container" view.
That way, you can use the .isHidden property when adding an arranged subview, and allow the animation to update the "container" view:
The "add view" function now becomes (I added a Bool so we can skip the animation on the initial add in viewDidLoad()):
func addCustomView(_ animated: Bool) {
let customView = CustomView.instanceFromNib()
stackView.addArrangedSubview(customView)
customView.isHidden = true
if animated {
DispatchQueue.main.async {
UIView.animate(withDuration: 1) {
customView.isHidden = false
}
}
} else {
customView.isHidden = false
}
}
And we can get rid of all of the hide() / show() and zeroHeightConstraint in the custom view class:
class CustomView: UIView {
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
borderView.layer.cornerRadius = borderView.bounds.height * 0.5
}
}
Since it's a bit difficult to clearly show everything here, I forked your project with the changes: https://github.com/DonMag/add-expand-animation-in-scroll-view
Edit
Another "quirk" of animating a stack view shows up when adding the first arranged subview (also, when removing the last one).
One way to get around that is to add an empty view as the first subview.
So, for this example, in viewDidLoad() before adding an instance of CustomView:
let v = UIView()
stackView.addArrangedSubview(v)
This will make the first arranged subview a zero-height view (so it won't be visible).
Then, if you're implementing removing custom views, just make sure you don't remove that first, empty view.
If your stack view has .spacing = 0 noting else is needed.
If your stack view has a non-zero spacing, add another line:
let v = UIView()
stackView.addArrangedSubview(v)
stackView.setCustomSpacing(0, after: v)
I did a little research on this and the consensus was to update the isHidden and alpha properties when inserting a view with animations.
In CustomView:
func hide() {
alpha = 0.0
isHidden = true
zeroHeightConstraint.isActive = true
}
func show() {
alpha = 1.0
isHidden = false
zeroHeightConstraint.isActive = false
}
In your view controller:
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
self.stackView.layoutIfNeeded()
UIView.animate(withDuration: 00.5) {
customView.show()
self.stackView.layoutIfNeeded()
}
}
Also, the constraints in your storyboard aren't totally correct. You are seeing a red constraint error because autolayout doesn't know the height of your stackView. You can give it a fake height and make sure that "Remove at build time" is checked.
Also, get rid of your scrollView contentView height constraint defined as View.height >= Frame Layout Guide.height. Autolayout doesn't need to know the height, it just needs to know how subviews inside of the contentView stack up to define its vertical content size.
Everything else looks pretty good.

UIScrollView with multiple UIView/UIImageView stacked on each other for drawing

I can't believe something this easy when I did not use auto-layout is this hard with auto-layout. I put everything in a contentView so that it's a simultaneous zoom for both views (ImageView and UIView). The UIView for drawing should be the same size as the UIImageView and not bigger. I have this hierarchy at the moment.
UIScrollView
- ContentView
-- UIView (For drawing eg, drawView)
-- UIImage (For showing a background image to draw on)
The drawView is a view on top of the imageView, the problem now is as following:
Users can draw out of bounds of the UIImageView. This should only be possible drawing on the imageView.
Drawings are under the UIImageView, while the drawView is on top.
Code:
var afbeelding: UIImage?
#IBOutlet var imageView: UIImageView!
#IBOutlet var drawView: DrawingCanvas!
#IBOutlet var contentView: UIView!
var dag: Dag?
#IBOutlet var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
drawView.dag = dag
if let afbeelding = afbeelding {
imageView.image = afbeelding
}
scrollView.panGestureRecognizer.minimumNumberOfTouches = 2
scrollView.panGestureRecognizer.maximumNumberOfTouches = 2
scrollView.minimumZoomScale = 0.2;
scrollView.zoomScale = 1.0;
scrollView.maximumZoomScale = 5.0
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return contentView
}
Images:
Could anyone steer me in the right direction?
I'm not very familiar with Interface Builder but for your layer order problem I'd try swapping them; perhaps the order in which they're shown is the order in which they're added.
As for drawing outside the bounds, you'd need to look at the documentation for DrawingCanvas but perhaps its clipsToBounds property will work?
Users can draw out of bounds of the UIImageView. This should only be possible drawing on the imageView.
It seems like your image view has its contentMode set to .scaleAspectFit, but has all 4 sides pinned to the superview. This means that the image view spans the entire contentView, but the image itself is smaller.
I think the easiest way around this is to get the frame of the actual image using AVMakeRect(aspectRatio:insideRect:), then adjusting drawView's frame.
let rect = AVMakeRect(aspectRatio: image.size, insideRect: imageView.frame)
/**
Instead of constraining `drawView`'s edges, use x, y, width, and height.
This makes setting them easier.
You'll need to connect your constraints via an #IBOutlet (see https://stackoverflow.com/a/40584432/14351818)
*/
drawViewLeftConstraint.constant = rect.origin.x
drawViewTopConstraint.constant = rect.origin.y
drawViewWidthConstraint.constant = rect.width
drawViewHeightConstraint.constant = rect.height
Drawings are under the UIImageView, while the drawView is on top.
Actually, the image view is on top. The order is lowest -> highest, so just switch them.

How to create the shadow of material-like table view cells?

I am trying to make my table view cells look "material". Here is something similar to what I want to do (source):
Note that there is a shadow around the whole table view in the above image. What I want is that shadow, but applied to each table view cell, instead of the whole table view.
I first designed my cell in an XIB file. I put a UIView called containerView as a subview of the content view. I added constraints so that the containerView has a top, bottom, left, right margin of 8. This is so that the containerView is a little smaller than the content view, so that the shadow I put on it will be visible.
I also added a UILabel called label as the subview of containerView to show some text.
This is the UITableViewCell subclass:
class QueueItemCell: UITableViewCell {
#IBOutlet var label: UILabel!
#IBOutlet var container: UIView!
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
...
}
override func setSelected(_ selected: Bool, animated: Bool) {
...
}
override func awakeFromNib() {
container.layer.shadowColor = UIColor.black.cgColor
container.layer.shadowOpacity = 0.7
container.layer.shadowOffset = CGSize(width: 3, height: 9)
container.layer.shadowRadius = 4
container.layer.cornerRadius = 4
container.layer.shadowPath = UIBezierPath(roundedRect: container.bounds, cornerRadius: 4).cgPath
selectionStyle = .none
}
}
There is nothing special about the data source and delegate methods except that I set the cells' height to 61 in heightForRowAt.
When I run the app, I got something like this:
The shadow on the bottom and left edges are quite good. But the right edge is a total disaster. The top edge also does not have a shadow, which is undesirable. I tried to do trial and error with shadowPath and shadowOffset but there's always one or two edges that looks bad.
How can I achieve a shadow on all edges of the cell, as shown in the first image?
in awakeFromNib you have wrong view size. You need to move container.layer.shadowPath = UIBezierPath(roundedRect: container.bounds, cornerRadius: 4).cgPath into layoutSubviews
or remove this code
container.layer.shadowPath = UIBezierPath(roundedRect: container.bounds, cornerRadius: 4).cgPath
so shadow will be configured automatically

Discrepancy in the widths of programmatically added views vs. interface builder views

I have come across a conundrum of sorts in regards unexpected (at least for me) sizes of UIViews.
Consider this UIViewController class. interfaceBuilderView was declared in a Storyboard file and constrained to take up the whole area of the UIView. So, I would expect to have interfaceBuilderView be the same size as programicallyCreatedView when calling *.frame.width. But they aren't.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var interfaceBuilderView: MyCustomView!
override func viewDidLoad() {
super.viewDidLoad()
let programmicallyCreatedView = MyCustomView(frame: self.view.frame)
//commented out this to get first picture
self.view.addSubview(programmicallyCreatedView)
self.interfaceBuilderView.setAppearance()
self.programmicallyCreatedView.setAppearance()
print(self.view.frame.width)//prints 375
print(self.interfaceBuilderView.frame.width)//prints 600
print(self.programmicallyCreatedView.frame.width)//prints 375
}
}
Now, consider this implementation of the MyCustomView class.
import UIKit
class MyCustomView: UIView {
func setAppearance() {
let testViewWidth: CGFloat = 200.0
let centerXCoor = (self.frame.width - testViewWidth) / 2.0
let testView = UIView(frame: CGRectMake(centerXCoor, 0, testViewWidth, 100))
testView.backgroundColor = UIColor.redColor()
self.addSubview(testView)
}
}
As you can see, I simply draw a red rectagle of width 200.0, and it is supposed to be centered. Here are the results.
Using the Interface Builder created view.
And using the programmatically created view.
As you can see, the programmatically created view achieves the desired results. No doubt because the size printed is the same as the superview (375).
Therefore, my question is simply why is this happening? Furthermore, how can I use a view declared in interface builder and programmatically add other views to it with dimensions and placement that I expect?
A few thoughts:
This code is accessing frame values in viewDidLoad, but the frame values are not yet reliable at that point. The view hasn't been laid out yet. If you're going to mess around with custom frame values, do this in viewDidAppear or viewDidLayoutSubviews.
Nowadays, we really don't generally use frame values anymore. Instead, we define constraints to define the layout programmatically. Unlike custom frame values, you can define constraints when you add the subviews in viewDidLoad.
You have the scene's main view, the MyCustomView and then yet another UIView which is red. That strikes me as unnecessarily confusing.
I would advise that you just add your programmatically created subview in viewDidLoad and specify its constraints. Using the new iOS 9 constraints syntax, you can just specify that it should be centered, adjacent to the top of the view, half the width, and one quarter the height:
override func viewDidLoad() {
super.viewDidLoad()
let redView = UIView()
redView.backgroundColor = UIColor.redColor()
redView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(redView)
NSLayoutConstraint.activateConstraints([
redView.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor),
redView.topAnchor.constraintEqualToAnchor(view.topAnchor),
redView.widthAnchor.constraintEqualToAnchor(view.widthAnchor, multiplier: 0.5),
redView.heightAnchor.constraintEqualToAnchor(view.heightAnchor, multiplier: 0.25)
])
}
Clearly, adjust these constraints as suits you, but hopefully this illustrates the idea. Don't use frame anymore, but rather use constraints.

Mask in UITableViewCell subclass not properly rendering on first load

I am trying to create a UITableViewCell subclass containing two rounded views, one on top and one on bottom, that together end up as a rounded rectangular view inside the cell, with indented space on all 4 sides (set by auto layout constrains in the storyboard for the prototype cell). These cells are part of a tableview that is loaded into a UIContainerView which has its contents swapped out based on the selection of a selection control.
Here is what I want the cell to look like (blacked out):
Here is what it looks like briefly, when first loading:
Here is what it looks like after it first loads:
When I switch to a different tab, then come back, it renders the cell correctly.
I use this method in the parent view controller (adapted from this)
func cycleFromViewController(oldViewController: UIViewController, toViewController newViewController: UIViewController) {
oldViewController.willMoveToParentViewController(nil)
self.addChildViewController(newViewController)
self.addSubView(newViewController.view, toView:self.containerView!)
newViewController.view.alpha = 0
newViewController.view.layoutIfNeeded()
UIView.animateWithDuration(0.25, animations: {
newViewController.view.alpha = 1
oldViewController.view.alpha = 0
},
completion: { finished in
oldViewController.view.removeFromSuperview()
oldViewController.removeFromParentViewController()
newViewController.didMoveToParentViewController(self)
})
}
The parent view controller's viewDidLoad method is called like this:
override func viewDidLoad() {
... // grab data in a background network call, populating the array of model objects
self.currentSelectedViewController!.view.translatesAutoresizingMaskIntoConstraints = false
self.addChildViewController(self.currentSelectedViewController!)
self.addSubView(self.currentSelectedViewController!.view, toView: self.containerView)
self.refreshContainerView()
super.viewDidLoad()
}
refreshContainerView looks like this:
func refreshContainerView() {
let currentVC = self.currentSelectedViewController as! MyTableViewController
currentVC.modelObjectList = self.modelObjectList
self.label.hidden = true
self.button.hidden = true
currentVC.tableView.reloadData()
}
Here is my cell's layout subviews method:
override func layoutSubviews() {
super.layoutSubviews()
self.reminderView.backgroundColor = UIColor.grayColor()
if let aModel = self.model {
self.configureWithModel(aModel)
}
self.setMaskToView(self.topView, corners: UIRectCorner.TopLeft.union(UIRectCorner.TopRight))
self.setMaskToView(self.bottomView, corners: UIRectCorner.BottomLeft.union(UIRectCorner.BottomRight))
}
Any thoughts as to how to fix
1. the initial brief loading without the insets and
2. the final rendering of the initial load with the rounded corners on the right side not properly rendering?
This cell exists in a storyboard as a prototype, with the insets created via auto layout constraints. (a constant setting the top and bottom view's distance from the top, bottom, right and left as appropriate). Clearly these constraints work when the cell is reloaded, but not on the initial load for some reason that is escaping me.
Evidently the answer was fairly simple. The mask method was being called in layoutSubviews for the cell, the the views themselves did not yet have their bounds set. So I subclassed the view into a new RoundedView class, and added a var for the corners and a modified mask method:
class RoundedView: UIView {
var corners : UIRectCorner = []
override func layoutSubviews() {
self.setMaskForCorners(corners)
}
func setMaskForCorners(corners: UIRectCorner) {
let rounded = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: 10, height: 10))
let mask = CAShapeLayer()
mask.path = rounded.CGPath
self.layer.mask = mask
}
}
Then I changed the views to be that subclass and then call it like this:
self.topView.corners = UIRectCorner.TopLeft.union(UIRectCorner.TopRight)
self.bottomView.corners = UIRectCorner.BottomLeft.union(UIRectCorner.BottomRight)

Resources