Can we only configure UI correctly in viewDidAppear? and not in viewWillAppear/viewDidLoad? - objective-c

I have a static table view with cells that have a rounded border. I have noticed when testing on different simulators that whilst my auto layout constraints work, the border isn't always the right width. This particular screen consists of a view controller with a UIView containing an embedded tableViewController
I have done some investigating and found that the width of the border actually depends on the width of the storyboard phone. This means if I have a storyboard for an iPhone 8, the 8+ will have cells too short and vice versa, an 8+ storyboard results in cells that are too long (and extend off screen) for the 8.
Currently I am setting the cell borders in the viewDidLoad, here is the code I am using to configure the cells border:
- (void)configureCellThree {
//Add Border
CALayer *borderLayer = [CALayer layer];
CGRect borderFrame = CGRectMake(0, 0, (_contentCellThree.frame.size.width), (_contentCellThree.frame.size.height));
[borderLayer setBackgroundColor:[[UIColor clearColor] CGColor]];
[borderLayer setFrame:borderFrame];
[borderLayer setCornerRadius:_contentCellThree.frame.size.height / 2];
[borderLayer setBorderWidth:1.0];
[borderLayer setBorderColor:[kTextColor2 CGColor]];
// [borderLayer setOpacity:0.5];
[_contentCellThree.layer addSublayer:borderLayer];
}
Now if I run this code within the viewDidAppear, everything will work across both devices. I added some logs to my main view controller to find out how things were being set.
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"VDL - SELF.VIEW = %#", self.view);
NSLog(#"VDL - CONTAINER VIEW = %#", self.profileScrollingContainerView);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"VDA- SELF.VIEW = %#", self.view);
NSLog(#"VDA - CONTAINER VIEW = %#", self.profileScrollingContainerView);
}
I had suspected that viewDidLoad was using the sizing information from the storyboard instead of the view itself (which doesn't seem right). This logging confirms this. If I look at the UIView responsible for displaying my tableview, when the storyboard is set to 8+ it has the following frame attributes: X = 0, Y = 349, W = 414, H = 338. Now lets look at the results of the logging:
VDL - SELF.VIEW = <UIView: 0x7fa545401f10; frame = (0 0; 375 667);
VDL - CONTAINER VIEW = <UIView: 0x7fa545401b50; frame = (0 349; 414 338);
VDA- SELF.VIEW = <UIView: 0x7fa545401f10; frame = (0 64; 375 603);
VDA - CONTAINER VIEW = <UIView: 0x7fa545401b50; frame = (0 285; 375 269);
So when the view loads the tableview is getting the wrong information about the views size. When the viewDidAppear gets called it has the correct sizing of the view and will work properly. My issue here is that I don't want to be calling initialising code in my viewDidAppear.
I read here that I should be putting my UI Geometry code into the viewWillAppear however I have tried this and I get the same issues.
VWA - CONTAINER VIEW = <UIView: 0x7fec04d97700; frame = (0 349; 414 338);
So to consolidate my question, How can I get the properties of my view before the view has loaded/appeared so I can correctly setup my UI?
I have read that I will need to subclass UIView and potentially use setFrame however I don't really know how I'd actually go about doing this.

Subclass UITableViewCell and implement layoutSubviews, i.e. see the docs:
"Subclasses can override this method as needed to perform more precise layout of their subviews. You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want. You can use your implementation to set the frame rectangles of your subviews directly."

So it turns out I was using the wrong method to do this. I found this question which solved my whole issue. Basically if you need to perform UI calculations (such as adding custom views) you should be performing them in -(void)viewDidLayoutSubviews. This method is called after the view has worked out all of its sizing and constraints so anything you do in here will be executed using the right properties of your view!

Related

UI Code in viewDidLayoutSubViews doesn't render correctly at runtime

I have recently been battling with my view controller to set the correct dimensions of a border on a UIView.
*edit, this is for a static tableview (i am using it as a form). I am aware of rounding cells in a .xib and reusing the cell however I am not reusing cells, this approach would not be useful to me.
I have a screen with a static tableview which is embedded within a UIView and displayed on a main View Controller. I had been having problems with the border as if I set it anywhere before viewDidAppear my method to add the border would use the wrong dimensions and display the border incorrectly. Here is the method I'm using to add a border to my UIView:
- (void)createCellBorder: (UIView *)view container:(UIView *)container {
CALayer *borderLayer = [CALayer layer];
CGRect adjustedContainer = CGRectMake(0, 0, container.frame.size.width - 20, container.frame.size.height);
CGRect borderFrame = CGRectMake(0, 0, (adjustedContainer.size.width), (view.frame.size.height));
NSLog(#"VIEW DIMENSIONS FOR %# %f, %f",view , view.frame.size.width, view.frame.size.height);
[borderLayer setBackgroundColor:[[UIColor clearColor] CGColor]];
[borderLayer setFrame:borderFrame];
[borderLayer setCornerRadius:view.frame.size.height / 2];
[borderLayer setBorderWidth:1.0];
[borderLayer setBorderColor:[kTextColor2 CGColor]];
[view.layer addSublayer:borderLayer];
}
Note * To fix some bugs with a single cell having the wrong dimensions (always the final cell in the table) I am setting the width of the border layer based on the UITableView it is contained in.
Anyway I eventually found out that instead of using viewDidAppear I should be using viewDidLayoutSubviews to set my UI Geometry as at this point in time the view has calculated its dimensions. This works a treat and my cells will display with rounded borders... However there is a problem with the rendering itself.
First lets have a look at the cells being rounded from the viewDidAppear.
Everything looks fine, this is exactly how I want my cells to look (I have removed the content of them to demonstrate the borders only). Lets have a look at the code I'm using to round the cells.
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self configureCells];
}
- (void)configureCells {
[self createCellBorder:_contentCellOne container:_profileTable];
[self createCellBorder:_contentCellTwo container:_profileTable];
[self createCellBorder:_contentCellThree container:_profileTable];
[self createCellBorder:_contentCellFour container:_profileTable];
[self createCellBorder:_contentCellFive container:_profileTable];
[self createCellBorder:_contentCellSix container:_profileTable];
[self createCellBorder:_contentCellSeven container:_profileTable];
}
Now lets change where I call configureCells from and see the difference.
-(void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self configureCells];
}
All that has changed is where I call this method from. And in theory this should be the right place to configure my UI (and certainly more optimal than calling from viewDidAppear).
As you can see there is some quite noticeable 'distortion' around the corners of the cells. The pictures provided are from the simulator but I have tried this on a device and get the same result (maybe looks worse on my phone).
I have no idea why this is happening, and it means that whilst I have got my code out of the viewDidAppear, its just as unusable as before. I assume there will be a rendering option I can tweak in the viewDidLayoutSubViews but can't seem to find anything. All suggestions greatly appreciated!
In my opinion, you should create a subclass for UITableViewCell and configure border of cells inside layoutIfNeeded method.
I have create a demo project, you can check it TableViewCellBorder.

