Swift button frame height issue (viewDidLayoutSubviews) - ios

I've got some square buttons that I'd like to add rounded corners to that are proportional to the button's height. In past versions of my app, I had implemented this feature without issues using viewDidLayoutSubviews(). For some reason, after pushing a new version of my app with other features I had tweaked, this section of code no longer functions as expected. Here is the code:
override func viewDidLayoutSubviews() {
for button in buttons {
button!.layer.shadowColor = UIColor.black.cgColor
button!.layer.shadowOffset = CGSize(width: 0, height: 1.0)
button!.layer.shadowOpacity = 0.4
button!.layer.shadowRadius = button!.frame.height / 40
button!.layer.cornerRadius = button!.frame.height / 10
}
Again, this block of code used to work just fine but for some reason it no longer works. What I am experiencing is much larger relative radii on smaller buttons (iPhone SE) compared to bigger buttons (iPads).
To troubleshoot, in viewDidLayoutSubviews(), I'm printing the button!.frame.height and I'm noticing that no matter what device I use the frame height is 395.5, which I believe is the correct size only on the 12.9" iPad. Therefore, the buttons look correct on the 12.9" iPad but the radii end up being too large on all of the smaller devices.
Any idea what's going on here? Why is it that they're all returning the same frame height even though they're visually very different sizes on the different devices?

I copy and pasted the above code into the viewWillAppear() method and
the problem was resolved. I then deleted the code from
viewWillAppear(), leaving me with my original code during posting of
question, and it is continuing to run as expected (working). What
could possibly be the cause of this intermittent behavior
The reason when you initialized the buttons in viewWillAppear and remove them but it still work because your button's frame did not change in the viewDidLayoutSubview method. And the viewDidLayoutSubview is invoked only controller's view is updated, rotated, or changed, which in your case it does not.
If you try to rotate your device you will see your parent view's frame changed.
For more information about view hierarchy. See this article

Try like this:-
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for button in buttons {
button!.layer.shadowColor = UIColor.black.cgColor
button!.layer.shadowOffset = CGSize(width: 0, height: 1.0)
button!.layer.shadowOpacity = 0.4
button!.layer.shadowRadius = button!.frame.height / 40
button!.layer.cornerRadius = button!.frame.height / 10
}

Related

Swift circular corners doesn't work properly on different screen sizes

I'm trying to achieve smooth round corners on imageviews. Although it's working well on non-plus devices, i can not reach my goal at plus screen devices. Incidentally, i recognize, it's not even work well on the devices that font size enlarged. I applied following code that can be found every topic.
mergedImage.image = lastImage
mergedImage.layer.masksToBounds = false
mergedImage.layer.cornerRadius = mergedImage.frame.size.height / 2
mergedImage.clipsToBounds = true
And it's results like pictures below.
It looks like the black shape changes size. This can happen when using AutoLayout for example. If that is the case, you need to calculate the corner radius each time the frame changes.
I think the best way to do this is by subclassing UIView to create a "black shape view", and then overriding its layoutSubviews method:
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height / 2
}
If you don't have a subclass, you can for example do this in UIViewController.viewDidLayoutSubviews:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
mergedImage.layer.cornerRadius = bounds.height / 2
}

Gradient layer not in the right place

