I am on macOS 10.16 (BigSur) XCode 12, objective-c.
I have create a NSTableView within an NSScroll View programmatically and filled it with dummy data.
I CAN scroll down while i CANNOT scroll left and right. So i assume there's some misconfiguration in the code but i can't figure it out.
Here's the behaviour -as you can see theres more content on the right, but scrollbar doesn't scroll:
And here's the code:
NSEdgeInsets insets = NSEdgeInsetsMake (0,0,0,0); // This is usually dynamic, just for demo purposes
NSEdgeInsets margins = NSEdgeInsetsMake (0,0,0,0); // This is usually dynamic, just for demo purposes
// Create tableView
NSTableView* tableView = [[NSTableView alloc] initWithFrame:NSZeroRect];
tableView.translatesAutoresizingMaskIntoConstraints = NO;
tableView.backgroundColor = [NSColor clearColor];
tableView.delegate = self.delegate ? self.delegate : weakSelf;
tableView.dataSource = self.delegate ? self.delegate : weakSelf;
tableView.rowHeight = [SHDataTableCellView height];
tableView.intercellSpacing = NSMakeSize(2, 2);
tableView.headerView = [[NSTableHeaderView alloc] initWithFrame:NSMakeRect(0, 0, 0, CGFLOAT_MIN)];
if (#available(macOS 11.0, *)) {tableView.style = NSTableViewStylePlain;}
self.tableView = tableView;
// Create Scroll View
NSScrollView* scrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect];
// Setup scrollView
scrollView.drawsBackground = NO;
scrollView.hasVerticalScroller = YES;
scrollView.hasHorizontalScroller = YES;
scrollView.autohidesScrollers = NO;
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.documentView = tableView;
scrollView.scrollerStyle = NSScrollerKnobStyleDefault;
scrollView.automaticallyAdjustsContentInsets = NO;
scrollView.contentInsets = insets;
scrollView.scrollerInsets = NSEdgeInsetsMake(-(insets.top), -(insets.left), -(insets.bottom), -(insets.right));
self.scrollView = scrollView;
[self.view addSubview:scrollView];
// Add constraints
NSDictionary *viewBindings = NSDictionaryOfVariableBindings(view,scrollView);
NSDictionary *metrics = #{ #"marginLeft" : #(margins.left),
#"marginRight" : #(margins.right),
#"marginBottom" : #(margins.bottom),
#"marginTop" : #(margins.top),
#"insetLeft" : #(insets.left),
#"insetRight" : #(insets.right),
#"insetBottom" : #(insets.bottom),
#"insetTop" : #(insets.top)};
[view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(marginLeft)-[scrollView]-(marginRight)-|" options:0 metrics:metrics views:viewBindings]];
[view addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-(marginTop)-[scrollView]-(marginBottom)-|" options:0 metrics:metrics views:viewBindings]];
[scrollView addConstraint:[NSLayoutConstraint constraintWithItem:self.tableView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:scrollView
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0]];
[scrollView addConstraint:[NSLayoutConstraint constraintWithItem:scrollView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:60]]; // equals 2 rows of content
NSEdgeInsets margin and insets are set to 0 in this example are tend to be used as dynamic variables (you may ignore them)
Removed the width constraint and it worked. Sometimes its just too obvious and you still don't see it.
Related
I'm really stuck with this issue. I'm trying to add a button on top of an AVPlayerViewController that says "Skip Intro" (similar to the button that Netflix has on their content).
I've added 4 or 5 buttons horizontally starting just above the left side of the seek bar and each button are spaced about 10 points apart. I only need 1 button, but at this point I cannot figure out why I can't click any of these buttons at all:
I've declared the button and the focus guide variables as private:
Here's the viewDidLoad method:
- (void)viewDidLoad
{
[super viewDidLoad];
self.avPlayerViewController = [[AVPlayerViewController alloc] init];
[self addChildViewController:self.avPlayerViewController];
[self.view addSubview:self.avPlayerViewController.view];
self.avPlayerViewController.showsPlaybackControls = YES;
self.avPlayerViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:self.avPlayerViewController.view
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeHeight
multiplier:1.0
constant:0];
NSLayoutConstraint *width = [NSLayoutConstraint constraintWithItem:self.avPlayerViewController.view
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:0.0];
NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:self.avPlayerViewController.view
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0];
NSLayoutConstraint *leading = [NSLayoutConstraint constraintWithItem:self.avPlayerViewController.view
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0];
[self.view addConstraint:height];
[self.view addConstraint:width];
[self.view addConstraint:top];
[self.view addConstraint:leading];
[self.avPlayerViewController didMoveToParentViewController:self];
[self setupBreakButtonWithWidth:225 height:50 xCoordinate:100 yCoordinate:850];
[self setupBreakButtonWithWidth:225 height:50 xCoordinate:335 yCoordinate:850];
[self setupBreakButtonWithWidth:225 height:50 xCoordinate:570 yCoordinate:850];
[self setupBreakButtonWithWidth:225 height:50 xCoordinate:805 yCoordinate:850];
[self setupFocusGuide];
[self createBannerAdView];
[self resetUpNextState];
}
As you can see, I'm just adding buttons to the bottom of the screen (above the seek bar) and then I setup the focus guide and the preferredFocusEnvironments:
- (NSArray<id<UIFocusEnvironment>> *)preferredFocusEnvironments {
NSLog(#"**** BMNativeVideoPlayerViewController -> preferredFocusEnvironments");
return #[self.skipBreakButton];
}
- (void)setupFocusGuide
{
NSLog(#"**** BMNativeVideoPlayerViewController -> setupFocusGuide");
// allocate focus guide and add it to the view
self.focusGuide = [[UIFocusGuide alloc] init];
[self.view addLayoutGuide:self.focusGuide];
// define constraints
[self.focusGuide.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = YES;
[self.focusGuide.rightAnchor constraintEqualToAnchor:self.view.rightAnchor].active = YES;
[self.focusGuide.topAnchor constraintEqualToAnchor:_skipBreakButton.topAnchor].active = YES;
[self.focusGuide.bottomAnchor constraintEqualToAnchor:_skipBreakButton.bottomAnchor].active = YES;
// select the default focusable view
self.focusGuide.preferredFocusEnvironments = #[self.skipBreakButton];
}
Here is the method where the button is configured, along with the button handler method and the didUpdateFocusInContext method (although I don't know if it's doing what it's supposed to):
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
self.focusGuide.preferredFocusEnvironments = #[self.skipBreakButton];
}
- (void)setupBreakButtonWithWidth:(CGFloat)width height:(CGFloat)height xCoordinate:(CGFloat)x yCoordinate:(CGFloat)y
{
NSLog(#"**** BMNativeVideoPlayerViewController -> setupBreakButton");
_skipBreakButton = [[UIButton alloc] init];
_skipBreakButton.backgroundColor = [UIColor whiteColor];
[self.skipBreakButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
_skipBreakButton.alpha = 1.0f;
_skipBreakButton.layer.cornerRadius = 3;
_skipBreakButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.avPlayerViewController.contentOverlayView addSubview:_skipBreakButton];
[self.avPlayerViewController.contentOverlayView bringSubviewToFront:_skipBreakButton];
NSLayoutConstraint *xConstraint = [NSLayoutConstraint
constraintWithItem:_skipBreakButton attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual toItem:self.view attribute:
NSLayoutAttributeLeading multiplier:1.0 constant:x];
NSLayoutConstraint *yConstraint = [NSLayoutConstraint
constraintWithItem:_skipBreakButton attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual toItem:self.view attribute:
NSLayoutAttributeTop multiplier:1.0f constant:y];
NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:_skipBreakButton
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:width];
NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:_skipBreakButton
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:height];
[self.view addConstraints:#[xConstraint, yConstraint, widthConstraint, heightConstraint]];
[self.skipBreakButton setUserInteractionEnabled:YES];
[self.skipBreakButton setTitle:#"Skip Intro" forState:UIControlStateNormal];
[self.skipBreakButton.titleLabel setFont:[UIFont fontWithName:#"Helvetica-Bold" size:26.0]];
NSLog(#"**** Can the skip break button be focused? %d", self.skipBreakButton.canBecomeFocused);
[self.skipBreakButton addTarget:self action:#selector(skipBreakButtonClicked:) forControlEvents:UIControlEventPrimaryActionTriggered];
}
- (void)skipBreakButtonClicked:(UIButton *) sender {
NSLog(#"**** BMNativeVideoPlayerViewController -> skipBreakButtonClicked");
}
I cannot click on any of these buttons. I need help to figure out how to get it to work.
It seems that a view is on top of this button but I'm not sure how to fix it:
(lldb) po [UIFocusDebugger checkFocusabilityForItem: 0x7ff86fe60510]
The following issues were found that would prevent this item from being focusable:
- ISSUE: One or more ancestors have issues that may be preventing this item from being focusable. Details:
<_AVPlayerViewControllerContainerView 0x7ff86fc29eb0>:
- ISSUE: This view returns YES from -canBecomeFocused, which will prevent its subviews from being focusable.
Please advise?
AVPlayer view controller steals all of the view inputs when it is full screen. In my experience what you have to do is keep the player 1 pixel smaller than the full screen when you have your custom elements and then when your custom elements aren't on the screen you can make it full size. If you set breakpoints in your focus override code you will see that it doesn't hit and this is because Apple is controlling the inputs on their AVPlayerViewController.
If you try the above it should resolve the issue.
You could achieve this behaviour by presenting a modal view controller (with a skip button on it) as overlay over the context of the player.
You can then dismiss the modal view controller after a few seconds (as done in the example below), or programatically if you know when the intro actually finishes.
import UIKit
import AVFoundation
import AVKit
class ThePlayerViewController: AVPlayerViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
play(stream: URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8")!)
presentSkipOverlay()
}
// MARK: - Private
private func play(stream: URL) {
let asset = AVAsset(url: stream)
let playetItem = AVPlayerItem(asset: asset)
player = AVPlayer(playerItem: playetItem)
player?.play()
}
private func presentSkipOverlay() {
let skipOverlayViewController = SkipOverlayViewController()
skipOverlayViewController.onSkip = {
[weak self] in
// Skip the intro here
self?.player?.seek(to: CMTime(seconds: 60.0, preferredTimescale: 1))
}
skipOverlayViewController.modalPresentationStyle = .overCurrentContext
skipOverlayViewController.accessibilityViewIsModal = true
present(skipOverlayViewController, animated: true, completion: {
// Dismiss the overlay automatically after 3 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
skipOverlayViewController.dismiss(animated: true, completion: nil)
}
})
}
}
And this would be a naive implementation of the Overlay view controller:
final class SkipOverlayViewController: UIViewController {
var onSkip: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
setUpView()
}
// MARK: - Private
private lazy var skipButton: UIButton = {
let skipButton = UIButton()
skipButton.backgroundColor = .white
skipButton.setTitleColor(.black, for: .normal)
skipButton.setTitle("Skip Intro", for: .normal)
skipButton.addTarget(self, action: #selector(skipButtonWasPressed), for: .primaryActionTriggered)
return skipButton
}()
private func setUpView() {
view.addSubview(skipButton)
skipButton.translatesAutoresizingMaskIntoConstraints = false
skipButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
skipButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -200).isActive = true
skipButton.widthAnchor.constraint(equalToConstant: 225).isActive = true
}
// MARK: - Actions
#objc
func skipButtonWasPressed() {
print("Skip button was Pressed")
onSkip?()
}
}
And this is the result:
I have used autolayout in various implementations for UITableViewCell, the approach being let the intrinsic size define the size and which in turn will provide height for tableview rows.
Strangely but targeting iOS7 and above with Autolayout in UITableViewCell isn't working as desired.
To fix one of the other issues where tableview cells became squeezed (on iOS8 and above) I added following check
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_1 ){
self.tblvChatList.rowHeight = UITableViewAutomaticDimension;
self.tblvChatList.estimatedRowHeight = 44;
}
The only other tableview methods that I have used are
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if ([self.arrDataSource count] > ZERO_VALUE) {
self.tblvChatList.backgroundView = nil;
self.tblvChatList.separatorStyle = UITableViewCellSeparatorStyleNone;
return [self.arrDataSource count];
}
else {
// Display a message when the table is empty
if (!viewTableBackground) {
self.viewTableBackground = [[UIView alloc] initWithFrame:self.tblvChatList.bounds];
UIImageView* imgConChatLogo = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"IconConChatTheme"]];
[imgConChatLogo setContentMode:UIViewContentModeCenter];
[imgConChatLogo setTranslatesAutoresizingMaskIntoConstraints:NO];
UILabel* lblNoListDataError = [[UILabel alloc] initWithFrame:CGRectMake(ZERO_VALUE, ZERO_VALUE,
(2*self.view.bounds.size.width)/3, self.view.bounds.size.height)];
lblNoListDataError.text = NSLocalizedString(#"STR_WRITE_SOMETHING_TO_MATCH",
#"No data is currently available. Please pull down to refresh.");
[lblNoListDataError setTranslatesAutoresizingMaskIntoConstraints:NO];
lblNoListDataError.textColor = [UIColor lightGrayColor];
lblNoListDataError.numberOfLines = ZERO_VALUE;
lblNoListDataError.textAlignment = NSTextAlignmentCenter;
lblNoListDataError.font = [Theme fontForRegularBody];
[lblNoListDataError sizeToFit];
[viewTableBackground addSubview:lblNoListDataError];
[viewTableBackground addSubview:imgConChatLogo];
[viewTableBackground addConstraint:
[NSLayoutConstraint constraintWithItem:imgConChatLogo
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:viewTableBackground
attribute:NSLayoutAttributeCenterX
multiplier:1
constant:0]];
[viewTableBackground addConstraint:
[NSLayoutConstraint constraintWithItem:lblNoListDataError
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:viewTableBackground
attribute:NSLayoutAttributeCenterX
multiplier:1
constant:0]];
[viewTableBackground addConstraint:
[NSLayoutConstraint constraintWithItem:imgConChatLogo
attribute:NSLayoutAttributeBaseline
relatedBy:NSLayoutRelationEqual
toItem:viewTableBackground
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:-10]];
[viewTableBackground addConstraint:
[NSLayoutConstraint constraintWithItem:lblNoListDataError
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:viewTableBackground
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:10]];
}
self.tblvChatList.backgroundView = self.viewTableBackground;
self.tblvChatList.separatorStyle = UITableViewCellSeparatorStyleNone;
}
return ZERO_VALUE;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell* cell = nil;
NSUInteger userId = [[NSUserDefaults standardUserDefaults] integerForKey:#"USER_ID"];
ChatMessage* message = (ChatMessage*)[self.arrDataSource objectAtIndex:indexPath.row];
if (userId == [message messageSenderId]) {
static NSString* reuseIdentifierReceiverCell = #"ChatReceiverCustomCell";
cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifierReceiverCell forIndexPath:indexPath];
[(ChatReceiverCell*)cell showMessage:message];
}
else {
static NSString* reuseIdentifierSenderCell = #"ChatSenderCustomCell";
cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifierSenderCell forIndexPath:indexPath];
// Configure the cell...
[(ChatSenderCell*)cell showMessage:message];
}
return cell;
}
I haven't used either estimateHeightForRowAtIndexPath: or heightForRowAtIndexPath: and let the content decide the height for tableviewRow.
Issue:
Why the tableView cells are all squeezed up when running on iOS7, iOS7.1 (Simulator and devices) but show perfectly on iOS8 and above for same content for the same content.
To fix this squeezing, If I implement estimatedHeightForRowAtIndexPath: then not implementing heightForRowAtIndexPath: results in a crash on iOS7. Is there are way to let tableview infer the row height from the autolayout system itself (As derived from intrinsic content size of UILabel with extra padding kept in consideration as I have added required margins) ?
The autolayout constrains for UILabel inside the cell are as follows
I want to have custom UILabel inside UIPickerView which is right aligned and with width = half of the screen width:
How to position everything with NSConstraints? I had some problems since UILabel was not attached to super view when I have a chance to modify it.
I managed to get it working but it is not done with NSConstraints and is probably wrong (I was able to set up frame if I embeded UILabel inside another view). Any suggestions how to do it properly?
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
CGFloat width = [UIScreen mainScreen].bounds.size.width;
UIView * newView = [[UIView alloc] init];
UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(0, 0.0f, width / 2 + 18, [pickerView rowSizeForComponent:component].height)];
lbl.text = [self.datasource objectAtIndex:row];
lbl.textAlignment = NSTextAlignmentRight;
lbl.font=[UIFont CPGeneralFontWithSize:22];
[newView addSubview:lbl];
return newView;
}
I don't think there's anything wrong with the way you're adding your label. The view returned by viewForRow:forComponent: is the same width as the picker view by default, so adding the label as a subview is the correct way to get the look you want. If you want to add the label using constraints, you can do it like this,
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
UIView * newView = [[UIView alloc] init];
UILabel *lbl = [[UILabel alloc] init];
lbl.translatesAutoresizingMaskIntoConstraints = NO;
lbl.backgroundColor = [UIColor redColor];
lbl.text = self.datasource[row];
lbl.textAlignment = NSTextAlignmentRight;
lbl.font=[UIFont PGeneralFontWithSize:22];
[newView addSubview:lbl];
[newView addConstraint:[NSLayoutConstraint constraintWithItem:lbl attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:newView attribute:NSLayoutAttributeLeft multiplier:1 constant:0]];
[newView addConstraint:[NSLayoutConstraint constraintWithItem:lbl attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:newView attribute:NSLayoutAttributeWidth multiplier:0.5 constant:0]];
[newView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[lbl]|" options:0 metrics:nil views:#{#"lbl":lbl}]];
return newView;
}
When my table view is empty, I want to add a simple label into the tableview footer to display a "no content" message. It was working fine when I was setting the frames of the UILabel and the UIView that I used for footerView, but now I'm trying to convert to using auto-layout, and I can't get it to work.
Here's what I'm doing:
// Create the footer
UIView *tempFooter = [[UIView alloc] init];
UILabel *atext = [[UILabel alloc] init];
tempFooter.translatesAutoresizingMaskIntoConstraints = NO;
atext.translatesAutoresizingMaskIntoConstraints = NO;
atext.numberOfLines = 0;
[atext setText:#"Add Some Text"];
atext.textAlignment = NSTextAlignmentCenter;
atext.font = [UIFont italicSystemFontOfSize:14];
atext.textColor = [UIColor grayColor];
atext.backgroundColor = [UIColor clearColor];
atext.lineBreakMode = NSLineBreakByWordWrapping;
// add label to footer view, and add constraints
[tempFooter addSubview:atext];
NSLayoutConstraint *tvatt1 = [NSLayoutConstraint constraintWithItem:tempFooter
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:atext attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:10.0];
NSLayoutConstraint *tvatt2 = [NSLayoutConstraint constraintWithItem:tempFooter
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:atext
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:10.0];
NSLayoutConstraint *tvatt3 = [NSLayoutConstraint constraintWithItem:tempFooter
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:atext
attribute:NSLayoutAttributeTop
multiplier:5.0
constant:0];
NSLayoutConstraint *tvatt4 = [NSLayoutConstraint constraintWithItem:tempFooter
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:atext
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant: 0];
[tempFooter addConstraints: #[tvatt1, tvatt2, tvatt3, tvatt4]];
// set the footerview
self.tview.tableFooterView = tempFooter;
When I run this, I get a crash:
2014-09-08 19:57:16.594 SimpleList[49442:60b] * Assertion failure in
-[UITableView layoutSublayersOfLayer:], /SourceCache/UIKit_Sim/UIKit-2935.137/UIView.m:8794 2014-09-08
19:57:21.073 SimpleList[49442:60b] * Terminating app due to uncaught
exception 'NSInternalInconsistencyException', reason: 'Auto Layout
still required after executing -layoutSubviews. UITableView's
implementation of -layoutSubviews needs to call super
I'm not sure what I'm doing wrong. I've also tried setting constraints for the self.tview.tableFooterView wrt the tempFooter (pinning against all 4 sides) but that doesn't change the crash. I also call:
[self.tview.tableFooterView layoutSubviews];
but that doesn't help either.
Any ideas what I'm doing wrong?
You shouldn't set translatesAutoresizingMaskIntoConstraints to NO for table views, their cells, or table header and footer views (or for your controller's self.view for that matter). It's ok to use constraints for views inside those views (the content view of cells or the header and footer views). I added a simplified version of your code to viewDidLoad, and it worked as expected. Note that you need to give the footer view a height and an x position (the y position and the width are ignored),
UIView *tempFooter = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 50)];
UILabel *atext = [[UILabel alloc] init];
atext.translatesAutoresizingMaskIntoConstraints = NO;
atext.numberOfLines = 0;
[atext setText:#"Add Some Text"];
atext.textAlignment = NSTextAlignmentCenter;
atext.font = [UIFont italicSystemFontOfSize:14];
atext.textColor = [UIColor grayColor];
atext.backgroundColor = [UIColor clearColor];
atext.lineBreakMode = NSLineBreakByWordWrapping;
[tempFooter addSubview:atext];
NSDictionary *dict = NSDictionaryOfVariableBindings(atext);
[tempFooter addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-10-[atext]-10-|" options:0 metrics:nil views:dict]];
[tempFooter addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[atext]|" options:0 metrics:nil views:dict]];
self.tableView.tableFooterView = tempFooter;
I'm not sure what you were trying to accomplish with your constraint, tvatt3. The multiplier (you have 5) doesn't do anything with a top or left constraint since the value of those attributes is 0. I used the visual constraints, rather than the method you used, but when I used your code to add the constraints, it worked also .
Interestingly, changing the Autoresizing mask in the xib solved my problem.
Constraints not working:
Working:
I was able to make dynamically auto layout table footer width and height by implementing #rdelmar solution for constraints and adding these lines below. Same solution applies to table header view.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let tableFooter = tableView.tableFooterView {
let maxSize = CGSize(width: self.tableView.bounds.size.width,
height: CGFloat.greatestFiniteMagnitude)
let size = tableFooter.systemLayoutSizeFitting(maxSize)
let height = size.height
var headerFrame = tableFooter.frame
// If we don't have this check, viewDidLayoutSubviews() will get
// repeatedly, causing the app to hang.
if height != headerFrame.size.height {
headerFrame.size.height = height
tableFooter.frame = headerFrame
tableView.tableFooterView = tableFooter
}
}
}
P. S. I prefer Interface Builder for constraints setup
hope this will help you
I am not sure of this but it help me in my project, first add every thing in contentView (all the subview and the constraint)
[self.contentView addSubview:atext]
and then comes to code part
NSLayoutConstraint *tvatt1 = [NSLayoutConstraint constraintWithItem:tempFooter
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:atext attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:10.0];
code saying for left attribute
temfooter(must be equal) = 1.0*atext +10.0
i.e
temfooter must be equal to the modified atext , so need to make sure that your constraints are not wrong.
This is my first post to Stack Overflow, and I'm a iOS beginner, so please bear with me!
I have an example app where I have three UIViews (headerView, scrollViewContainer, and bodyView) in a parent UIView (topView), and all of these views are created in code. The topView is added to a UIScrollView (pageScrollView) created in storyboard.
pageScrollView is filling the full screen of an iPhone, and I used Autolayout. The app only contains the ViewController.h and companioning .m-file seen below, plus Appdelegate.x. I think I used the single view application template to start with. I'm using iOS 6 and Xcode 4.6, but I also tried 4.5.
I've tried to remove as much as possible of other code that's irrelevant to this problem.
The problem:
When the app starts it shows all its views correctly, and the scrollView allows to view all three views as intended. But after rotating to landscape, the scrollView somehow offsets the content. For example: Stay at top and rotate = content looks OK, but scroll down a bit and rotate makes the top of the content not to show.
What I have tried: I've searched the net for help, but I haven't found anything that seemed to help me. I've added logging of various data like origin, and contentSize, and also tried to set some of them but without any success. I also used 'po [[UIWindow keyWindow] _autolayoutTrace]' to ensure that the constraints are OK.
I can't see what I'm doing wrong. Are there any obvious things missing in my code?
Thanks in advance!
Here's the ViewController.m:
#import "ViewController.h"
#interface ViewController ()
{
UIView *topView;
UIView *headerView;
UIView *bodyView;
UIView *scrollViewContainer;
UIInterfaceOrientation newOrientation;
CGFloat bodyViewHeight;
CGSize newBounds;
float pictureScrollHeight;
}
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
newBounds = [self sizeInOrientation:[UIApplication sharedApplication].statusBarOrientation];
newOrientation = [UIApplication sharedApplication].statusBarOrientation;
bodyViewHeight = 1200; // The height of the body view varies in size depending on orientation
self.pageScrollView.translatesAutoresizingMaskIntoConstraints = NO;
topView = [[UIView alloc] init];
[topView setBackgroundColor:[UIColor clearColor]];
topView.translatesAutoresizingMaskIntoConstraints = NO;
[self.pageScrollView addSubview:topView];
headerView = [[UIView alloc] init];
[headerView setBackgroundColor:[UIColor redColor]];
headerView.translatesAutoresizingMaskIntoConstraints = NO;
[topView addSubview:headerView];
scrollViewContainer = [[UIView alloc] init];
[scrollViewContainer setBackgroundColor:[UIColor blueColor]];
scrollViewContainer.translatesAutoresizingMaskIntoConstraints = NO;
[topView addSubview:scrollViewContainer];
bodyView = [[UIView alloc] init];
[bodyView setBackgroundColor:[UIColor greenColor]];
bodyView.translatesAutoresizingMaskIntoConstraints = NO;
[topView addSubview:bodyView];
[self updateViewConstraints];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}
- (void)updateViewConstraints
{
[super updateViewConstraints];
// Remove old constraints
[self.view removeConstraints:self.view.constraints];
[self.pageScrollView removeConstraints:self.pageScrollView.constraints];
[topView removeConstraints:topView.constraints];
[scrollViewContainer removeConstraints:scrollViewContainer.constraints];
if ((newOrientation == UIDeviceOrientationLandscapeLeft) || (newOrientation == UIDeviceOrientationLandscapeRight)) {
pictureScrollHeight = 300;
} else {
pictureScrollHeight = 203;
}
[headerView setNeedsDisplay];
[bodyView setNeedsDisplay];
CGFloat topViewHeight = bodyViewHeight + 55 + pictureScrollHeight;
//self.pageScrollView = _pageScrollView
NSDictionary *viewsDict = NSDictionaryOfVariableBindings(_pageScrollView, topView, headerView, bodyView, scrollViewContainer);
NSDictionary *metricsDict = #{#"topViewHeight": [NSNumber numberWithFloat:topViewHeight],
#"newBoundsWidth": [NSNumber numberWithFloat:newBounds.width],
#"pictureScrollHeight": [NSNumber numberWithFloat:pictureScrollHeight],
#"bodyViewHeight": [NSNumber numberWithFloat:bodyViewHeight]};
// pageScrollView - child to self.view
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[_pageScrollView]-0-|" options:0 metrics:nil views:viewsDict]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[_pageScrollView]-0-|" options:0 metrics:nil views:viewsDict]];
// topView - child to pageScrollView
[self.pageScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[topView(newBoundsWidth)]-0-|" options:0 metrics:metricsDict views:viewsDict]];
[self.pageScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[topView(topViewHeight)]-0-|" options:0 metrics:metricsDict views:viewsDict]];
// headerView - child to topView
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[headerView]-0-|" options:0 metrics:nil views:viewsDict]];
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[headerView(55.0)]" options:0 metrics:nil views:viewsDict]];
// scrollViewContainer - child to topView
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[scrollViewContainer]-0-|" options:0 metrics:nil views:viewsDict]];
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[headerView]-0-[scrollViewContainer(pictureScrollHeight)]" options:0 metrics:metricsDict views:viewsDict]];
// bodyView - child to topView
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[bodyView]-0-|" options:0 metrics:nil views:viewsDict]];
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[scrollViewContainer]-0-[bodyView(bodyViewHeight)]-0-|" options:0 metrics:metricsDict views:viewsDict]];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
newOrientation = toInterfaceOrientation;
newBounds = [self sizeInOrientation:toInterfaceOrientation];
}
-(CGSize) sizeInOrientation:(UIInterfaceOrientation)orientation
{
CGSize size = [UIScreen mainScreen].bounds.size;
UIApplication *application = [UIApplication sharedApplication];
if (UIInterfaceOrientationIsLandscape(orientation))
{
size = CGSizeMake(size.height, size.width);
}
if (application.statusBarHidden == NO)
{
size.height -= MIN(application.statusBarFrame.size.width, application.statusBarFrame.size.height);
}
return size;
}
#end
First of all you do not need to recreate all constraints in -updateViewConstraints method. You need to just update them in this place.
To achieve your aim do the following:
Create your constraints only once. For example in method -setupConstraints. And keep reference to those that need to be updated. See code below.
In method -updateViewConstraints just update topView height and width constraints and height for scrollViewContainer.
Here's the ViewController.m should look like:
#import "ViewController.h"
#interface ViewController ()
#property (nonatomic, strong) IBOutlet UIScrollView* pageScrollView;
#property (nonatomic, strong) NSLayoutConstraint* pictureHeightConstraint;
#property (nonatomic, strong) NSLayoutConstraint* topViewWidthConstraint;
#property (nonatomic, strong) NSLayoutConstraint* topViewHeightConstraint;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
newBounds = [self sizeInOrientation:[UIApplication sharedApplication].statusBarOrientation];
newOrientation = [UIApplication sharedApplication].statusBarOrientation;
bodyViewHeight = 1200; // The height of the body view varies in size depending on orientation
self.pageScrollView.translatesAutoresizingMaskIntoConstraints = NO;
topView = [[UIView alloc] init];
[topView setBackgroundColor:[UIColor clearColor]];
topView.translatesAutoresizingMaskIntoConstraints = NO;
[self.pageScrollView addSubview:topView];
headerView = [[UIView alloc] init];
[headerView setBackgroundColor:[UIColor redColor]];
headerView.translatesAutoresizingMaskIntoConstraints = NO;
[topView addSubview:headerView];
scrollViewContainer = [[UIView alloc] init];
[scrollViewContainer setBackgroundColor:[UIColor blueColor]];
scrollViewContainer.translatesAutoresizingMaskIntoConstraints = NO;
[topView addSubview:scrollViewContainer];
bodyView = [[UIView alloc] init];
[bodyView setBackgroundColor:[UIColor greenColor]];
bodyView.translatesAutoresizingMaskIntoConstraints = NO;
[topView addSubview:bodyView];
[self setupConstraints];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}
- (void)setupConstraints {
// Remove old constraints
[self.view removeConstraints:self.view.constraints];
[self.pageScrollView removeConstraints:self.pageScrollView.constraints];
[topView removeConstraints:topView.constraints];
[scrollViewContainer removeConstraints:scrollViewContainer.constraints];
if ((newOrientation == UIDeviceOrientationLandscapeLeft) || (newOrientation == UIDeviceOrientationLandscapeRight)) {
pictureScrollHeight = 300;
} else {
pictureScrollHeight = 203;
}
[headerView setNeedsDisplay];
[bodyView setNeedsDisplay];
CGFloat topViewHeight = bodyViewHeight + 55 + pictureScrollHeight;
//self.pageScrollView = _pageScrollView
NSDictionary *viewsDict = NSDictionaryOfVariableBindings(_pageScrollView, topView, headerView, bodyView, scrollViewContainer);
// pageScrollView - child to self.view
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[_pageScrollView]-0-|" options:0 metrics:nil views:viewsDict]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0- [_pageScrollView]-0-|" options:0 metrics:nil views:viewsDict]];
// topView - child to pageScrollView
[self.pageScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[topView]-0-|" options:0 metrics:nil views:viewsDict]];
[self.pageScrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[topView]-0-|" options:0 metrics:nil views:viewsDict]];
NSLayoutConstraint* topViewWidthConstraint = [NSLayoutConstraint constraintWithItem:topView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:newBounds.width];
self.topViewWidthConstraint = topViewWidthConstraint;
[topView addConstraint:self.topViewWidthConstraint];
NSLayoutConstraint* topViewHeightConstraint = [NSLayoutConstraint constraintWithItem:topView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:topViewHeight];
self.topViewHeightConstraint = topViewHeightConstraint;
[topView addConstraint:self.topViewHeightConstraint];
// headerView - child to topView
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[headerView]-0-|" options:0 metrics:nil views:viewsDict]];
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[headerView(55.0)]" options:0 metrics:nil views:viewsDict]];
// scrollViewContainer - child to topView
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[scrollViewContainer]-0-|" options:0 metrics:nil views:viewsDict]];
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[headerView]-0-[scrollViewContainer]" options:0 metrics:nil views:viewsDict]];
NSLayoutConstraint* pictureHeightConstraint = [NSLayoutConstraint constraintWithItem:scrollViewContainer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:pictureScrollHeight];
self.pictureHeightConstraint = pictureHeightConstraint;
[scrollViewContainer addConstraint:self.pictureHeightConstraint];
// bodyView - child to topView
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[bodyView]-0-|" options:0 metrics:nil views:viewsDict]];
[topView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V: [scrollViewContainer]-0-[bodyView]-0-|" options:0 metrics:nil views:viewsDict]];
}
- (void)updateViewConstraints
{
[super updateViewConstraints];
if ((newOrientation == UIDeviceOrientationLandscapeLeft) || (newOrientation == UIDeviceOrientationLandscapeRight)) {
pictureScrollHeight = 300;
} else {
pictureScrollHeight = 203;
}
CGFloat topViewHeight = bodyViewHeight + 55 + pictureScrollHeight;
self.pictureHeightConstraint.constant = pictureScrollHeight;
self.topViewHeightConstraint.constant = topViewHeight;
self.topViewWidthConstraint.constant = newBounds.width;
[self.pageScrollView setNeedsUpdateConstraints];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
newBounds = [self sizeInOrientation:toInterfaceOrientation];
}
-(CGSize)sizeInOrientation:(UIInterfaceOrientation)orientation
{
CGSize size = [UIScreen mainScreen].bounds.size;
UIApplication *application = [UIApplication sharedApplication];
if (UIInterfaceOrientationIsLandscape(orientation))
{
size = CGSizeMake(size.height, size.width);
}
if (application.statusBarHidden == NO)
{
size.height -= MIN(application.statusBarFrame.size.width, application.statusBarFrame.size.height);
}
return size;
}
#end
ScrollView calculates its content size automatically depending on subviews constraints added into it. Off course it works only in autolayout enviroument.
Check the auto-resizing masks.
[self.pageScrollView setAutoresizingMask:UIViewAutoresizingFlexibleHeight];
It looks like that you are NOT doing anything with the newBounds in willRotateToInterfaceOrientation. Shouldn't you call your updateView method after getting the new bounds.