redraw a custom uiview when changing a device orientation - ios

If I draw my chart inside - (void)drawRect:(CGRect)rect is just enough to set [_chartView setContentMode:UIViewContentModeRedraw] and this method will be called when device changes it's orienatation and it's possible to calculate f.e. new center point for my chart.
If I create a view like a subview using - (id)initWithFrame:(CGRect)frame and then add it in view controller like [self.view addSubview:chartView];. How in this case I can handle rotation to redraw my chart?

While a preferred solution requires zero lines of code, if you must trigger a redraw, do so in setNeedsDisplay, which in turn invokes drawRect.
No need to listen to notifications nor refactor the code.
Swift
override func layoutSubviews() {
super.layoutSubviews()
self.setNeedsDisplay()
}
Objective-C
- (void)layoutSubviews {
[super layoutSubviews];
[self setNeedsDisplay];
}
Note:
layoutSubviews is a UIView method, not a UIViewController method.

To make your chart rendered correctly when device orientation changes you need to update chart's layout, here is the code that you should add to your view controller:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
_chartView.frame = self.view.bounds;
[_chartView strokeChart];
}

Zero Lines of Code
Use .redraw
Programmatically invoking myView.contentMode = .redraw when creating the custom view should suffice. It is a single flag in IB and, as such, the 0 lines of code prefered way. See Stack Overflow How to trigger drawRect on UIView subclass.

Go here to learn how to receive notifications for when the device orientation changes. When the orientation does change, just call [chartView setNeedsDisplay]; to make drawRect: get called so you can update your view. Hope this helps!

The code you'll add to your view controller:
- (void)updateViewConstraints
{
[super updateViewConstraints];
[_chartView setNeedsDisplay];
}

Unfortunately some answers suggest to override controller methods, but I've some custom UITableViewCells with a shadow around and rotating the device stretches the cells but doesn't redraw the shadow. So I don't want to put my code within a controller to (re)draw a subview.
The solution for me is to listen to a UIDeviceOrientationDidChange notification within my custom UITableViewCell and then call setNeedsDisplay() as suggested.
Swift 4.0 Code example in one of my custom UITableViewCells
override func awakeFromNib() {
super.awakeFromNib()
NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationDidChangeNotification), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}
#objc func deviceOrientationDidChangeNotification(_ notification: Any) {
Logger.info("Orientation changed: \(notification)")
setNeedsLayout()
}
BTW: Logger is a typealias to SwiftyBeaver.
Thanks to #Aderis pointing me to the right direction.

I tried "setNeedsDisplay" in a dozen ways and it didn't work for adjusting the shadow of a view I was animating to a new position.
I did solve my problem with this though. If your shadow doesn't seem to want to cooperate/update, you could try just setting the shadow to nil, then setting it again:
UIView.animateKeyframes(withDuration: 0.2 /*Total*/, delay: 0.0, options: UIViewKeyframeAnimationOptions(), animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 10/10, animations:{
// Other things to animate
//Set shadow to nil
self.viewWithShadow.layer.shadowPath = nil
})
}, completion: { finished in
if (!finished) { return }
// When the view is moved, set the shadow again
self.viewWithShadow.layer.shadowPath = UIBezierPath(rect: self.descTextView.bounds).cgPath
})
If you don't even have a shadow yet and need that code, here's that:
func addShadowTo (view: UIView) {
view.layer.masksToBounds = false
view.layer.shadowColor = UIColor.gray.cgColor
view.layer.shadowOffset = CGSize( width: 1.0, height: 1.0)
view.layer.shadowOpacity = 0.5
view.layer.shadowRadius = 6.0
view.layer.shadowPath = UIBezierPath(rect: view.bounds).cgPath
view.layer.shouldRasterize = true
}

Thanks Keenle, for the above ObjC solution. I was messing around with creating programmed constraints all morning, killing my brain, not understanding why they would not recompile/realign the view. I guess because I had created the UIView programmatically using CGRect...this was taking precedence.
However, here was my solution in swift:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
myUIView.center = CGPoint(x: (UIScreen.mainScreen().bounds.width / 2), y: (UIScreen.mainScreen().bounds.height / 2))
}
So relieved! :)