I have the following code as follows:
playView.layer.cornerRadius = 16
let gradient1 = CAGradientLayer()
gradient1.frame = playView.frame
gradient1.cornerRadius = 16
if #available(iOS 10.0, *) {
// set P3 colour
} else {
// set sRGB colour
}
gradient1.startPoint = CGPoint(x: 0, y: 0)
gradient1.endPoint = CGPoint(x: 1, y: 1)
playView.layer.insertSublayer(gradient1, at: 0)
On the 3rd line of the block of code above, I set the frame of the gradient equal to the frame I want it to fill.
When I run the app on different devices, the gradient layer will only fill the correct area if the device the app is being run on is the one selected in the Interface Builder.
I currently have the code in viewDidLoad(), and so the issue can be solved by moving the code to viewDidAppear(), but then when the app is loaded, there will be a slight delay before the gradient appears, not giving a smooth look and feel.
Is there another method I can put the code in, so that the gradient shows in the correct place, whilst at the same time being there as soon as the user sees the screen? Or alternatively, a way to make the gradient fill the view, whilst still keeping the code in viewDidLoad()?
EDIT: viewWillAppear() does not work, nor does viewWillLayoutSubviews(). Surely there must be away to solve this?
You can put inside this block:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
}
viewDidAppear() works. This screen is the root controller - I don't know if that makes a difference or not, but there is no visible delay on applying the gradient backgrounds.
I would be interested if anyone could explain this? I have a bar chart in another part of the app and in viewDidAppear() there is code to complete the bar chart, however there is a delay in it being filled in.
Change the layer.frame property inside the viewDidLayoutSubviews
method. This is to make sure that the subview (playView) has already a proper frame.

iOS 11 breaks row selection

I've recently tested my app in iOS 11 and for some reason I'm not able to select one of the first 12 rows in a dynamically populated table view. The didSelectRow isn't even triggered for these rows. The other rows work fine, but even when scrolling down and back up (the cells should have been re-used again by then) the first 12 rows don't work.
Even on a static table view all cells that appear on screen when switching to that view controller will not respond, neither will controls inside them, even when they are in different sections. Cells that are out of screen initially again work fine.
I'll be trying to test this in an app with boilerplate code, but is this a known bug? I couldn't find anything online about it.
I've tested this after updating the devices to iOS 11, then again from Xcode 9 beta 6 without changes to the code, and again after migrating to Swift 4. Same behaviour inside the simulator. Up to iOS 10 everything is fine, only with iOS 11 the problem occurs.
This will break my app for users in two weeks, I need to fix it, so any help or advice very much appreciated!
UPDATE: As Paulw11 suggested, there is indeed another view blocking the rows. This was notable as row 12 could only be selected in the lower part of the cell, but not in the upper part.
The cause for this issue is the following code:
extension UIViewController {
func setBackgroundImage(forTableView tableView: UITableView) {
let bgImage = UIImage(named: "Background Image.png")
let bgImageView = UIImageView(image: bgImage)
tableView.backgroundView = bgImageView
let rect = bgImageView.bounds
let effect = UIBlurEffect(style: UIBlurEffectStyle.dark)
let blurView = UIVisualEffectView(effect: effect)
let height: CGFloat
switch screenSize.height {
case 480, 568: height = 455
case 736: height = 623
default: height = 554
}
blurView.frame = CGRect(x: 0, y: 0, width: rect.width, height: height)
let container = UIView(frame: rect)
bgImageView.addSubview(blurView)
let bgOverlay = UIImage(named: "Background Overlay.png")
let bgOverlayImageView = UIImageView(image: bgOverlay)
bgOverlayImageView.alpha = 0.15
bgImageView.addSubview(bgOverlayImageView)
self.view.insertSubview(container, at: 1)
}
}
Somehow since iOS 11 this background image seems to be rendered in front of the cells. Not inserting the container view into the table view's view will solve the issue. I've tried setting the zPosition of the container's layer but it does not help. How can I move the background image behind the cells again.
It's weird that this behaviour would change from iOS 10 to 11...
UPDATE 2: Inserting the container at index -1 fixes the issue:
self.view.insertSubview(container, at: -1)
I don't get why this works, though, shouldn't this index be out of range?
UPDATE 3: As Paulw11 pointed out below, the container is completely useless, it was left over from testing and removing it fixes the issue.
The container view seems to be appearing in front of the other views and preventing touches from making it through to the table view.
As an aside, I would see if you can refactor this to use constraints; It always worries me when you see hard-coded screen sizes, as that may break when new devices are released.

layoutIfNeeded sequence acts differently on iPad versus iPhone; how to fix?

