I've found some questions and answers to remove offset of UITableViews in ios7, namely this one here
How to fix UITableView separator on iOS 7?
I was wondering if anyone had come across the correct functions to remove inset margins. Something similar to this answer in objective-c
if ([tableView respondsToSelector:#selector(setSeparatorInset:)]) {
[tableView setSeparatorInset:UIEdgeInsetsZero];
}
You can just set the property: tableView.separatorInset = UIEdgeInsetsZero
Just like the Objective-C example, but converted to swift. I had some trouble myself. This code works in a UITableView if you were doing it in a UITableViewController you would substitute self.tableView for self:
// iOS 7
if(self.respondsToSelector(Selector("setSeparatorInset:"))){
self.separatorInset = UIEdgeInsetsZero
}
// iOS 8
if(self.respondsToSelector(Selector("setLayoutMargins:"))){
self.layoutMargins = UIEdgeInsetsZero;
}
And for the cell (iOS 8 only) put the code below in the following function:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
get the cell, and set the following property:
// iOS 8
if(cell.respondsToSelector(Selector("setLayoutMargins:"))){
cell.layoutMargins = UIEdgeInsetsZero;
}
Put the following lines in viewDidLoad()
tableView.layoutMargins = UIEdgeInsetsZero
tableView.separatorInset = UIEdgeInsetsZero
Now look for your cellForRowAtIndexPath method and add this:
cell.layoutMargins = UIEdgeInsetsZero
nowadays ".zero" syntax...
tableView.layoutMargins = UIEdgeInsets.zero
tableView.separatorInset = UIEdgeInsets.zero
cell.layoutMargins = UIEdgeInsets.zero
for Swift 3 just type:
tableView.separatorInset = .zero
You can do this via the console by modifying the "Default Insets" to be "Custom Insets"
Inside the table view cell click the slider icon
Under Table View Cell go to where Separator says "Default Insets"
Click the dropdown menu and select "Custom Insets"
Choose your left and right number (I have mine set to 0 for edge to edge divider)
Image showing default settings
Imgage showing custom settings
Related
I am trying to make a UITableView line up with the height sizing of paragraphs in a UITextView. Example: The timestamps to the left are what I am trying to do. I changed my code to use UIView's instead of TVcells to see what was wrong and you can see the orange view is overlapping the cyan one, meaning that the views don't actually line up but they overlap. NOTE: I am wanting to use the TableView not UIView's I am having trouble understanding how the text heights are calculated in iOS. I am using the below code to get the heights of each paragraph:
let liveParagraphView = textView.selectionRects(for: txtRange).reduce(CGRect.null) { $0.union($1.rect) }
After this I calculate the height of each then feed that into my UITableView heightForRowAt
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let models = getParagraphModel()
let height = models[indexPath.row].height
let finalHeight = models[indexPath.row].height
let heightValue = finalHeight
return CGFloat(heightValue);
}
Every line has different height values but even when using these values it's not lining up. The problem seems to be that every line calculates a Y Position which is not directly under the line before it. It's ON TOP OF!! Resulting in the UITableView not being alined when new cells are added and that 'overlay' of the selectionRects isn't taken into account. Am I correct by this? How could I go about achieving this?
Swift 5
Firstly you should set your textView (which is in the cell) dynamic height:
textView.translatesAutoresizingMaskIntoConstraints = true
textView.sizeToFit()
textView.isScrollEnabled = false
Then calculate your textView's number of lines in textDidChange etc. for update tableView's layout.
let numOfLines = (yourTextView.contentSize.height / yourTextView.font.lineHeight) as? Int
When textView's text one line down you should update tableView layout:
tableView.beginUpdates()
tableView.endUpdates()
And then you should set your tableView cell's intrinsicContentSize for dynamic rowHeight:
Set your cell's (which is the contains textView) layout without static height,
Set your tableView's rowHeight and estimatedRowHeight:
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 44 // whatever you want
So now you have tableView cell with dynamicHeight
This is the code I used to hide the separator for a single UITableViewCell prior to iOS 11:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 0) {
// Remove separator inset
if ([cell respondsToSelector:#selector(setSeparatorInset:)]) {
[cell setSeparatorInset:UIEdgeInsetsMake(0, tableView.frame.size.width, 0, 0)];
}
// Prevent the cell from inheriting the Table View's margin settings
if ([cell respondsToSelector:#selector(setPreservesSuperviewLayoutMargins:)]) {
[cell setPreservesSuperviewLayoutMargins:NO];
}
// Explictly set your cell's layout margins
if ([cell respondsToSelector:#selector(setLayoutMargins:)]) {
[cell setLayoutMargins:UIEdgeInsetsMake(0, tableView.frame.size.width, 0, 0)];
}
}
}
In this example, the separator is hidden for the first row in every section. I don't want to get rid of the separators completely - only for certain rows.
In iOS 11, the above code does not work. The content of the cell is pushed completely to the right.
Is there a way to accomplish the task of hiding the separator for a single UITableViewCell in iOS 11?
Let me clarify in advance that I do know that I can hide the separator for the entire UITableView with the following code (to hopefully avoid answers instructing me to do this):
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
EDIT: Also to clarify after a comment below, the code does exactly the same thing if I include the setSeparatorInset line at all. So even with only that one line, the content of the cell is pushed all the way to the right.
If you are not keen on adding a custom separator to your UITableViewCell I can show you yet another workaround to consider.
How it works
Because the color of the separator is defined on the UITableView level there is no clear way to change it per UITableViewCell instance. It was not intended by Apple and the only thing you can do is to hack it.
The first thing you need is to get access to the separator view. You can do it with this small extension.
extension UITableViewCell {
var separatorView: UIView? {
return subviews .min { $0.frame.size.height < $1.frame.size.height }
}
}
When you have an access to the separator view, you have to configure your UITableView appropriately. First, set the global color of all separators to .clear (but don't disable them!)
override func viewDidLoad() {
super.viewDidLoad()
tableView.separatorColor = .clear
}
Next, set the separator color for each cell. You can set a different color for each of them, depends on you.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SeparatorCell", for: indexPath)
cell.separatorView?.backgroundColor = .red
return cell
}
Finally, for every first row in the section, set the separator color to .clear.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
cell.separatorView?.backgroundColor = .clear
}
}
Why it works
First, let's consider the structure of the UITableViewCell. If you print out the subviews of your cell you will see the following output.
<UITableViewCellContentView: 0x7ff77e604f50; frame = (0 0; 328 43.6667); opaque = NO; gestureRecognizers = <NSArray: 0x608000058d50>; layer = <CALayer: 0x60400022a660>>
<_UITableViewCellSeparatorView: 0x7ff77e4010c0; frame = (15 43.5; 360 0.5); layer = <CALayer: 0x608000223740>>
<UIButton: 0x7ff77e403b80; frame = (0 0; 22 22); opaque = NO; layer = <CALayer: 0x608000222500>>
As you can see there is a view which holds the content, the separator, and the accessory button. From this perspective, you only need to access the separator view and modify it's background. Unfortunately, it's not so easy.
Let's take a look at the same UITableViewCell in the view debugger. As you can see, there are two separator views. You need to access the bottom one which is not present when the willDisplay: is called. This is where the second hacky part comes to play.
When you will inspect these two elements, you will see that the first (from the top) has a background color set to nil and the second has a background color set to the value you have specified for entire UITableView. In this case, the separator with the color covers the separator without the color.
To solve the issue we have to "reverse" the situation. We can set the color of all separators to .clear which will uncover the one we have an access to. Finally, we can set the background color of the accessible separator to what is desired.
Begin by hiding all separators via tableView.separatorStyle = .none. Then modify your UITableViewCell subclass to something as follows:
class Cell: UITableViewCell {
var separatorLine: UIView?
...
}
Add the following to the method body of tableView(_:cellForRowAt:):
if cell.separatorLine == nil {
// Create the line.
let singleLine = UIView()
singleLine.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
singleLine.translatesAutoresizingMaskIntoConstraints = false
// Add the line to the cell's content view.
cell.contentView.addSubview(singleLine)
let singleLineConstraints = [singleLine.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 8),
singleLine.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor),
singleLine.topAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -1),
singleLine.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: 0)]
cell.contentView.addConstraints(singleLineConstraints)
cell.separatorLine = singleLine
}
cell.separatorLine?.isHidden = [Boolean which determines if separator should be displayed]
This code is in Swift, so do as you must for the Objective-C translation and make sure to continue your version checking. In my tests I don't need to use the tableView(_:willDisplayCell:forRowAt:) at all, instead everything is in the cellForRowAtIndexPath: method.
Best way IMO is just to add a simple UIView with 1pt height.
I wrote the following protocol which enables you to use it in any UITableViewCell you like:
// Base protocol requirements
protocol SeperatorTableViewCellProtocol: class {
var seperatorView: UIView! {get set}
var hideSeperator: Bool! { get set }
func configureSeperator()
}
// Specify the separator is of a UITableViewCell type and default separator configuration method
extension SeperatorTableViewCellProtocol where Self: UITableViewCell {
func configureSeperator() {
hideSeperator = true
seperatorView = UIView()
seperatorView.backgroundColor = UIColor(named: .WhiteThree)
contentView.insertSubview(seperatorView, at: 0)
// Just constraint seperatorView to contentView
seperatorView.setConstant(edge: .height, value: 1.0)
seperatorView.layoutToSuperview(.bottom)
seperatorView.layoutToSuperview(axis: .horizontally)
seperatorView.isHidden = hideSeperator
}
}
You use it like this:
// Implement the protocol with custom cell
class CustomTableViewCell: UITableViewCell, SeperatorTableViewCellProtocol {
// MARK: SeperatorTableViewCellProtocol impl'
var seperatorView: UIView!
var hideSeperator: Bool! {
didSet {
guard let seperatorView = seperatorView else {
return
}
seperatorView.isHidden = hideSeperator
}
}
override func awakeFromNib() {
super.awakeFromNib()
configureSeperator()
hideSeperator = false
}
}
And that's all. You are able to customize any UITableViewCell subclass to use a separator.
Set separator visibility from tableView:willDisplayCell:forRowAtIndexPath by:
cell.hideSeperator = false / true
I also followed this pattern once. Over the years I adjusted it. Just today I had to remove the directionalLayoutMargins part to be able to make it work. Now My function looks like this:
func adjustCellSeparatorInsets(at indexPath: IndexPath,
for modelCollection: ModelCollection,
numberOfLastSeparatorsToHide: Int) {
guard modelCollection.isInBounds(indexPath) else { return }
let model = modelCollection[indexPath]
var insets = model.separatorInsets
let lastSection = modelCollection[modelCollection.sectionCount - 1]
let shouldHideSeparator = indexPath.section == modelCollection.sectionCount - 1
&& indexPath.row >= lastSection.count - numberOfLastSeparatorsToHide
// Don't show the separator for the last N rows of the last section
if shouldHideSeparator {
insets = NSDirectionalEdgeInsets(top: 0, leading: 9999, bottom: 0, trailing: 0)
}
// removing separator inset
separatorInset = insets.edgeInsets
// prevent the cell from inheriting the tableView's margin settings
preservesSuperviewLayoutMargins = false
}
See this link if you prefer to inspect it on Github.
The PR of the removal with an explanation can be found here.
Actually when i work with UITableView, i always create custom cell class and for separators and usually make my own separator as UIView with height 1 and left and right constraints, in Your case make those steps:
1. Create custom cell.
2. Add UIView as separator.
3. Link this separator to your custom class.
4. Add hideSeparator method to your class.
-(void)hideSeparator{
self.separator.hidden == YES;
}
5. Hide the separator for any cell you want.
Hope that solves your question.
I have recently migrated some code to new iOS 11 beta 5 SDK.
I now get a very confusing behaviour from UITableView. The tableview itself is not that fancy. I have custom cells but in most part it is just for their height.
When I push my view controller with tableview I get an additional animation where cells "scroll up" (or possibly the whole tableview frame is changed) and down along push/pop navigation animation. Please see gif:
I manually create tableview in loadView method and setup auto layout constraints to be equal to leading, trailing, top, bottom of tableview's superview. The superview is root view of view controller.
View controller pushing code is very much standard: self.navigationController?.pushViewController(notifVC, animated: true)
The same code provides normal behaviour on iOS 10.
Could you please point me into direction of what is wrong?
EDIT: I have made a very simple tableview controller and I can reproduce the same behavior there. Code:
class VerySimpleTableViewController : UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = String(indexPath.row)
cell.accessoryType = .disclosureIndicator
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let vc = VerySimpleTableViewController.init(style: .grouped)
self.navigationController?.pushViewController(vc, animated: true)
}
}
EDIT 2: I was able to narrow issue down to my customisation of UINavigationBar. I have a customisation like this:
rootNavController.navigationBar.setBackgroundImage(createFilledImage(withColor: .white, size: 1), for: .default)
where createFilledImage creates square image with given size and color.
If I comment out this line I get back normal behaviour.
I would appreciate any thoughts on this matter.
This is due to UIScrollView's (UITableView is a subclass of UIScrollview) new contentInsetAdjustmentBehavior property, which is set to .automatic by default.
You can override this behavior with the following snippet in the viewDidLoad of any affected controllers:
tableView.contentInsetAdjustmentBehavior = .never
https://developer.apple.com/documentation/uikit/uiscrollview/2902261-contentinsetadjustmentbehavior
In addition to maggy's answer
OBJECTIVE-C
if (#available(iOS 11.0, *)) {
scrollViewForView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
This issue was caused by a bug in iOS 11 where the safeAreaInsets of
the view controller's view were set incorrectly during the navigation
transition, which should be fixed in iOS 11.2. Setting the
contentInsetAdjustmentBehavior to .never isn't a great workaround
because it will likely have other undesirable side effects. If you do
use a workaround you should make sure to remove it for iOS versions >=
11.2
-mentioned by smileyborg (Software Engineer at Apple)
You can edit this behavior at once throughout the application by using NSProxy in for example didFinishLaunchingWithOptions:
if (#available(iOS 11.0, *)) {
[UIScrollView appearance].contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
Here's how I managed to fix this issue while still allowing iOS 11 to set insets automatically. I am using UITableViewController.
Select "Extend edges under top bars" and "Extend edges under opaque bars" in your view controller in storyboard (or programmatically). The safe area insets will prevent your view from going under the top bar.
Check the "Insets to Safe Area" button on your table view in your storyboard. (or tableView.insetsContentViewsToSafeArea = true) - This might not be necessary but it's what I did.
Set the content inset adjustment behavior to "Scrollable Axes" (or tableView.contentInsetAdjustmentBehavior = .scrollableAxes) - .always might also work but I did not test.
One other thing to try if all else fails:
Override viewSafeAreaInsetsDidChange UIViewController method to get the table view to force set the scroll view insets to the safe area insets. This is in conjunction with the 'Never' setting in Maggy's answer.
- (void)viewSafeAreaInsetsDidChange {
[super viewSafeAreaInsetsDidChange];
self.tableView.contentInset = self.view.safeAreaInsets;
}
Note: self.tableView and self.view should be the same thing for UITableViewController
This seems more like a bug than intended behavior. It happens when navigation bar is not translucent or when background image is set.
If you just set contentInsetAdjustmentBehavior to .never, content insets won't be set correctly on iPhone X, e.g. content would go into bottom area, under the scrollbars.
It is necessary to do two things:
1. prevent scrollView animating up on push/pop
2. retain .automatic behaviour because it is needed for iPhone X. Without this e.g. in portrait, content will go below bottom scrollbar.
New simple solution: in XIB: Just add new UIView on top of your main view with top, leading and trailing to superview and height set to 0. You don't have to connect it to other subviews or anything.
Old solution:
Note: If you are using UIScrollView in landscape mode, it still doesn't set horizontal insets correctly(another bug?), so you must pin scrollView's leading/trailing to safeAreaInsets in IB.
Note 2: Solution below also has problem that if tableView is scrolled to the bottom, and you push controller and pop back, it will not be at the bottom anymore.
override func viewDidLoad()
{
super.viewDidLoad()
// This parts gets rid of animation when pushing
if #available(iOS 11, *)
{
self.tableView.contentInsetAdjustmentBehavior = .never
}
}
override func viewDidDisappear(_ animated: Bool)
{
super.viewDidDisappear(animated)
// This parts gets rid of animation when popping
if #available(iOS 11, *)
{
self.tableView.contentInsetAdjustmentBehavior = .never
}
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
// This parts sets correct behaviour(insets are correct on iPhone X)
if #available(iOS 11, *)
{
self.tableView.contentInsetAdjustmentBehavior = .automatic
}
}
I can reproduce the bug for iOS 11.1 but it seems that the bug is fixed since iOS 11.2.
See http://openradar.appspot.com/34465226
please make sure along with above code, add additional code as follows. It solved the problem
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) // print(thisUISBTV.adjustedContentInset)
}
Also if you use tab bar, bottom content inset of the collection view will be zero. For this, put below code in viewDidAppear:
if #available(iOS 11, *) {
tableView.contentInset = self.collectionView.safeAreaInsets
}
In my case this worked (put it in viewDidLoad):
self.navigationController.navigationBar.translucent = YES;
Removing extra space at top of collectionView or tableView
if #available(iOS 11.0, *) {
collectionView.contentInsetAdjustmentBehavior = .never
//tableView.contentInsetAdjustmentBehavior = .never
} else {
automaticallyAdjustsScrollViewInsets = false
}
Above code collectionView or tableView goes under navigation bar.
Below code prevent the collection view to go under the navigation
self.edgesForExtendedLayout = UIRectEdge.bottom
but I love to use below logic and code for the UICollectionView
Edge inset values are applied to a rectangle to shrink or expand the
area represented by that rectangle. Typically, edge insets are used
during view layout to modify the view’s frame. Positive values cause
the frame to be inset (or shrunk) by the specified amount. Negative
values cause the frame to be outset (or expanded) by the specified
amount.
collectionView.contentInset = UIEdgeInsets(top: -30, left: 0, bottom: 0, right: 0)
//tableView.contentInset = UIEdgeInsets(top: -30, left: 0, bottom: 0, right: 0)
The best way for UICollectionView
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: -30, left: 0, bottom: 0, right: 0)
}
Delete this code work for me
self.edgesForExtendedLayout = UIRectEdgeNone
if #available(iOS 11, *) {
self.edgesForExtendedLayout = UIRectEdge.bottom
}
I was using UISearchController with custom resultsControllers that has table view. Pushing new controller on results controller caused tableview to go under search.
The code listed above totally fixed the problem
Separator line not showing full width in iPad but it was set full width for iPhone devices after using these lines.
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset = UIEdgeInsets.zero
cell.layoutMargins = UIEdgeInsets.zero
I've done all this via programmatically. Here is my code
you can directly changed in your Attribute Inspector change separator Inset --> custom and set left --> 0
and in your cell class also change the layout margin
Update
if want to remove the separator inset from all cells, you need to do two things. First, add these two lines of code to your table view controller's viewDidLoad() method:
override func viewDidLoad() {
super.viewDidLoad()
tableView.layoutMargins = .zero
tableView.separatorInset = .zero
}
Now look for you cellForRowAt method and add this:
cell.layoutMargins = .zero
The placeholder cells were not extending full width when I was programatically creating the UITableView. After trawling through all the possible methods, I discovered this.
tableview.cellLayoutMarginsFollowReadableWidth = false
I set this after setting the contentInset = .zero and the layoutMargins = .zero
Hopefully this helps other people in the same position.
Try using Attribute Inspector to set the separator inset.
Change the Left value from 15 to 0.
Try by adding custom separator insets in tableview from the storyboard or the xib shown in the below image.
This will definitely work.
I have been struggling this issue for 3 days and still can not figure it out. I do hope anyone here can help me.
Currently, i have an UITableView with customized cell(subclass of UITableViewCell) on it. Within this customized cell, there are many UILabels and all of them are set with Auto Layout (pining to cell content view) properly. By doing so, the cell height could display proper height no matter the UILabel is with long or short text.
The problem is that when i try to set one of the UILabels (the bottom one) to be hidden, the content view is not adjusted height accordingly and so as cell.
What i have down is i add an Hight Constraint from Interface Builder to that bottom label with following.
Priority = 250 (Low)
Constant = 0
Multiplier = 1
This make the constrain with the dotted line. Then, in the Swift file, i put following codes.
override func viewDidLoad() {
super.viewDidLoad()
//Setup TableView
tableView.allowsMultipleSelectionDuringEditing = true
//For tableView cell resize with autolayout
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 200
}
func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
let cell = tableView.cellForRowAtIndexPath(indexPath) as! RecordTableViewCell
cell.lbLine.hidden = !cell.lbLine.hidden
if cell.lbLine.hidden != true{
//show
cell.ConstrainHeightForLine.priority = 250
}else{
//not show
cell.ConstrainHeightForLine.priority = 999
}
//tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
return indexPath
}
The tricky thing is that when i call tableView.reloadRowAtIndexPaths(), the cell would display the correct height but with a bug that it has to be trigger by double click (selecting) on the same row rather than one click.
For this, i also try following code inside the willSelectRowAtIndexPath method, but none of them is worked.
cell.contentView.setNeedsDisplay()
cell.contentView.layoutIfNeeded()
cell.contentView.setNeedsUpdateConstraints()
Currently the result is as following (with wrong cell Height):
As showed in the Figure 2, UILabel 6 could be with long text and when i hide this view, the content view is still showing as large as it before hiding.
Please do point me out where i am wrong and i will be appreciated.
I finally change the code
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
to the following
tableView.reloadData()
Then, it work perfectly.
However, i don't really know the exactly reason on it. Hope someone can still comment it out.