Related

Swift - Restarting an animation after clicking a button

I was hoping someone could help.
I have two rectangles which move across the screen. The first constraints for the (rectangles) are activated in my setupViews function which is called in my viewDidLoad.
In my viewDidLayoutSubviews I have an animation function which is called moving the two rectangles.
When I click a button however I would like the rectangles to return to their initial position and begin the animation from the beginning.
How could I successfully do this?
I initially did not have my animation being called in my viewDidLayoutSubviews, just in another function (which was being called in my viewDidAppear(_ animated: Bool)), however, upon reading it seems as if calling the animation in the viewDidLayoutSubviews might be the best course of action.
I can successfully stop the animation, but getting it to restart again from its initial starting point is proving to be troublesome.
I have tried creating the constraints again in the buttons sender function. But this doesn’t seem to do anything, as I’m not sure you can change an elements constraints without reloading the viewDidLoad again.
Below is my code………
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
func setupViews() {
self.view.addSubview(backgroundImageView)
self.backgroundImageView.addSubview(contentView)
self.backgroundImageView.addSubview(slideDownTintedWhiteView)
self.backgroundImageView.addSubview(restartSlidingAnimation)
self.contentView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
self.slideDownTintedWhiteView.snp.makeConstraints { (make) in
make.top.equalToSuperview()
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
make.bottom.equalTo(backgroundImageView.snp.top)
}
self.restartSlidingAnimation.snp.makeConstraints { (make) in
make.centerX.equalToSuperview()
make..centerY.equalToSuperview()
make.width.equalTo(10)
make.height.equalTo(10)
}
self.restartSlidingAnimation.addTarget(self, action: #selector(restartAnimationFunction(_:)), for: .touchUpInside)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
UIView.animate(withDuration: 10, delay: 0, animations: {
//Red View:
self.contentView.frame = CGRect(x: self.contentView.frame.origin.x, y: self.contentView.frame.height, width: self.contentView.frame.width, height: self.contentView.frame.height)
//White View:
self.slideDownTintedWhiteView.frame = CGRect(x: self.contentView.frame.origin.x, y: 0, width: self.contentView.frame.width, height: self.contentView.frame.height)
}, completion: nil)
}
restartAnimationFunction() {
// Here is obviously where I need to somehow re-establish the constraints of the rectangles back to their original location or somehow start the animation from the beginning.
}
func stopContentViewAnimation() {
// This is where I am able to stop the animation (called from another button)
pausedTime = self.contentView.layer.convertTime(CACurrentMediaTime(), from: nil)
self.contentView.layer.speed = 0.0
self.contentView.layer.timeOffset = pausedTime
pausedTime = self.slideDownTintedWhiteView.layer.convertTime(CACurrentMediaTime(), from: nil)
self.slideDownTintedWhiteView.layer.speed = 0.0
self.slideDownTintedWhiteView.layer.timeOffset = pausedTime
}
Thanks for any help you can provide.
1- First the animation should not be inside viewDidLayoutSubviews as it's being called many times , best place for it is viewDidAppear
2- You don't set any constraints to backgroundImageView
3- To animate a child remove it's constraints from itself and from it's parent
self.backgroundImageView.removeConstraints(self.backgroundImageView.constraints)
self.contentView.removeConstraints(self.contentView.constraints)
self.slideDownTintedWhiteView.removeConstraints(self.slideDownTintedWhiteView.constraints)
self.restartSlidingAnimation.removeConstraints(self.restartSlidingAnimation.constraints)
4- Re construct the other case constraints and when ready to animate put
self.view.layoutIfNeeded()
inside the animation block
OFF course I don't recommend removing all constraints and constructing them again , but you can have a reference to the constraints you want to change and alter their constants , but for a beginner this may be good to go

How to detect when UIView size is changed in swift ios xcode? [duplicate]

This question already has answers here:
Is there a UIView resize event?
(7 answers)
Closed 7 months ago.
I am trying some stuffs out with CATiledLayer inside UIScrollView.
Somehow, the size of UIView inside the UIScrollView gets changed to a large number. I need to find out exactly what is causing this resize.
Is there a way to detect when the size of UIView(either frame, bounds) or the contentSize of UIScrollView is resized?
I tried
override var frame: CGRect {
didSet {
println("frame changed");
}
}
inside UIView subclass,
but it is only called once when the app starts, although the size of UIView is resized afterwards.
There's an answer here:
https://stackoverflow.com/a/27590915/5160929
Just paste this outside of a method body:
override var bounds: CGRect {
didSet {
// Do stuff here
}
}
viewWillLayoutSubviews() and viewDidLayoutSubviews() will be called whenever the bounds change. In the view controller.
You can also use KVO:
You can set a KVO like this, where view is the view you want to observe frame changes for:
self.addObserver(view, forKeyPath: "center", options: NSKeyValueObservingOptions.New, context: nil)
And you can get the changes with this notification:
override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: NSDictionary!, context: CMutableVoidPointer) {
}
The observeValueForKeyPath will be called whenever the frame of the view you are observing changes.
Also remember to remove the observer when your view is about to be deallocated:
view.removeObserver(self, forKeyPath:"center")
You can create a custom class, and use a closure to get the updated rect comfortably. Especially handy when dealing with classes (like CAGradientLayer which want you to give them a CGRect):
GView.swift:
import Foundation
import UIKit
class GView: UIView {
var onFrameUpdated: ((_ bounds: CGRect) -> Void)?
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
self.onFrameUpdated?(self.bounds)
}
}
Example Usage:
let headerView = GView()
let gradientLayer = CAGradientLayer()
headerView.layer.insertSublayer(gradientLayer, at: 0)
gradientLayer.colors = [
UIColor.mainColorDark.cgColor,
UIColor.mainColor.cgColor,
]
gradientLayer.locations = [
0.0,
1.0,
]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
headerView.onFrameUpdated = { _ in // here you have access to `bounds` and `frame` with proper values
gradientLayer.frame = headerView.bounds
}
If you are not adding your views through code, you can set the Custom Class property in storyboard to GView.
Please note that the name GView was chosen as a company measure and probably choosing something like FrameObserverView would be better.
This is a simple and not-too-hacky solution: You remember the last size of your view, compare it to the new size in an overridden layoutSubviews method, and then do something when you determine that the size has changed.
/// Create this as a private property in your UIView subclass
private var lastSize: CGSize = .zero
open override func layoutSubviews() {
// First call super to let the system update the layout
super.layoutSubviews()
// Check if:
// 1. The view is part of the view hierarchy
// 2. Our lastSize var doesn't still have its initial value
// 3. The new size is different from the last size
if self.window != nil, lastSize != .zero, frame.size != lastSize {
// React to the size change
}
lastSize = frame.size
}
Note that you don't have to include the self.window != nil check, but I assume that in most cases you are only interested in being informed of size changes for views that are part of the view hierarchy.
Note also that you can remove the lastSize != .zero check if you want to be informed about the very first size change when the view is initially displayed. Often we are not interested in that event, but only in subsequent size changes due to device rotation or a trait collection change.
Enjoy!
The answers are correct, although for my case the constraints I setup in storyboard caused the UIView size to change without calling back any detecting functions.
For UIViews, as easy as:
override func layoutSubviews() {
super.layoutSubviews()
setupYourNewLayoutHereMate()
}
You can use the FrameObserver Pod.
It is not using KVO or Method Swizzling so won't be breaking your code if the underlying implementation of UIKit ever changes.
whateverUIViewSubclass.addFrameObserver { frame, bounds in // get updates when the size of view changes
print("frame", frame, "bounds", bounds)
}
You can call it on a UIView instance or any of its subclasses, like UILabel, UIButton, UIStackView, etc.
STEP 1:viewWillLayoutSubviews
Called to notify the view controller that its view is about to
layout its subviews
When a view's bounds change, the view adjusts the position of its
subviews. Your view controller can override this method to make
changes before the view lays out its subviews. The default
implementation of this method does nothing.
STEP 2:viewDidLayoutSubviews
Called to notify the view controller that its view has just laid out
its subviews.
When the bounds change for a view controller's view, the view
adjusts the positions of its subviews and then the system calls this
method. However, this method being called does not indicate that the
individual layouts of the view's subviews have been adjusted. Each
subview is responsible for adjusting its own layout.
Your view controller can override this method to make changes after
the view lays out its subviews. The default implementation of this
method does nothing.
Above these methods are called whenever bounds of UIView is changed