Fire up Xcode and for clarity build only to say 9.3, universal app. So, compare 9.3 iPads with 9.3 iPhones. Build to both simulator and devices - issue exhibits on both.
The app rotates in all four directions.
Have a typical situation where you do something like this...
#IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!
var heightFraction:CGFloat = 0.6
{
didSet
{
if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
if ( heightFraction < minHeight ) { heightFraction = minHeight }
let h = view.bounds.size.height
spaceshipHeightPerScreen.constant = h * heightFraction
self.view.layoutIfNeeded() // holy! read on....
}
}
Notice the layoutIfNeeded() after the change to the constraint.
Continuing the typical example, you will have something like
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
heightFraction = (heightFraction)
// use "autolayout power" for perfection every pass.
// now that basic height/position is set,
save/load reactive positions...
position detail stuff...
}
Check it out ... I was doing this all day and only happened to use iPhones.
Interestingly you do not need the layoutIfNeeded call:
#IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!
var heightFraction:CGFloat = 0.6
{
didSet
{
if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
if ( heightFraction < minHeight ) { heightFraction = minHeight }
let h = view.bounds.size.height
spaceshipHeightPerScreen.constant = h * heightFraction
}
}
Works fine.
However at the end of the day I put it on some iPads and .... everything broke!
Whenever you rotate landscape/portrait, problems.
After a head scratch, I realized that incredibly you do need the layoutIfNeeded call, on iPad. That's on the identical OS.
Indeed the behavior exhibits regardless of OS version. And it exhibits for ALL iPhones / ALL iPads.
#IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!
var heightFraction:CGFloat = 0.6
{
didSet
{
if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
if ( heightFraction < minHeight ) { heightFraction = minHeight }
let h = view.bounds.size.height
spaceshipHeightPerScreen.constant = h * heightFraction
self.view.layoutIfNeeded() //MUST HAVE, IN IPAD CASE!!!!!!
}
}
To me it is incredibly troubling that they would work differently.
What I'm wondering is, is there perhaps a setting somewhere to make them work the same? Could it be my fault somehow?
Are there any other know differences between the two - or indeed is it "known" that there are a few bugs like this?
I can't think of anything odd or unusual I did anywhere, except the whole app has override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { return .All } in the first view as is normal if you want to turn the device upside down; I doubt it's related. Other than that it's a very "clean" fresh app.
It gave me a glitch-in-the-matrix feeling - it was terrifying.
What could cause this?
Per RobM's question, the SimulatedMetrics settings (Attributes tab) on the initial ViewController are...
General scheme of the app: the first scene "General" is full-screen, the size of the device. There's a container to "Live" which is the same size (using "Trailing" etc/ constraints as zero all round). In Live, there's a container view "Quad" which indeed is also fully sized to "Live," so it's also fullscreen. Quad:UIViewController exhibits the issue I describe. Quad contains various objects (images, custom controls etc) which sit around on the view. When the app launches, all is fine.
On rotation of the device (or similar): just after the change to the constraint (I don't know if that's relevant): the layoutIfNeeded call IS needed for iPads (all iPads), but is NOT needed for iPhones (all iPhones). The behavior is identical in the simulator and on devices.
Another example
I found another astounding example of this.
In a UICollectionView, custom cells (just simple static sized cells). If you happen to change a constraint (imagine say resizing an icon or product shot within the cell).
On iPad you do have to be sure to readjust in layoutIfNeeded or it will not work on the first appearance of the cell.
Whereas on iPhone it definitely behaves differently: it will "do that for you", before the first appearance of the cell, if you happen to omit it.
I tested that on every iPad and every iPhone. (Also, the unusual behavior exhibits exactly on devices or simulators: simulator makes no difference.)
I'm not able to reproduce what you're seeing; it would be nice to see a complete example. In my mockup I configured a view controller with a view having a single subview, with a constraint to control the subview height. I altered the subview height constraint in viewDidLayout based on the view size. The behavior was identical for both iPhone and iPad, and worked sans calling layoutIfNeeded on any view.
That said, I think you're changing subview constraints once the view has completed its layout - yes? I think the better way to do that would be to layout your subviews ahead of that, via viewWillTransitionToSize:withTransitionCoordinator:.
func viewWillTransitionToSize(_ size: CGSize,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
This way auto layout for the view hierarchy can complete in a single pass. This method is only called when the view is changing size, so it won't be called when the view is first loaded; you'll have to set up your initial constraints somewhere else - since they're dependent on view size perhaps you can use viewWillAppear.
Alternatively, (and possibly more correctly), subclass your view controller's view and override updateConstraints. This is the most appropriate place for changing your constraint constants.
Finally, in your property setter, you shouldn't ever call view.layoutIfNeeded(). If anything, you can set view.setNeedsLayout() so that layout happens in the next runloop iteration, and picks up ALL changes that may need to be represented.
The default simulated metrics size is “inferred”, which (if the scene isn't the target of a segue or relationship) gives you a 600x600 view, which doesn't correspond to the screen size of any iOS device. You changed the simulated metrics size at some point to “iPhone 5.5-inch”, probably to match the size of your main test device.
When a view is loaded from a storyboard (or xib), it's loaded at the size it had in the storyboard. It may then be resized by its container (either the UIWindow if it's the root view of the app, or by its superview if it's the root view of a contained view controller).
In your case, it sounds like your main test device's screen has the same size as the root view in your storyboard, so the test device doesn't have to run as much layout as you might expect.
When you use a test device whose screen size differs from your root view's size in the storyboard, the test device has to do more layout.
I didn't try to reproduce your problem, so I'm not claiming that this is a full explanation of what you're seeing. There may well be an iOS bug involved. Nevertheless, this should explain why your app behaves differently on different devices. I believe this is also why Apple chose the default inferred size of 600x600: since no device screen is that size, all devices will have to do the same amount of layout.

issues with UIImageView.layer.cornerRadius to create rounded images on different pixel densities ios

I'm simply trying to create a perfectly round image. Here's my swift code:
myImage.layer.cornerRadius = myImage.frame.size.width/2
myImage.layer.masksToBounds = true
This works on a 4s, but is not quite round on a 5s, and appears as a rounded rectangle on a iphone 6.
I'm assuming this has to do with frame.size.width returning values in pixels not points or something like that, but I've been unable to solve this problem.
If you're putting that code in viewDidLoad, try moving it to viewDidLayoutSubviews.
I'm guessing it's an auto layout issue -- although you've used the corner radius property appropriately and are in fact making the image frame circular, after auto-layout, the corner radius stays the same, but the image stretches so that it's no longer circular.
If your code is in viewDidLoad in ViewController, try moving it to viewDidLayoutSubviews.
If your rounded imageView is in tableViewCell, try moving it to draw.
override func draw(_ rect: CGRect) {
avatarView.layer.cornerRadius = avatarView.frame.size.width / 2
avatarView.layer.masksToBounds = true
avatarView.clipsToBounds = true
avatarView.contentMode = .scaleAspectFill
}
The problem is that if you change the cornerRadius of Any view, and expect it to look like a circle, the view has to be a square.
Now, because of different devices and different device size, the size of your image view might change and it may be a rectangle.
For e.g.
If you view is a 50 * 50
myImage.layer.cornerRadius = myImage.frame.size.width/2
This would add corner radios of 25 on both sides to make it a circle.
But because of auto layout of device change, your view might be a 50 * 80
Corner radius would add a 25, but as the height is 80, it will add 25 on both sides, and the remaining 30 won't be a curve and look straight.
What you can do is try viewing the screen in various orientations in the storyboard and change auto layout Constraints (Or structs and springs) to ensure that the image view is a square in all the devices
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myImage.layer.cornerRadius = myImage.frame.size.width/2
myImage.layer.masksToBounds = true
}
This should work:
myImage.layer.cornerRadius = myImage.**bounds**.size.width/2
myImage.layer.masksToBounds = true

Resources