swift - frame issue in UINavigationController class when rotating - swift

I've used a custom UINavigationController class to put a colour gradient across the navbar. The custom navclass determines the frame size of the navbar, then puts the gradient inside. Problem is, when I rotate from portrait to landscape, the gradient only fills the portrait portion of the landscape bar. I've only assigned the custom UINavigationController class to the navigation view in the storyboard.
So I'm guessing I somehow need to call a refresh for the frame size in the custom navclass when a rotation is done, but I'm not sure how or where?
Here's the relevant code snippet, from the override func viewDidLoad() of the custom navclass.
let gradientlayer = CAGradientLayer()
gradientLayer.frame = self.navigationBar.bounds
self.navigationBar.layer.inserSublayer(gradientLayer, atIndex: 1)
I tried putting it inside viewWillAppear(), but that didn't work. Any help?

viewDidLoad() and viewWillAppear() don't get called on rotation, so your code won't update the frame there.
You should add gradientLayer.frame = self.navigationBar.bounds to viewWillLayoutSubviews() but leave the other code in viewDidLoad() so you don't wind up with multiple gradient layers.

Related

How to make a nav bar transparent before scrolling (iOS 14)

I'd like to implement a nav style like what is found in the "Add Reminder" view controller from Apple's Reminders (iOS 14). I've tried hooking into the scrollview delegate methods but I'm not sure how to change the alpha of the default nav bar background/shadow image.
I've tried changing the nav bar style on scroll and, while that works, it doesn't fade in/out like in the example. That makes me think the answer lies manipulating the alpha value. Thanks in advance!
I've found a (hacky) solution that works in iOS 14 (untested in other versions). It makes an assumption about the private view structure of UINavigationBar, so there's no guarantee that it will work in future iOS versions, but it's unlikely to crash - the worst that should happen is that the bar will fail to hide, or only partially hide.
Assuming that you are placing the code inside a UIViewController subclass that it acting as the delegate for a UITableView, UICollectionView or UIScrollView, the following should work:
override func viewDidLoad() {
super.viewDidLoad()
// this hides the bar initially
self.navigationController?.navigationBar.subviews.first?.alpha = 0
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let navigationController = self.navigationController else { return }
let navBarHeight = navigationController.navigationBar.frame.height
let threshold: CGFloat = 20 // distance from bar where fade-in begins
let alpha = (scrollView.contentOffset.y + navBarHeight + threshold) / threshold
navigationController.navigationBar.subviews.first?.alpha = alpha
}
The magic threshold value is a little hard to explain, but it's basically the distance from the bar at which the fade in will start. A value of 20 means the bar starts to fade in when the scrollView content is 20 points away. A value of 0 would mean the bar snaps straight from fully transparent to fully opaque the moment the scrollView content touches it.

How to make a CAShapeLayer have a blur effect?

I have this CAShapeLayer that I want to have a blur effect. How would I be able to do that?
EDIT
I tried it this way but the blur view doesn't show. Anyone know why? Thanks!
func createLayer(in rect: CGRect) -> CAShapeLayer{
let effectView = UIVisualEffectView(effect:UIBlurEffect(style: .regular))
effectView.frame = rect
let view = UIView(frame: rect)
view.addSubview(effectView)
let mask = CAShapeLayer()
mask.frame = rect
mask.cornerRadius = 10
effectView.layer.mask = mask
maskLayer.append(mask)
layer.insertSublayer(mask, at: 1)
return mask
}
The short answer: You don't. You can add a visual effects view (UIVisualEffectView) of type blur (a UIBlurEffect) on top of the shape layer's view, or you could write code that takes the contents of the shape layer, applies a Core Image filter to it, and copies the output to another layer.
Using a UIVisualEffectView is a lot easier than working with Core Image filters, but a visual effect view operates on a view, not a layer. You'll need to make the shaper layer be part of the layer's layer hierarchy in order to use it.
Edit:
Your code has errors and doesn't really make sense. Your method createLayer (which I guess is a view controller instance method?) creates and returns a shape layer.
That method creates a throw-away UIView that is never added to the view hierarchy, nor passed back to the caller. That view will get deallocated as soon as your method returns.
Next you create a visual effects view and make that a subview of the throw-away view. Since the only place that view is attached is to the throw-away view, it will also get deallocated as soon as your method returns.
Next you create a shape layer and set it up as the mask of some other layer maskLayer, which you don't explain. Nor do you install a path into the shape layer.
If you have a view called shapeView, of class ShapeView, and you want to attach a visual effects view to it, you could use code like this:
class ViewController: UIViewController {
#IBOutlet weak var shapeView: ShapeView!
var blurView: UIVisualEffectView?
override func viewDidLoad() {
super.viewDidLoad()
blurView = UIVisualEffectView(effect:UIBlurEffect(style: .regular))
blurView?.frame = shapeView.frame
//Add the blur view on top of the shape view
view.addSubview(blurView!)
}
override func viewDidLayoutSubviews() {
//Update the blurView's frame if needed
blurView?.frame = shapeView.frame
}
}