Animate `backgroundColor` of a `UIView` that implements `drawRect`

I have a custom UIView and I would like to animate its backgroundColor property. This is an animatable property of a UIView.
This is the code:
class ETTimerUIView: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// other methods
func flashBg() {
UIView.animateWithDuration( 1.0, animations: {
self.backgroundColor = UIColor.colorYellow()
})
}
override func drawRect() {
// Something related to a timer I'm rendering
}
This code causes causes the animation to skip and the color to change immediately:
self.backgroundColor = UIColor.colorYellow() // Changes immediately to yellow
If I animate alpha, this animates from 1 to 0 over one second as expected:
self.alpha = 0 // animates
How do I animate a background color change in this situation?
Implementing drawRect blocks backgroundColor animation, but no answer is provided yet.
Maybe this is why you can't combine drawRect and animateWithDuration, but I don't understand it much.
I guess I need to make a separate view--should this go in the storyboard in the same view controller? programmatically created?
Sorry, I'm really new to iOS and Swift.
It is indeed not working when I try it, I had a related question where putting the layoutIfNeeded() method inside the animation worked and made the view smoothly animating (move button towards target using constraints, no reaction?). But in this case, with the backgroundColor, it does not work. If someone knows the answer I will be interested to know.
But if you need a solution right now, you could create a UIView (programmatically or via the storyboard) that is used only as a container. Then you add 2 views inside : one on top, and one below, with the same frame as the container. And you only change the alpha of the top view, which let the user see the view behind :
class MyView : UIView {
var top : UIView!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.blueColor()
top = UIView(frame: CGRectMake(0,0, self.frame.width, self.frame.height))
top.backgroundColor = UIColor.yellowColor()
self.addSubview(top)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let sub = UIView(frame: CGRectMake(0,0, self.frame.width, self.frame.height))
sub.backgroundColor = UIColor.purpleColor()
self.sendSubviewToBack(sub)
UIView.animateWithDuration(1, animations: { () -> Void in
self.top.alpha = 0
}) { (success) -> Void in
println("anim finished")
}
}
}
The answer is that you cannot animate backgroundColor of a view that implements drawRect. I do not see docs for this anywhere (please comment if you know of one).
You can't animate it with animateWithDuration, nor with Core Animation.
This thread has the best explanation I've found yet:
When you implement -drawRect:, the background color of your view is then drawn into the associated CALayer, rather than just being set on the CALayer as a style property... thus prevents you from getting a contents crossfade
The solution, as #Paul points out, is to add another view above, behind, or wherever, and animate that. This animates just fine.
Would love a good understanding of why it is this way and why it silently swallows the animation instead of hollering.
Not sure if this will work for you, but to animate the background color of a UIView I add this to a UIView extension:
extension UIView {
/// Pulsates the color of the view background to white.
///
/// Set the number of times the animation should repeat, or pass
/// in `Float.greatestFiniteMagnitude` to pulsate endlessly.
/// For endless animations, you need to manually remove the animation.
///
/// - Parameter count: The number of times to repeat the animation.
///
func pulsate(withRepeatCount count: Float = 1) {
let pulseAnimation = CABasicAnimation(keyPath: "backgroundColor")
pulseAnimation.fromValue = <#source UIColor#>.cgColor
pulseAnimation.toValue = <#target UIColor#>.cgcolor
pulseAnimation.duration = 0.4
pulseAnimation.autoreverses = true
pulseAnimation.repeatCount = count
pulseAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
self.layer.add(pulseAnimation, forKey: "Pulsate")
CATransaction.commit()
}
}
When pasting this in to a source file in Xcode, replace the placeholders with your two desired colors. Or you can replace the entire lines with something like these values:
pulseAnimation.fromValue = backgroundColor?.cgColor
pulseAnimation.toValue = UIColor.white.cgColor

