iOS UIView shadow shown different in simulator - ios

I have a UIView and a shadow around it. The problem is that the shadow looks good in the simulator but not on a real device. Both simulator and my iPhone have the iOS version.
My code: I use it inside a UITableViewCell class.
override func awakeFromNib() {
super.awakeFromNib()
let shadowFrame: CGRect = cellView.layer.bounds
let shadowPath: CGPath = UIBezierPath(rect: shadowFrame).cgPath
cellView.layer.shadowOffset = CGSize(width: 0, height: 1)
cellView.layer.shadowColor = UIColor.gray.cgColor
cellView.layer.shadowRadius = 1
cellView.layer.shadowOpacity = 0.6
cellView.clipsToBounds = false
cellView.layer.shadowPath = shadowPath
}
The simulator, 2. my iPhone:
EDIT
The iPhone 7 simulator shows the same shadow as my iPhone 6.
Only the iPhone 7plus Simulator can display the shadow right. Is that a Xcode bug?

This code will work fine when the dimensions of the view are the same as your nib. However, awakeFromNib is likely to be called before the autolayout engine has completed its layout passes, meaning cellView.layer.bounds will not be set to the dimensions that rendered on screen. You should try moving the setting of your shadow path to layoutSubviews in your UITableViewCell subclass or cellForRowAtIndexPath in your UITableViewDataSource
Update
You can also try to just observe when your table view cell's frame is set and update the shadow path then.
class MyTableViewCell: UITableViewCell
{
var cellView: UIView?
override var frame: CGRect
{
didSet
{
guard let cellView = self.cellView, self.frame != oldValue else { return }
cellView.layer.shadowPath = UIBezierPath(rect: cellView.bounds).cgPath
}
}
}

Related

UIView - What override will allow updating a UIView frame

I have a UIView .xib, and am subclassing UIView. When I load the Nib, the frame of the view is set and awakeFromNib() is called.
I have 3 buttons. When I load the view, I pass in callbacks for each button. If one or more of the callbacks is nil, then I hide the buttons and resize the view:
let viewSize = 50
var adjustedSize = 0
if (self.option2String == nil){
self.option2View.isHidden = true
adjustedSize -= viewSize
}
if (self.option3String == nil){
self.option3View.isHidden = true
adjustedSize -= viewSize
}
let _size = self.innerView.frame.size
let size = CGSize(width: _size.width, height: _size.height + CGFloat(adjustedSize))
self.innerView.frame = CGRect(origin: self.innerView.frame.origin, size: size)
I have tried putting this code in awakeFromNib(), and didMoveToSuperview(), but the frame does not change size.
If I enclose the last line in DispatchQueue.main.async, then it works. But I'm concerned that this is just luck due to timing.
Is this best practice? Where can I resize a view from within a UIView subclass?
EDIT: Confirmed, the DispatchQueue.main.async is just luck. It only works 50% of the time.
Having the view inside a UIViewController, call a function that resizes your view inside the controller's viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
myView.resize()
}

Constraints affecting shadow of UIView?

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?

Slow load time for custom UIView in Swift