UIImageView autoresizingmask not working in certain cases

I am experimenting with a block-breaking iOS app to learn more about UI features. Currently, I am having issues trying to make it work for screen rotation.
I am able to get the blocks to re-arrange properly after screen rotation but am having trouble with getting the UIImageView for the paddle re-arrange.
My code is split as follows, VC calls initializes an object of the BlockModel class. This object stores a CGRect property (which is the CGRect corresponding to the paddle's ImageView).
The VC then creates an imageView initialized with the paddle image, sets the autoresinging property on the image view (to have flexible external masks), sets the frame based on the CGRect in the model object and adds the imageView as a sub-view of the main view being handled by the VC.
The code is below.
When I rotate, I am seeing that the ImageView is not being automatically repositioned.
If I do all the image view and CGRect creation in the vC, then it works (code sample 2).
Is this expected behavior? If yes, why is autoresizing not kicking in if the CGRect is obtained from a property in another object?
Full Xcode project code is here (github link)
EDIT
Looks like things don't work if I store the imageView as a property. I was doing this to have quick access to it. Why doesn't it work if imageView is stored as a property?
Code where model is initialized
self.myModel = [[BlockerModel alloc] initWithScreenWidth:self.view.bounds.size.width andHeight:self.view.bounds.size.height];
Model initialization code
-(instancetype) initWithScreenWidth:(CGFloat)width andHeight:(CGFloat)height
{
self = [super init];
if (self)
{
self.screenWidth = width;
self.screenHeight = height;
UIImage* paddleImage = [UIImage imageNamed:#"paddle.png"];
CGSize paddleSize = [paddleImage size];
self.paddleRect = CGRectMake((self.screenWidth-paddleSize.width)/2, (1 - PADDLE_BOTTOM_OFFSET)*self.screenHeight, paddleSize.width, paddleSize.height);
}
return self;
}
Code in VC where imageView is initialized
self.paddleView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"paddle"]];
self.paddleView.backgroundColor = [UIColor clearColor];
self.paddleView.opaque = NO;
self.paddleView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin|UIViewAutoresizingFlexibleLeftMargin|UIViewAutoresizingFlexibleRightMargin|UIViewAutoresizingFlexibleBottomMargin;
NSLog(#"Paddle rect is %#",NSStringFromCGRect(self.myModel.paddleRect));
[self.paddleView setFrame:self.myModel.paddleRect];
[self.view addSubview:self.paddleView];
If I instead use this code in the VC to initialize imageView things work
UIImage* paddleImage = [UIImage imageNamed:#"paddle.png"];
CGSize paddleSize = [paddleImage size];
CGRect paddleRect = CGRectMake((self.view.bounds.size.width-paddleSize.width)/2, (1 - PADDLE_BOTTOM_OFFSET)*self.view.bounds.size.height, paddleSize.width, paddleSize.height);
UIImageView *paddleView = [[UIImageView alloc] initWithImage:paddleImage];
paddleView.backgroundColor = [UIColor clearColor];
paddleView.opaque = NO;
paddleView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin|UIViewAutoresizingFlexibleLeftMargin|UIViewAutoresizingFlexibleRightMargin|UIViewAutoresizingFlexibleBottomMargin;
[paddleView setFrame:paddleRect];
[self.view addSubview:paddleView];
Found the issue. I was using the model object to handle all my "game object location" logic. E.g VC would calculate the X axis deltas from the touch events & forward them to the model object. Also, CADisplayLink events would be forwarded so that model can update ball location based on velocity and time since last event. It will then use updated location to detect collisions. This split was used because model class also had the methods to detect collisions with sides, paddle/ball etc.
The issue was that the model object was rewriting the CGRect of the paddleView by adding the delta it received from VC to the origin.x of current paddleRect it had stored. This paddleRect did not take into account the automatic adjustment to the CGRect that is done by auto-resizing after a rotation.
The fix was for the VC to set the CGRect of the paddleRect (set to paddleView frame) before calling the method in the model to update all the game properties and detect collisions. This way model only takes care of logic of collusion detection and updating ball movement and velocity based on it. The VC uses current paddleView location and hence automatically accounts for the automatic adjustment to the CGRect that is done by auto-resizing after a rotation.
Source code in github link updated.

UIVIew from XIB with Autolayout to UItableView Header

I am writing because I have a problem with the Auto Layout.
I'm trying to create a simple view in InterfaceBuilder with Auto Layout I want to load code and enter as a header of a table (not as header section). I explain briefly what are the characteristics.
The imageView must be square and must be as wide as the screen.
The space under the picture to the bottom of view that contains the button and label must be high 50 points.
Between image and button has to be a fixed distance of 12 points.
Between image and label must be a fixed distance of 13 points.
All these features are able to get them with Auto Layout. I added a constraint to the aspect ratio of the image (1: 1) and the various constraints for distances. all right.
The real problem is that by launching the app on iphone 6+ simulator (414 points of width), the image (with the label and button) goes above the cells.
Enabling various transparencies I noticed that the superView of Image View, only increase the width. It does not increase its height! How do I fix?
This is the code:
- (void)viewDidLoad{
//...
PhotoDetailsHeaderView *hView = (PhotoDetailsHeaderView *)[[[NSBundle mainBundle] loadNibNamed:#"PhotoDetailsHeaderView" owner:self options:nil] objectAtIndex:0];
hView.delegate = self;
self.tableView.tableHeaderView = hView;
//...
}
This is how I create the xib:
and this is how it is on the simulator, the green box is Uiimageview and the yellow box (under green box) is the mainview (or superview):
How can fix it?
Many thanks to all!
You'll need to add a property to store your PhotoDetailsHeaderView:
#property (nonatomic, strong) PhotoDetailsHeaderView *headerView;
Then calculate its expected frame in viewDidLayoutSubviews. If it needs updating, update its frame and re-set the tableHeaderView property. This last step will force the tableView to adapt to the header's updated frame.
- (void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
CGRect expectedFrame = CGRectMake(0.0,0.0,self.tableview.size.width,self.tableView.size.width + 50.0);
if (!CGRectEqualToRect(self.headerView.frame, expectedFrame)) {
self.headerView.frame = expectedFrame;
self.tableView.tableHeaderView = self.headerView;
}
}
The problem is probably that in iOS you have to reset the header of the table view manually (if it has changed its size). Try something along these lines:
CGRect newFrame = imageView.frame;
imageView.size.height = imageView.size.width;
imageView.frame = newFrame;
[self.tableView setTableHeaderView:imageView];
This code should be in -(void)viewDidLayoutSubviews method of your view controller.

How to get object position and size in UIView objective c

I put UIImageView in my Scene from Object library, and give it an image and defined OUTLET in .h file. Now I want to check its coordinates, or center point, or frame X,Y,Width,Height.
I am using
This
CGRect newFrameSize = CGRectMake(recycleBin.frame.origin.x, recycleBin.frame.origin.y,
recycleBin.frame.size.width, recycleBin.frame.size.height);
or
CGRect newFrameSize = recycleBin.frame;
by using this
NSLog(#"%#", NSStringFromCGRect(newFrameSize));
gives same result that is
2013-01-16 21:42:25.101 xyzapp[6474:c07] {{0, 0}, {0, 0}}
I want its actual position and size when viewcontroller loaded, so when user click on image view it will fadeout by zoom-in towards users and will disappear, and when user tap on reset button, it fadein and zoom-in back to original form (reverse to the previous animation).
Also give me hint, how to perform this animation on UIImageView or any button or label. Thx
Unfortunately, you can't check an item's actual frame as set in IB in -viewDidLoad. The earliest you can check it (that I've found) is by overriding -viewDidAppear:. But, since -viewDidAppear: could be called multiple times throughout the life of the view, you need to make sure you're not saving the frame it's in the modified state.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if(savedFrame == CGRectZero) {
savedFrame = self.recycleBin.frame;
NSLog(#"Frame: %#", NSStringFromCGRect(savedFrame));
}
}
Where savedFrame is a member variable (or you could make it a property).
From the description of the animation you're wanting, it sounds like adjusting the frame isn't the way to go about it. It sounds like you're wanting to get the effect of the view stretching and fading out (and the reverse when being reset)? If so, some code like this might be more so what you're looking for...
Fade out:
float animationDuration = 2.0f; // Duration of animation in seconds
float zoomScale = 3.0f; // How much to zoom in duration the animation
[UIView animateWithDuration:animationDuration animations:^{
CGAffineTransform transform = CGAffineTransformMakeScale(zoomScale, zoomScale);
self.recycleBin.transform = transform;
self.recycleBin.alpha = 0; // Make fully transparent
}];
And then, to reset the view:
float animationDuration = 2.0f; // Duration of animation in seconds
[UIView animateWithDuration:animationDuration animations:^{
CGAffineTransform transform = CGAffineTransformMakeScale(1.0f, 1.0f);
self.recycleBin.transform = transform;
self.recycleBin.alpha = 1.0; // Make fully opaque
}];
You can play around with the numbers to see if you get the effects you desire. Most animations in iOS are actually extremely simple to do. This code would work for any UIView subclass.
It sounds as if your IBOutlet is not attached to your class.
Open up your view controller header file (if that is where you property declaration is) and look beside the declaration:
Notice how on the first IBOutlet, the circle (to the left of the line number) is filled in. This means that it is connected to your scene. However, the second one is not (the circle is not filled in).

UIButton inside UIView doesn't respond to touch events

I've put a UIButton inside a custom UIView and the button is not receiving any touch events (it doesn't get into the highlighted state, so my problem is not about being unable to wire up a touch inside up handler). I've tried both putting it into the XIB in Interface Builder, and also tried programatically adding the UIButton into the UIView seperately, both ended with no luck. All my views are inside a UIScrollView, so I first though UIScrollView may be blocking them, so I've also added a button programatically exactly the same way I add my custom view into UIScrollView, and the button worked, elimination the possibility of UIScrollView could be the cause. My View's hiearchy is like this:
The button is over the image view, and the front layer isn't occupying my button completely, so there's no reason for me not be physically interacting with the button. At my custom view's code side, I'm creating my view as such:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
UIView *sub = [[[NSBundle mainBundle] loadNibNamed:#"ProfileView" owner:self options:nil] objectAtIndex:0];
[self addSubview:sub];
[sub setUserInteractionEnabled:YES];
[self setUserInteractionEnabled:YES];
CALayer *layer = sub.layer;
layer.masksToBounds = YES;
layer.borderWidth = 5.0;
layer.borderColor = [UIColor whiteColor].CGColor;
layer.cornerRadius = 30.0;
/*layer.shadowOffset = CGSizeZero;
layer.shadowRadius = 20.0;
layer.shadowColor = [[UIColor blackColor] CGColor];
layer.shadowOpacity = 0.8;
*/
}
return self;
}
I've tried all combinations of setUserInteractionsEnabled, and had no luck. (Yes, also set them to checked in Interface Builder too). I've also read in another question with a similar problem that I should try overriding 'canBecomeFirstResponder' to return 'YES' and I've also done that too. But the problem persists, I can't click the button. I've not given any special properties, settings to the button, it's just a regular one. My other objects in the view (labels below, image view behind the button etc.) are working properly without problems. What could be possibly wrong here?
Thanks,
Can.
UPDATE: Here is a quick reproduction of the problem: https://dl.dropbox.com/u/79632924/Test.zip
Try to run and click the button.
Looking at the test project, I believe your problem in the way you create TestView, you do not specify the frame for it, so basically the parent view is 0 size, and the subviews you see from XIB extending out of the parent view and thus do not get anything in responder chain.
You should either specify the frame when creating TestView, or adjust the frame after loading XIB file.
I have had this problem as well. The cause for me was that the UIButton superview frame was of height 0, so I believe that even though a touch was happening, it was not being passed down to the button.
After making sure that the button's superview took a larger rectangle as a frame the button actions worked.
The root cause for this problem on my side was a faulty auto layout implementation (I forgot to set the height constraint for the button's superview).
I've found the solution. I was initializing my custom view as:
MyView *view = [[MyView alloc] init];
I've initialized it instead with a frame of my view's size, and it started responding to events:
CGRect rect = CGRectMake(0,0,width,height);
MyView *view = [[MyView alloc] initWithFrame:rect];
Storyboard Solution
Just for anyone wanting a solution to this when using storyboards and constraints.
Add a constraint between the superview (containing the button) and the UIButton with an equal heights constraint.
In my case, I had selected embed UIButton in a UIView with no inset on the storyboard. Adding the additional height constraint between the UIButton and the superview allowed the UIButton to respond to touches.
You can confirm the issue by starting the View Debugger and visually confirm that the superview of the UIButton is not selectable.
(Xcode 11, *- Should also work in earlier versions)