I am working on a view which inherits from UIScrollView, and the requirement is that it should start at a contentOffset.y position that is dependent on the view size. Specifically I want to start one screen down in a content that is 3 x the view height.
Like this:
- (void)configureStartCondition {
self.contentSize = CGSizeMake(self.bounds.size.width, self.bounds.size.height * 3.0);
self.contentOffset = CGPointMake(0.0, self.bounds.size.height * 1.0);
}
The view itself is wired up with constraints in Storyboard, just like any view. As it works, the framework will initially give the view the size it has in the storyboard, then when the device size is known, the view's size will be changed to its final size. This is how it should work, and I am fine with this. My question is where do I call configureStartCondition?
An obvious solution would be to put this code in setFrame:, but it doesn't work. setFrame: is only called for the initial frame size, which might or might not be the final size. Why is this?
// NOT working
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self configureStartCondition];
}
A more common place would be in layoutSubview, where I usually do this kind of setup. However, as it is a UIScrollView the layoutSubview is called very frequently as the user scrolls the view. Meaning I would need to save the last height and compare it to make things work, then run through this test millions of times just to be able to initialize. It feels like a kludge to me.
// Working, but ugly
- (void)layoutSubview {
[super layoutSubviews];
if (self.bounds.size.height != self.savedHeight) {
self.savedHeight = self.bounds.size.height;
[self configureStartCondition];
}
// Do layout stuff
}
Another place that may seem good is setBounds:. It will get called for the view size change, but since the contentOffset property is tied to the bounds property, I actually get as many calls here as to layoutSubviews.
So, is there a better place to do it, or a better way to do it?
Side issue, less important in my case, but can the content offset be set from a storyboard?
EDIT: Solutions in Swift are also fine.
setContentSize: works perfect for me. It called only when size changed.
PS: My code to check: (sorry for swift but UIKit make no difference)
class CustomScrollView: UIScrollView {
override var contentSize: CGSize{
didSet{
var offset = contentOffset
offset.y = contentSize.height / 2.0
contentOffset = offset
}
}
}
Related
I'm developing iOS UI with auto layout.
Because I need to set corner radius of view's layer for make view looks like circle, get actual view's size is necessary. (I couldn't change layer by constraints)
Finally, I realize I can get view's real size in viewDidLayoutSubviews and I looks well. But when I called it's super method, size becomes wrong value.
- (void)viewDidLayoutSubviews {
// [super viewDidLayoutSubviews];
if (_viewProfileCoverForReward) {
float circleSize = imageViewOppositeProfile.frame.size.width;
_viewProfileCoverForReward.layer.cornerRadius = circleSize / 2;
NSLog([NSString stringWithFormat:#"(%ld)circle size : %f", super.missionData.missionID, circleSize]);
}
}
Here is my code and it works well. But in my opinion, don't call super's method is not good and why I must not call super's method if I want to get view's real size. It doesn't make sense!
Just like in the native iOS Mail app, when I push a UITableViewController onto a UINavigationController, I would like to make it so that the UITableView initially appears slightly scrolled downwards, obscuring its headerView beneath the navigation controller's navigation bar.
At the same time, even if the height of all of the cells is smaller than the height of the table view, it should be possible for the user to scroll up and down to explicitly show or hide the header view again.
With that logic, it would appear that there are two considerations to make for this implementation:
1) Ensuring that the minimum content size of the table view is at least the height of the table view's frame + the height of the header view.
2) When the table view is initially presented, the content offset is incremented by the height of the header view.
I've tried manually setting both the contentOffset and contentSize properties of the table view in 'viewWillAppear', however this appears to have no effect (It's possible the table view is getting reloaded after that point). Trying to set them in 'viewDidAppear' will work, but that's too late as it only gets called once the 'push' animation has completed.
While this sort of question has been asked before for previous iOS versions, I was unable to get any of them working in iOS 8. Additionally, they all dealt with changing the offset, but not the contentSize of the table view.
Has anyone gotten this sort of behavior working in iOS 7 and/or 8 before?
Update - (30/1/2015)
Alright. This wasn't sitting well with me last night, so I had another play with it, and I found a MUCH better and cleaner solution.
I discovered that the tableView property of UITableViewController is NOT readonly. So it actually makes more sense to simply manage the contentSize property in a UITableView subclass and then assign that subclass back to the UITableViewController.
#implementation TOCustomTableView
- (void)setContentSize:(CGSize)contentSize
{
CGFloat scrollInset = self.contentInset.top + self.contentInset.bottom;
CGFloat height = (CGRectGetHeight(self.bounds) - scrollInset) + CGRectGetHeight(self.tableHeaderView.frame);
contentSize.height = MAX(height, contentSize.height);
[super setContentSize:contentSize];
}
#end
---
#implementation TOCustomTableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView = [[TOCustomTableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
}
#end
This way, the table view's minimum contentSize is always explicitly set to be the height of the table view + the headerView size, achieving the desired effect with zero jittering. :)
Original Answer
trick14 pointed me in the right direction. So the correctly functioning code I ended up with.
- (void)resetTableViewInitialOffset
{
CGPoint contentOffset = self.tableView.contentOffset;
contentOffset.y = self.tableView.contentInset.top + CGRectGetHeight(self.headerView.frame);
self.tableView.contentOffset = contentOffset;
}
- (void)resetTableViewContentSize
{
CGSize contentSize = self.tableView.contentSize;
CGFloat scrollInset = self.tableView.contentInset.top + self.tableView.contentInset.bottom;
CGFloat height = (CGRectGetHeight(self.view.bounds) - scrollInset) + CGRectGetHeight(self.headerView.frame);
contentSize.height = MAX(height, contentSize.height);
self.tableView.contentSize = contentSize;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (!self.headerBarInitiallyHidden) {
[self resetTableViewContentSize];
[self resetTableViewInitialOffset];
self.headerBarInitiallyHidden = YES;
}
}
I'm also making sure to call 'resetTableViewContentSize' each time I perform a 'reloadData' on the table view as well.
I am working on my first project using Auto Layout and custom views. My question is this:
I created my custom view in Interface Builder then added constraints to stretch the view if needed which is working the way I want it to, however, consider the following code snippet from my custom view class -
// MyCustomView
-(id)initWithCoder:(NSCoder*)decoder
{
self = [super initWithCoder:decoder];
if(self != nil)
{
CGFloat layerWidth = self.bounds.size.width;
CGFloat layerHeight = self.bounds.size.height;
}
return self;
}
This code return the size set in IB. My drawing code relies on the new width and height (if the view has been stretched) but I don't know how to retrieve them.
First, that code is utterly silly because you are creating variables layerWidth and layerHeight and throwing them away, which is pointless.
Second, self.bounds.size is always the view's width and height. However, it is pointless to ask about this in initWithCoder:, which (as you have rightly seen) happens long before the view is put into the interface and even longer before the auto layout takes place that resizes it. If your drawing code relies on the bounds size, then retrieve the bounds size when you draw. If you need to draw again because the view has changed size, and if this is not happening all by itself, then implement layoutSubviews to tell the view that it needs to be drawn again.
I have following setup
XIB file which has only landscape view. This view is connection to my controller
There is a label on this view which is connected to IBOutlet UILabel* label
This label is configured like this (it occupies the whole width of screen).
I overrided viewWillAppear and do this (to get the size of label).
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGRect rect = _labelTitleLand.frame;
}
The strange thing (which I don't understand). That it returns size = (width 768, height 21) when it launched in portrait (on iPad), which is correct.
And it returns size = (width 741 height 21) when it's launched in landscape. Which is weird. I anticipated that it will return width 1024, height 21 for landscape.
I was under impression that at the moment of viewWillAppear, all controls sizes are calculated already.
Update 1
If I check labelTitleLand.frame on viewDidAppear then it returns correct results. However, I don't like this, because I want to do some actions (based on this size) which influence how view will be drawn. In the case, if I will do it on viewDidAppear, as I understand there will be visible redrawing.
The layout of the view hierarchy has to be complete before you will get the actual final frames.
So you should check the frame in viewDidLayoutSubviews, which will still be before the view hierarchy is actually drawn. If you need to make changes here you can without causing any redrawing to occur.
viewWillAppear is too early because this is before your autoresizing masks (and/or autolayout constraints) have had their effect.
This seems a problem related to when a method is actually called at runtime.
I solved similar situation using
[self performSelector:#selector(checkMethod) withObject:nil afterDelay:0.1];
in viewWillAppear, and then:
- (void)checkMethod
{
rect = _labelTitleLand.frame;
}
This gives your app the time needed to set its own frame.
It is not so elegant and it looks like a workaround, but it is very effective.
You can also try to force the frame of the UIView that is container of the UILabel in viewWillAppear like this:
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.view.frame = CGRectMake(0.0f, 0.0f, 1024.0f, 768.0f);
CGRect rect = _labelTitleLand.frame;
}
But the first solution is more reliable and usually no lag is experienced.
Forgive me to the obtuse title, as I'm unsure how to describe this question.
Recently many iOS apps utilise a scrolling UI design pattern which helps to maximise screen real-estate, typically hiding the header when the user scrolls downwards.
For example, Instragram's main view has the Instragram header at the top. Scrolling upwards on this view keeps the header fixed at the top, and the view bounces back normally to the top of the content. But scroll down and the header acts as part of the content, making way for an extra 44 points of vertical space.
Its probably that I haven't done much iOS work in a while, but I can't easily figure out how best to impliment this? Apologies for the terrible description.
If the header stays put no matter what, use a separate view on top of the scroll view.
If you use UITableView, you can use section headers.
EDIT Use this code:
- (void)scrollViewDidScroll:(UIScrollView*) scrollView
{
CGPoint offset = scrollView.contentOffset;
CGRect headerFrame = _headerView.frame;
if(offset.y > 0){
headerFrame.origin.y = offset.y;
}
else{
headerFrame.origin.y = 0.0;
}
[_headerView setFrame:headerFrame];
}
(Assumes _headerView is your header, sitting on top of the scroll view, not inside it. Also, both scroll view and header begin at the top of their parent view, y==0. Also, your view controller must be set up as delegate of the scroll view)
I just wrote this code from memory; haven't tested it but at most it should only need tweaking.
I tried ranReloaded's answer above but it seems that calling setFrame: on a UIScrollView stops the view from bouncing when going beyond its bounds.
Instead I set the scroll view to fit inside another UIView called scrollerWrapper. Applying the calculated origin and height to this view gives me effect I'm after plus retains the bounce behaviour.
- (void)scrollViewDidScroll:(UIScrollView*) scrollView
{
CGPoint offset = scrollView.contentOffset;
CGRect headerFrame = header.frame;
CGRect wrapperFrame = scrollerWrapper.frame;
if(offset.y > 0){
headerFrame.origin.y = -offset.y;
wrapperFrame.origin.y = MAX(0, headerFrame.size.height - offset.y);
}
else{
headerFrame.origin.y = 0.0;
wrapperFrame.origin.y = headerFrame.size.height;
}
wrapperFrame.size.height = self.view.frame.size.height - wrapperFrame.origin.y;
[header setFrame:headerFrame];
[scrollerWrapper setFrame:wrapperFrame];
}