Changing the frame of an inputAccessoryView in iOS 8

Long time lurker - first time poster!
I am having an issue while recreating a bar with a UITextView like WhatsApp does it.
I am using a custom UIView subclass, and lazily instantiating it on:
- (UIView *)inputAccessoryView
and returning YES on:
- (BOOL)canBecomeFirstResponder
Now, I want to change the size of the inputAccessoryView when the UITextView grows in size. On iOS 7, I would simply change the size of the frame of said view - and not it's origin -, and then call reloadInputViews and it would work: the view would be moved upwards so that it is fully visible above the keyboard.
On iOS 8, however, this does not work. The only way to make it work is to also change the origin of the frame to a negative value. This would be fine, except it creates some weird bugs: for example, the UIView returns to the 'original' frame when entering any text.
Is there something I am missing? I am pretty certain WhatsApp uses inputAccessoryView because of the way they dismiss the keyboard on drag - only in the latest version of the app.
Please let me know if you can help me out! Or if there is any test you would like me to run!
Thank you! :)
BTW, here is the code I am using to update the height of the custom UIView called composeBar:
// ComposeBar frame size
CGRect frame = self.composeBar.frame;
frame.size.height += heightDifference;
frame.origin.y -= heightDifference;
self.composeBar.frame = frame;
[self.composeBar.textView reloadInputViews]; // Tried with this
[self reloadInputViews]; // and this
Edit: full source code is available # https://github.com/manuelmenzella/SocketChat-iOS
I've been banging my head against the wall on this one for quite some time, as the behavior changed from iOS 7 to iOS 8. I tried everything, until the most obvious solution of all worked for me:
inputAccessoryView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
duh!
To sum up JohnnyC's answer: set your inpitAccessoryView's autoresizingMask to .flexibleHeight, calculate its intrinsicContentSize and let the framework do the rest.
Full code, updated for Swift 3:
class InputAccessoryView: UIView, UITextViewDelegate {
let textView = UITextView()
override var intrinsicContentSize: CGSize {
// Calculate intrinsicContentSize that will fit all the text
let textSize = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: CGFloat.greatestFiniteMagnitude))
return CGSize(width: bounds.width, height: textSize.height)
}
override init(frame: CGRect) {
super.init(frame: frame)
// This is required to make the view grow vertically
autoresizingMask = .flexibleHeight
// Setup textView as needed
addSubview(textView)
textView.translatesAutoresizingMaskIntoConstraints = false
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[textView]|", options: [], metrics: nil, views: ["textView": textView]))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[textView]|", options: [], metrics: nil, views: ["textView": textView]))
textView.delegate = self
// Disabling textView scrolling prevents some undesired effects,
// like incorrect contentOffset when adding new line,
// and makes the textView behave similar to Apple's Messages app
textView.isScrollEnabled = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
// Re-calculate intrinsicContentSize when text changes
invalidateIntrinsicContentSize()
}
}
The issue is that in iOS 8, an NSLayoutConstraint that sets the inputAccessoryView's height equal to its initial frame height is installed automatically. In order to fix the layout problem, you need to update that constraint to the desired height and then instruct your inputAccessoryView to lay itself out.
- (void)changeInputAccessoryView:(UIView *)inputAccessoryView toHeight:(CGFloat)height {
for (NSLayoutConstraint *constraint in [inputAccessoryView constraints]) {
if (constraint.firstAttribute == NSLayoutAttributeHeight) {
constraint.constant = height;
[inputAccessoryView layoutIfNeeded];
break;
}
}
}
Here's a complete, self-contained solution (thanks #JohnnyC and #JoãoNunes for pointing me in the right direction, #stigi for explaining how to animate intrinsicContent changes):
class InputAccessoryView: UIView {
// InputAccessoryView is instantiated from nib, but it's not a requirement
override func awakeFromNib() {
super.awakeFromNib()
autoresizingMask = .FlexibleHeight
}
override func intrinsicContentSize() -> CGSize {
let exactHeight = // calculate exact height of your view here
return CGSize(width: UIViewNoIntrinsicMetric, height: exactHeight)
}
func somethingDidHappen() {
// invalidate intrinsic content size, animate superview layout
UIView.animateWithDuration(0.2) {
self.invalidateIntrinsicContentSize()
self.superview?.setNeedsLayout()
self.superview?.layoutIfNeeded()
}
}
}
100% working and very simple solution is to enumerate all constraints and set new height value. Here is some C# code (xamarin):
foreach (var constraint in inputAccessoryView.Constraints)
{
if (constraint.FirstAttribute == NSLayoutAttribute.Height)
{
constraint.Constant = newHeight;
}
}
Unfortunately, iOS8 adds a private height constraint to the inputAccessoryView, and this constraint is not public.
I recommend recreating the accessory view when its frame should change, and call reloadInputViews so that the new one is installed.
This is what I do, and it works as expected.
Yep, iOS8 adds a private height constraint to the inputAccessoryView.
Taking into account that recreating whole inputAccessoryView and replace old one is can be really expensive operation, you can just remove constraints before reload input views
[inputAccessoryView removeConstraints:[inputAccessoryView constraints]];
[textView reloadInputViews];
Just another workaround
To fix this I used inputAccessoryView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
But of course this caused my textview to collapse.
So adding a constraint to the toolbar and updating it when I have to, or adding the constraint to the textview itself and update it worked for me.
frist, get inputAccessoryView and set nil
UIView *inputAccessoryView = yourTextView.inputAccessoryView;
yourTextView.inputAccessoryView = nil;
[yourTextView reloadInputViews];
then set frame and layout
inputAccessoryView.frame = XXX
[inputAccessoryView setNeedsLayout];
[inputAccessoryView layoutIfNeeded];
last set new inputAccessoryView again and reload
yourTextView.inputAccessoryView = inputAccessoryView;
[yourTextView reloadInputViews];