UINavigationBar bleeding its background when PageSheet is animating

My application has a UIViewController where uses the new .pageSheet modal presentation style introduced on iOS 13.
This UIViewController has a UINavigationBar on the top and it's pinned, by constraints, at the top, leading and trailing.
I noticed that the background from this view bleeds in white while the UIViewController is animating. Take a look on this recorded GIF from a real device:
Is there anything I can do to solve this? The UIViewController and UINavigationBar were created programatically.
Maybe doing this can solve it?
override func layoutSubviews() {
super.layoutSubviews()
var originalFrame = frame
frame = originalFrame
}
I am using Swift 5.1 and Xcode 11.3. The iPhone is running iOS 13.1.3.
Probably you set the view's layer to rasterize, this is making with the .pageSheet animation not draw it correctly.
The solution, remove the code below:
navigationBar.layer.shouldRasterize = true
navigationBar.layer.rasterizationScale = UIScreen.main.scale

UIPopoverPresentationController without overlay

I am implementing a popover view using UIPopoverPresentationController.
The trouble with this, is that by default, I have a shadow with a large radius for the controller.
I want to disable this - the overlay.
I have tried:
to customise the layout shadow (using a UIPopoverBackgroundView):
layer.shadowColor = UIColor.white.withAlphaComponent(0.01).cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 0
In view debugging - I can see behind the popup 4 image views with gray gradient background:
I am sure this is a default behaviour, of showing an overlay behind a popover.
How do disable this?
I found this and this. But those didn't helped.
If you take a closer look at the views hierarchy you will notice that the shadow layer _UIMirrorNinePatchView is a sublayer of UITransitionView same as UIPopoverView - both are on the same level.
views hierarchy picture
In this case you can try to hide this sublayer like so:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let shadowLayer = UIApplication.shared.windows.first?.layer.sublayers?[1].sublayers?[1] {
shadowLayer.isHidden = true
}
}
Make sure to hide it in viewDidLayoutSubviews to avoid exceptions related to missing sublayers or sublayer flickering.

Where to get frame size of custom UIView in its subclass

When does the correct frame size of the UIView is calculated when I add my custom UIView by using storyboard?
I can't get correct frame size in UIView's init(frame: CGRect) or init(coder aDecoder: NSCoder) or awakeFromNib(). I get the correct size in override func layoutSubviews() but this time the view is not added to the view controller's view.
Edit:
I want to do this in the UIView subclass because I add a CAGradientLayer layer (which has same size with my view) to my custom UIView. There must be a better way than setting up the view in UIViewControllers viewDidLayoutSubviews method.
There are two vertical constraints (trailing and leading space to superview) that adjust the width of my custom view. But the width is 600 in the mentioned methods, not the screen width.
func viewWillAppear(_ animated: Bool)
This is called on the viewController after the view hierarchy is in place and the geometry is set up.
It is usually the best place to arrange geometry-dependent viewController code, and is called just before transition animations are run.
It is followed by a call to viewDidAppear which is called after transition animations have completed.
update
as you want to set this directly in a custom view (rather than in the viewController) the appropriate UIView function to override is layoutSubviews. Be sure to call super.layoutSubviews() in the override.
override func layoutSubviews() {
super.layoutSubviews()
//self.frame will be correct here
}
This will be called after the viewController's viewWillAppear and before viewDidAppear
update 2: collectionView cells
Collection view cells get their frame data from the collectionview's layout. When a cell needs to know it's geometry, you can override applyLayoutAttributes
func applyLayoutAttributes(_ layoutAttributes: UICollectionViewLayoutAttributes!)
One of the default layoutAttributes is the frame property. See also my comment below on how to extract the frame directly from collectionView.layout.
Since viewDidLayoutSubviews() is getting called multiple times, I'd recommend using the viewDidAppear() delegate.
viewDidLoad() is called before any frame was set.
viewDidLayoutSubviews() is called during frame sizing and will be called multiple times with different frame sizes (usually frameZero before a frame is calculated).
viewDidAppear() All frames are set.
You should have an IBOutlet to your UIView and in the viewDidLayoutSubviews() method, get the size by yourViewOutlet.frame.size.