I have a view that has rows and columns of imageviews in it.
If this view is resized, I need to rearrange the imageviews positions.
This view is a subview of another view that gets resized.
Is there a way to detect when this view is being resized?
As Uli commented below, the proper way to do it is override layoutSubviews and layout the imageViews there.
If, for some reason, you can't subclass and override layoutSubviews, observing bounds should work, even when being kind of dirty. Even worse, there is a risk with observing - Apple does not guarantee KVO works on UIKit classes. Read the discussion with Apple engineer here: When does an associated object get released?
original answer:
You can use key-value observing:
[yourView addObserver:self forKeyPath:#"bounds" options:0 context:nil];
and implement:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == yourView && [keyPath isEqualToString:#"bounds"]) {
// do your stuff, or better schedule to run later using performSelector:withObject:afterDuration:
}
}
In a UIView subclass, property observers can be used:
override var bounds: CGRect {
didSet {
// ...
}
}
Without subclassing, key-value observation with smart key-paths will do:
var boundsObservation: NSKeyValueObservation?
func beginObservingBounds() {
boundsObservation = observe(\.bounds) { capturedSelf, _ in
// ...
}
}
Create subclass of UIView, and override layoutSubviews
Swift 4 keypath KVO -- This is how I detect autorotate and moving to iPad side panel. Should work work any view. Had to observe the UIView's layer.
private var observer: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
observer = view.layer.observe(\.bounds) { object, _ in
print(object.bounds)
}
// ...
}
override func viewWillDisappear(_ animated: Bool) {
observer?.invalidate()
//...
}
You can create a subclass of UIView and override the
setFrame:(CGRect)frame
method. This is the method called when the frame (i.e. the size) of the view is changed. Do something like this:
- (void) setFrame:(CGRect)frame
{
// Call the parent class to move the view
[super setFrame:frame];
// Do your custom code here.
}
Pretty old but still a good question. In Apple's sample code, and in some of their private UIView subclasses, they override setBounds roughly like:
-(void)setBounds:(CGRect)newBounds {
BOOL const isResize = !CGSizeEqualToSize(newBounds.size, self.bounds.size);
if (isResize) [self prepareToResizeTo:newBounds.size]; // probably saves
[super setBounds:newBounds];
if (isResize) [self recoverFromResizing];
}
Overriding setFrame: is NOT a good idea. frame is derived from center, bounds, and transform, so iOS will not necessarily call setFrame:.
If you're in a UIViewController instance, overriding viewDidLayoutSubviews does the trick.
override func viewDidLayoutSubviews() {
// update subviews
}
Related
I'm trying to hook up a scroll view using Interface Builder, and the UIScrollViewDelegate.scrollViewDidScroll method isn't getting invoked on scroll.
In IB, I have a view controller that uses my PagedScrollViewController as a custom class. In that class, I have:
class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
func scrollViewDidScroll(scrollView: UIScrollView!) {
println("scrollViewDidScroll")
}
}
Unfortunately, that println is never getting invoked. I know that PagedScrollViewController is being connected correctly because if I add a viewDidLoad method, that gets invoked. Is there something extra I need to do to attach the delegate other than setting the custom class?
Turns out I needed to attach the scroll view's delegate to the the controller. Here's what worked for me:
class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
}
I was thinking maybe something like this might work:
for (UIView* b in self.view.subviews)
{
[b removeFromSuperview];
}
I want to remove every kind of subview. UIImages, Buttons, Textfields etc.
[self.view.subviews makeObjectsPerformSelector: #selector(removeFromSuperview)];
It's identical to your variant, but slightly shorter.
self.view.subviews.forEach({ $0.removeFromSuperview() })
Identical version in Swift.
Swift:
extension UIView {
func removeAllSubviews() {
for subview in subviews {
subview.removeFromSuperview()
}
}
}
You can use like this
//adding an object to the view
view.addSubView(UIButton())
// you can remove any UIControls you have added with this code
view.subviews.forEach { (item) in
item.removeFromSuperview()
}
view is the view that you want to remove everything from. you are just removing every subview by doing forEach
For Swift 4+.You can make a extension to UIView. Call it whenever necessary.
extension UIView {
func removeAllSubviews() {
subviews.forEach { $0.removeFromSuperview() }
}
}
So in a UITableView when you have sections the section view sticks to the top until the next section overlaps it and then it replaces it on top. I want to have a similar effect, where basically I have a UIView in my UIScrollView, representing the sections UIView and when it hits the top.. I want it to stay in there and not get carried up. How do I do this? I think this needs to be done in either layoutSubviews or scrollViewDidScroll and do a manipulation on the UIVIew..
To create UIView in UIScrollView stick to the top when scrolled up do:
func createHeaderView(_ headerView: UIView?) {
self.headerView = headerView
headerViewInitialY = self.headerView.frame.origin.y
scrollView.addSubview(self.headerView)
scrollView.delegate = self
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let headerFrame = headerView.frame
headerFrame.origin.y = CGFloat(max(headerViewInitialY, scrollView.contentOffset.y))
headerView.frame = headerFrame
}
Swift Solution based on EVYA's response:
var navigationBarOriginalOffset : CGFloat?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
navigationBarOriginalOffset = navigationBar.frame.origin.y
}
func scrollViewDidScroll(scrollView: UIScrollView) {
navigationBar.frame.origin.y = max(navigationBarOriginalOffset!, scrollView.contentOffset.y)
}
If I recall correctly, the 2010 WWDC ScrollView presentation discusses precisely how to keep a view in a fixed position while other elements scroll around it. Watch the video and you should have a clear-cut approach to implement.
It's essentially updating frames based on scrollViewDidScroll callbacks (although memory is a bit hazy on the finer points).
Evya's solution works really well, however if you use Auto Layout, you should do something like this (The Auto Layout syntax is written in Masonry, but you get the idea.):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
//Make the header view sticky to the top.
[self.headerView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.scrollView.mas_top).with.offset(scrollView.contentOffset.y);
make.left.equalTo(self.scrollView.mas_left);
make.right.equalTo(self.scrollView.mas_right);
make.height.equalTo(#(headerViewHeight));
}];
[self.scrollView bringSubviewToFront:self.headerView];
}
I have a UIView which has about 8 different CALayer sublayers added to its layer.
If I modify the view's bounds (animated), then the view itself shrinks (I checked it with a backgroundColor), but the sublayers' size remains unchanged.
How to solve this?
I used the same approach that Solin used, but there's a typo in that code. The method should be:
- (void)layoutSubviews {
[super layoutSubviews];
// resize your layers based on the view's new bounds
mylayer.frame = self.bounds;
}
For my purposes, I always wanted the sublayer to be the full size of the parent view. Put that method in your view class.
Since CALayer on the iPhone does not support layout managers, I think you have to make your view's main layer a custom CALayer subclass in which you override layoutSublayers to set the frames of all sublayers. You must also override your view's +layerClass method to return the class of your new CALayer subclass.
I used this in the UIView.
-(void)layoutSublayersOfLayer:(CALayer *)layer
{
if (layer == self.layer)
{
_anySubLayer.frame = layer.bounds;
}
super.layoutSublayersOfLayer(layer)
}
Works for me.
I had the same problem. In a custom view's layer I added two more sublayers. In order to resize the sublayers (every time the custom view's boundaries change), 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.
Something like this:
-(void)layoutSubviews{
//keep the same origin, just update the width and height
if(sublayer1!=nil){
sublayer1.frame = self.layer.bounds;
}
}
2017:
The literal answer to this question:
"CALayers didn't get resized on its UIView's bounds change. Why?"
is that for better or worse:
needsDisplayOnBoundsChange
defaults to false (!!) in CALayer.
solution,
class CircularGradientViewLayer: CALayer {
override init() {
super.init()
needsDisplayOnBoundsChange = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func draw(in ctx: CGContext) {
go crazy drawing in .bounds
}
}
Indeed, I direct you to this QA
https://stackoverflow.com/a/47760444/294884
which explains, what the hell the critical contentsScale does. You usually need to set that, when you set needsDisplayOnBoundsChange.
Swift 3 Version
In Custom cell, Add Following lines
Declare first
let gradientLayer: CAGradientLayer = CAGradientLayer()
Then add following lines
override func layoutSubviews() {
gradientLayer.frame = self.YourCustomView.bounds
}
As [Ole] wrote CALayer does not support autoresizing on iOS. So you should adjust layout manually. My option was to adjust layer's frame within (iOS 7 and earlier)
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
or (as of iOS 8)
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinato
In your custom view, you need declare variable for your custom layer, don't declare variable in scope init. And just init it once time, don't try set null value and reinit
class CustomView:UIView {
var customLayer:CALayer = CALayer()
override func layoutSubviews() {
super.layoutSubviews()
// guard let _fillColor = self._fillColor else {return}
initializeLayout()
}
private func initializeLayout() {
customLayer.removeFromSuperView()
customLayer.frame = layer.bounds
layer.insertSubview(at:0)
}
}
I have a UIView which has about 8 different CALayer sublayers added to its layer.
If I modify the view's bounds (animated), then the view itself shrinks (I checked it with a backgroundColor), but the sublayers' size remains unchanged.
How to solve this?
I used the same approach that Solin used, but there's a typo in that code. The method should be:
- (void)layoutSubviews {
[super layoutSubviews];
// resize your layers based on the view's new bounds
mylayer.frame = self.bounds;
}
For my purposes, I always wanted the sublayer to be the full size of the parent view. Put that method in your view class.
Since CALayer on the iPhone does not support layout managers, I think you have to make your view's main layer a custom CALayer subclass in which you override layoutSublayers to set the frames of all sublayers. You must also override your view's +layerClass method to return the class of your new CALayer subclass.
I used this in the UIView.
-(void)layoutSublayersOfLayer:(CALayer *)layer
{
if (layer == self.layer)
{
_anySubLayer.frame = layer.bounds;
}
super.layoutSublayersOfLayer(layer)
}
Works for me.
I had the same problem. In a custom view's layer I added two more sublayers. In order to resize the sublayers (every time the custom view's boundaries change), 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.
Something like this:
-(void)layoutSubviews{
//keep the same origin, just update the width and height
if(sublayer1!=nil){
sublayer1.frame = self.layer.bounds;
}
}
2017:
The literal answer to this question:
"CALayers didn't get resized on its UIView's bounds change. Why?"
is that for better or worse:
needsDisplayOnBoundsChange
defaults to false (!!) in CALayer.
solution,
class CircularGradientViewLayer: CALayer {
override init() {
super.init()
needsDisplayOnBoundsChange = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func draw(in ctx: CGContext) {
go crazy drawing in .bounds
}
}
Indeed, I direct you to this QA
https://stackoverflow.com/a/47760444/294884
which explains, what the hell the critical contentsScale does. You usually need to set that, when you set needsDisplayOnBoundsChange.
Swift 3 Version
In Custom cell, Add Following lines
Declare first
let gradientLayer: CAGradientLayer = CAGradientLayer()
Then add following lines
override func layoutSubviews() {
gradientLayer.frame = self.YourCustomView.bounds
}
As [Ole] wrote CALayer does not support autoresizing on iOS. So you should adjust layout manually. My option was to adjust layer's frame within (iOS 7 and earlier)
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
or (as of iOS 8)
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinato
In your custom view, you need declare variable for your custom layer, don't declare variable in scope init. And just init it once time, don't try set null value and reinit
class CustomView:UIView {
var customLayer:CALayer = CALayer()
override func layoutSubviews() {
super.layoutSubviews()
// guard let _fillColor = self._fillColor else {return}
initializeLayout()
}
private func initializeLayout() {
customLayer.removeFromSuperView()
customLayer.frame = layer.bounds
layer.insertSubview(at:0)
}
}