Auto Layout constraint on CALayer IOS

Hi I am developing iPhone application in which I tried to set one side border for edittext. I did this in following way:
int borderWidth = 1;
CALayer *leftBorder = [CALayer layer];
leftBorder.borderColor = [UIColor whiteColor].CGColor;
leftBorder.borderWidth = borderWidth;
leftBorder.frame = CGRectMake(0, textField.frame.size.height - borderWidth, textField
.frame.size.width, borderWidth);
[textField.layer addSublayer:leftBorder];
I put some constraints on my edittext in IB so that when I rotate my device it will adjust width of text field according to that. My problem is that adjusts the width of edittext not adjusting the width of CALayer which I set for my edit text. So I think I have to put some constraints for my CALayer item as well. But I dont know how to do that. ANy one knows about this? Need Help. Thank you.
the whole autoresizing business is view-specific. layers don't autoresize.
what you have to do -- in code -- is to resize the layer yourself
e.g.
in a viewController you would do
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews]; //if you want superclass's behaviour...
// resize your layers based on the view's new frame
self.editViewBorderLayer.frame = self.editView.bounds;
}
or in a custom UIView you could use
- (void)layoutSubviews {
[super layoutSubviews]; //if you want superclass's behaviour... (and lay outing of children)
// resize your layers based on the view's new frame
layer.frame = self.bounds;
}
In Swift 5, you can try the following solution:
Use KVO, for the path "YourView.bounds" as given below.
self.addObserver(self, forKeyPath: "YourView.bounds", options: .new, context: nil)
Then handle it as given below.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "YourView.bounds" {
YourLayer.frame = YourView.bounds
return
}
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
More info about this here
According to this answer, layoutSubviews does not get called in all needed cases. I have found this delegate method more effective:
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if(self != nil) {
[self.layer addSublayer:self.mySublayer];
}
return self;
}
- (void)layoutSublayersOfLayer:(CALayer*)layer
{
self.mySublayer.frame = self.bounds;
}
I implemented the method layoutSubviews of my custom view; inside this method I just update each sublayer's frame to match the current boundaries of my subview's layer.
-(void)layoutSubviews{
sublayer1.frame = self.layer.bounds;
}
My solution when I create with dashed border
class DashedBorderView: UIView {
let dashedBorder = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
//custom initialization
dashedBorder.strokeColor = UIColor.red.cgColor
dashedBorder.lineDashPattern = [2, 2]
dashedBorder.frame = self.bounds
dashedBorder.fillColor = nil
dashedBorder.cornerRadius = 2
dashedBorder.path = UIBezierPath(rect: self.bounds).cgPath
self.layer.addSublayer(dashedBorder)
}
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
dashedBorder.path = UIBezierPath(rect: self.bounds).cgPath
dashedBorder.frame = self.bounds
}
}
Swift 3.x KVO Solution
(Updated #arango_86's answer)
Add Observer
self.addObserver(
self,
forKeyPath: "<YOUR_VIEW>.bounds",
options: .new,
context: nil
)
Observe Values
override open func observeValue(
forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?
) {
if (keyPath == "<YOUR_VIEW.bounds") {
updateDashedShapeLayerFrame()
return
}
super.observeValue(
forKeyPath: keyPath,
of: object,
change: change,
context: context
)
}
When I have to apply KVO I prefer to do it with RxSwift (Only if I am using it for more stuff, do not add this library just for this.)
You can apply KVO with this library too, or in the viewDidLayoutSubviews but I've had better results with this.
view.rx.observe(CGRect.self, #keyPath(UIView.bounds))
.subscribe(onNext: { [weak self] in
guard let bounds = $0 else { return }
self?.YourLayer.frame = bounds
})
.disposed(by: disposeBag)
Riffing off arango_86's answer – if you are applying the KVO fix within your own UIView subclass, the more "Swifty" way to do this is to override bounds and use didSet on it, like so:
override var bounds: CGRect {
didSet {
layer.frame = bounds
}
}
I had a similar problem - needing to set the frame of a 'CALayer' when using auto layout with views (in code, not IB).
In my case, I had a slightly convoluted hierarchy, having a view controller within a view controller. I ended up at this SO question and looked into the approach of using viewDidLayoutSubviews. That didn't work. Just in case your situation is similar to my own, here's what I found...
Overview
I wanted to set the frame for the CAGradientLayer of a UIView that I was positioning as a subview within a UIViewController using auto layout constraints.
Call the subview gradientView and the view controller child_viewController.
child_viewController was a view controller I'd set up as a kind of re-usable component. So, the view of child_viewController was being composed into a parent view controller - call that parent_viewController.
When viewDidLayoutSubviews of child_viewController was called, the frame of gradientView was not yet set.
(At this point, I'd recommend sprinkling some NSLog statements around to get a feel for the sequence of creation of views in your hierarchy, etc.)
So I moved on to try using viewDidAppear. However, due to the nested nature of child_viewController I found viewDidAppear was not being called.
(See this SO question: viewWillAppear, viewDidAppear not being called, not firing).
My current solution
I've added viewDidAppear in parent_viewController and from there I'm calling viewDidAppear in child_viewController.
For the initial load I need viewDidAppear as it's not until this is called in child_viewController that all of the subviews have their frames set. I can then set the frame for the CAGradientLayer...
I've said that this is my current solution because I'm not particularly happy with it.
After initially setting the frame of the CAGradientLayer - that frame can become invalid if the layout changes - e.g. rotating the device.
To handle this I'm using viewDidLayoutSubviews in child_viewController - to keep the frame of the CAGradientLayer within gradientView, correct.
It works but doesn't feel good.
(Is there a better way?)

Resources