Background
In order to make a text view that scrolls horizontally for vertical Mongolian script, I made a custom UIView subclass. The class takes a UITextView, puts it in a UIView, rotates and flips that view, and then puts that view in a parent UIView.
The purpose for the rotation and flipping is so that the text will be vertical and so that line wrapping will work right. The purpose of sticking everything in a parent UIView is so that Auto layout will work in a storyboard. (See more details here.)
Code
I got a working solution. The full code on github is here, but I created a new project and stripped out all the unnecessary code that I could in order to isolate the problem. The following code still performs the basic function described above but also still has the slow loading problem described below.
import UIKit
#IBDesignable class UIMongolTextView: UIView {
private var view = UITextView()
private var oldWidth: CGFloat = 0
private var oldHeight: CGFloat = 0
#IBInspectable var text: String {
get {
return view.text
}
set {
view.text = newValue
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect){
super.init(frame: frame)
}
override func sizeThatFits(size: CGSize) -> CGSize {
// swap the length and width coming in and going out
let fitSize = view.sizeThatFits(CGSize(width: size.height, height: size.width))
return CGSize(width: fitSize.height, height: fitSize.width)
}
override func layoutSubviews() {
super.layoutSubviews()
// layoutSubviews gets called multiple times, only need it once
if self.frame.height == oldHeight && self.frame.width == oldWidth {
return
} else {
oldWidth = self.frame.width
oldHeight = self.frame.height
}
// Remove the old rotation view
if self.subviews.count > 0 {
self.subviews[0].removeFromSuperview()
}
// setup rotationView container
let rotationView = UIView()
rotationView.frame = CGRect(origin: CGPointZero, size: CGSize(width: self.bounds.height, height: self.bounds.width))
rotationView.userInteractionEnabled = true
self.addSubview(rotationView)
// transform rotationView (so that it covers the same frame as self)
rotationView.transform = translateRotateFlip()
// add view
view.frame = rotationView.bounds
rotationView.addSubview(view)
}
func translateRotateFlip() -> CGAffineTransform {
var transform = CGAffineTransformIdentity
// translate to new center
transform = CGAffineTransformTranslate(transform, (self.bounds.width / 2)-(self.bounds.height / 2), (self.bounds.height / 2)-(self.bounds.width / 2))
// rotate counterclockwise around center
transform = CGAffineTransformRotate(transform, CGFloat(-M_PI_2))
// flip vertically
transform = CGAffineTransformScale(transform, -1, 1)
return transform
}
}
Problem
I noticed that the custom view loads very slowly. I'm new to Xcode Instruments so I watched the helpful videos Debugging Memory Issues with Xcode and Profiler and Time Profiler.
After that I tried finding the issue in my own project. It seems like no matter whether I use the Time Profiler or Leaks or Allocations tools, they all show that my class init method is doing too much work. (But I kind of knew that already from the slow load time before.) Here is a screen shot from the Allocations tool:
I didn't expand all of the call tree because it wouldn't have fit. Why are so many object being created? When I made a three layer custom view I knew that it wasn't ideal, but the number of layers that appears to be happening from the call tree is ridiculous. What am I doing wrong?
You shouldn't add or delete any subview inside layoutSubviews, as doing so triggers a call to layoutSubviews again.
Create your subview when you create your view, and then only adjust its position in layoutSubviews rather than deleting and re-adding it.

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)

Changing the height of UIToolbar in iOS 7

I am trying to change the height of my UIToolbar in a new iOS 7 project but I am not able to.
I am using a UINavigationController to manage a couple of UIViewController.
I tried setting the frame for the toolbar via the navigation controller but alas, the toolbar property is read-only.
I looked at "Is there a way to change the height of a UIToolbar?" but that did not work.
I tried subclassing UIToolbar, forcing a custom height and setting the right class in the Storyboard but that did not work neither, height keeps on being 44px.
I thought about auto-layout could not set any constraint on the size of the toolbar, every field is disabled.
I can set a custom view in a UIBarButtonItem with a bigger height than the toolbar. The big item will be correctly rendered but it will overflow from the toolbar.
This is the best I could do: screenshot
Is it actually possible to change the height of the UIToolbar in iOS 7?
Or am I supposed to create a bunch of custom items to mimic it?
Following the #Antoine suggestion using sizeThatFits, here is my Toolbar subclass with an height of 64:
import UIKit
class Toolbar: UIToolbar {
override func layoutSubviews() {
super.layoutSubviews()
frame.size.height = 64
}
override func sizeThatFits(size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
size.height = 64
return size
}
}
Then, when initializing the navigation controller, I say it should use that class:
let navigationController = UINavigationController(navigationBarClass: nil, toolbarClass: Toolbar.self)
The easiest way I found to set the toolbar height was to use a height constraint as follows:
let toolbarCustomHeight: CGFloat = 64
toolbar.heightAnchor.constraintEqualToConstant(toolbarCustomHeight).active = true
I've fixed this by subclassing UIToolbar and pasting the following code:
override func layoutSubviews() {
super.layoutSubviews()
var frame = self.bounds
frame.size.height = 52
self.frame = frame
}
override func sizeThatFits(size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
size.height = 52
return size
}
If you are using same height for all screens, this should do the trick
extension UIToolbar {
open override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: 60)
}
}
Although many solutions point in the right direction, they have either some layout issues or doesn't work properly. So, here's my solution:
Swift 3, custom UIToolbar subclass
class Toolbar: UIToolbar {
let height: CGFloat = 64
override func layoutSubviews() {
super.layoutSubviews()
var newBounds = self.bounds
newBounds.size.height = height
self.bounds = newBounds
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
size.height = height
return size
}
}
You can customize the height of your UIToolbar in iOS 7 with the following code. I have it tested and working in my current project.
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// Make the Toolbar visible with this line OR check the "Shows Toolbar" option of your Navigation Controller in the Storyboard
[self.navigationController setToolbarHidden:NO];
CGFloat customToolbarHeight = 60;
[self.navigationController.toolbar setFrame:CGRectMake(0, self.view.frame.size.height - customToolbarHeight, self.view.frame.size.width, customToolbarHeight)];
}

Resources