I'm building an iOS app in swift with Xcode 6.
I'm trying to embed a view controller with a table view in a scrollview. When the user drags in the table view, it is suppose to move the table, not the the scrollview that it is embedded in.
I've made this illustration, to clearify my view and view controller hierachy:
The red area is the content size area of the scrollview.
The green and blue areas are different view controllers, embedded in the scrollview.
The yellow area is a Text field in the blue view controller.
The orange area is a table view in the blue view controller.
I have enabled paging in the scrollview, so that it snaps to either the green or blue view controller. How can I pop the Table view to the top of the view hierachy, so that the only way to scroll the scrollview, will be to drag in the text field.
import UIKit
class RootViewController: UIViewController, UIScrollViewDelegate {
var scrollView: UIScrollView!
var greenViewController: GreenViewController!
var blueViewController: BlueViewController!
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView(frame: CGRectMake(0, 0, self.view.frame.width, self.view.frame.height))
scrollView.delegate = self
scrollView.pagingEnabled = true
self.greenViewController = self.storyboard?.instantiateViewControllerWithIdentifier("Green View Controller") as! GreenViewController
self.blueViewController = self.storyboard?.instantiateViewControllerWithIdentifier("Blue View Controller") as! BlueViewController
greenViewController.view.frame = CGRectMake(0, 0, view.bounds.width, view.bounds.height)
blueViewController = CGRectMake(0, view.bounds.height, view.bounds.width, view.bounds.height)
scrollView.addSubview(greenViewController.view)
scrollView.addSubview(blueViewController.view)
scrollView.contentSize = CGSizeMake(view.bounds.width, view.bounds.height*2)
self.view.addSubview(scrollView)
}
I hope that I have expressed myself clearly.
EDIT:
I've tried changing the size of the scrollview, when it scrolls. The idea was to change the height of the frame so it matches the height of the textfield when it is scrolled all the way down. But it seems that it also changes the visible part of whats embedded in the scrollview:
func scrollViewDidScroll(scrollView: UIScrollView) {
if self.scrollView.contentOffset.y > textField.View.bounds.height {
self.scrollView.frame.size.height = view.bounds.height - scrollView.contentOffset.y - textField.View.bounds.height
println(self.scrollView.frame)
}
}
Ok it might be bit late now but still i m posting this as a tutorial !
This is a less prefered way to achieve this. Better way would be,
Using table view controller as parent view and then using prototype cells as static cell(In 'n' numbers as per your requirement) as a card view or for any other use
The every different cell used would be considered as a section and no of prototype cells will be equal to no of sections in code as in snippet below
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 3
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 2 {
return list.count
}
return 1
}
number of rows in case of section 0 and 1 would be 1 as static part
Whereas No of rows in case of section 2 i.e dynamic part would be equal to count of list.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : CustomTableViewCell.swift = CustomTableViewCell.swift()
switch indexPath.section {
case 0:
cell = tableView.dequeueReusableCellWithIdentifier("staticCell1", forIndexPath: indexPath) as! CustomTableViewCell
break
case 1:
cell = tableView.dequeueReusableCellWithIdentifier("staticCell2", forIndexPath: indexPath) as! CustomTableViewCell
break
case 2:
cell = tableView.dequeueReusableCellWithIdentifier("dynamicCell", forIndexPath: indexPath) as! CustomTableViewCell
break
default:
break
}
return cell;
}
and thats it! Work is done! Party!
I got reference from here
Mixing static and dynamic sections in a grouped table view?
I mocked this up. My View hierarchy looks like this
ViewController's UIView
...UIView (to act as a container view for all the scrollable content)
.......UIView (for the top content) - green
.......UIView (for the bottom content) - blue
............UILabel
............UITableView (with scrollable content - more rows than visible)
I wired #IBOutlets to the UIScrollView (scrollView) and UIView (containerView) for the scrollable area.
in viewDidLoad I added:
scrollView.contentSize = containerView.frame.size
If I click anywhere outside the tableView (top area, text area, etc...) I scrolls the scrollView. If I try to scroll in the table view, the tableView scrolls (the scrollView does not).
Is that what you were trying to achieve?
Related
I need to do this app that has a weird configuration.
As shown in the next image, the main view is a UIScrollView. Then inside it should have a UIPageView, and each page of the PageView should have a UITableView.
I've done all this so far. But my problem is that I want the scrolling to behave naturally.
The next is what I mean naturally. Currently when I scroll on one of the UITableViews, it scrolls the tableview (not the scrollview). But I want it to scroll the ScrollView unless the scrollview cannot scroll cause it got to its top or bottom (In that case I'd like it to scroll the tableview).
For example, let's say my scrollview is currently scrolled to the top. Then I put my finger over the tableview (of the current page being shown) and start scrolling down. I this case, I want the scrollview to scroll (no the tableview). If I keep scrolling down my scrollview and it reaches the bottom, if I remove my finger from the display and put it back over the tebleview and scroll down again, I want my tableview to scroll down now because the scrollview reached its bottom and it's not able to keep scrolling.
Do you guys have any idea about how to implement this scrolling?
I'm REALLY lost with this. Any help will be greatly appreciate it :(
Thanks!
The solution to simultaneously handling the scroll view and the table view revolves around the UIScrollViewDelegate. Therefore, have your view controller conform to that protocol:
class ViewController: UIViewController, UIScrollViewDelegate {
I’ll represent the scroll view and table view as outlets:
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var tableView: UITableView!
We’ll also need to track the height of the scroll view content as well as the screen height. You’ll see why later.
let screenHeight = UIScreen.mainScreen().bounds.height
let scrollViewContentHeight = 1200 as CGFloat
A little configuration is needed in viewDidLoad::
override func viewDidLoad() {
super.viewDidLoad()
scrollView.contentSize = CGSizeMake(scrollViewContentWidth, scrollViewContentHeight)
scrollView.delegate = self
tableView.delegate = self
scrollView.bounces = false
tableView.bounces = false
tableView.scrollEnabled = false
}
where I’ve turned off bouncing to keep things simple. The key settings are the delegates for the scroll view and the table view and having the table view scrolling being turned off at first.
These are necessary so that the scrollViewDidScroll: delegate method can handle reaching the bottom of the scroll view and reaching the top of the table view. Here is that method:
func scrollViewDidScroll(scrollView: UIScrollView) {
let yOffset = scrollView.contentOffset.y
if scrollView == self.scrollView {
if yOffset >= scrollViewContentHeight - screenHeight {
scrollView.scrollEnabled = false
tableView.scrollEnabled = true
}
}
if scrollView == self.tableView {
if yOffset <= 0 {
self.scrollView.scrollEnabled = true
self.tableView.scrollEnabled = false
}
}
}
What the delegate method is doing is detecting when the scroll view has reached its bottom. When that has happened the table view can be scrolled. It is also detecting when the table view reaches the top where the scroll view is re-enabled.
I created a GIF to demonstrate the results:
Modified Daniel's answer to make it more efficient and bug free.
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var tableHeight: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
//Set table height to cover entire view
//if navigation bar is not translucent, reduce navigation bar height from view height
tableHeight.constant = self.view.frame.height-64
self.tableView.isScrollEnabled = false
//no need to write following if checked in storyboard
self.scrollView.bounces = false
self.tableView.bounces = true
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 30))
label.text = "Section 1"
label.textAlignment = .center
label.backgroundColor = .yellow
return label
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "Row: \(indexPath.row+1)"
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.scrollView {
tableView.isScrollEnabled = (self.scrollView.contentOffset.y >= 200)
}
if scrollView == self.tableView {
self.tableView.isScrollEnabled = (tableView.contentOffset.y > 0)
}
}
Complete project can be seen here:
https://gitlab.com/vineetks/TableScroll.git
After many trials and errors, this is what worked best for me. The solution has to solve two needs 1) determine who's scrolling property should be used; tableView or scrollView? 2) make sure that the tableView doesn't give authority to the scrollView until it has reached the top of it's table/content.
In order to see if the scrollview should be used for scrolling vs the tableview, i checked to see if the UIView right above my tableview was within frame. If the UIView is within frame, it's safe to say the scrollView should have authority to scroll. If the UIView is not within frame, that means that the tableView is taking up the entire window, and therefor should have authority to scroll.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.intersects(UIView.frame) == true {
//the UIView is within frame, use the UIScrollView's scrolling.
if tableView.contentOffset.y == 0 {
//tableViews content is at the top of the tableView.
tableView.isUserInteractionEnabled = false
tableView.resignFirstResponder()
print("using scrollView scroll")
} else {
//UIView is in frame, but the tableView still has more content to scroll before resigning its scrolling over to ScrollView.
tableView.isUserInteractionEnabled = true
scrollView.resignFirstResponder()
print("using tableView scroll")
}
} else {
//UIView is not in frame. Use tableViews scroll.
tableView.isUserInteractionEnabled = true
scrollView.resignFirstResponder()
print("using tableView scroll")
}
}
hope this helps someone!
None of the answers here worked perfectly for me. Each one had it's owned nuanced problem (needing to do a repeated swipe when one scrollview hit it's bottom, or the scroll indicator not looking correct, etc), so figured I'd throw in another answer.
Ole Begemann has a great write up on doing this exactly https://oleb.net/blog/2014/05/scrollviews-inside-scrollviews/
Despite being an old post, the concepts still apply to the current APIs. Additionally, there is a maintained (Xcode 9 compatible) Objective-C implementation of his approach https://github.com/eyeem/OLEContainerScrollView
If you are facing problem with the nested scrolling issue , here tis the simplest solution for it .
go to your design screen
select your scroll view and then disable bounce on scroll
if your view uses table view inside scroll view then disable bounce on scroll of the table view as well
run and check it is solved
check how to disable bounce on scroll of a scroll view
check how to disable bounce on scroll of a tableview view
I was struggling with this problem, too. There is a very simple solution.
In interface builder:
create simple ViewController
add a simple View, it will be our header, and constrain it to superview
it's the red view on the example below
I have added 12px from top, left and right, and set fixed height to 128px
embed a PageViewController, making sure it is constrained to the superview, and not the header
Now, here comes the fun part: for each page you add, make sure its tableView has an offset from top. Thats it. You can do if with this code, for example (assuming you use UITableViewController as a page):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let tables = viewControllers.compactMap { $0 as? UITableViewController }
tables.forEach {
$0.tableView.contentInset = UIEdgeInsets(top: headerView.bounds.height, left: 0, bottom: 0, right: 0)
$0.tableView.contentOffset = CGPoint(x: 0, y: -headerView.bounds.height)
}
}
No messy scroll inside scroll inside table view, no mangling with delegates, no duplicated scrolls, perfectly natural behavior. If you can't see the header, it is probably because of the tableView background color. You have to set it to clear, for the header to be visible from under the tableView.
I think there are two options.
Since you know the size of the scroll view and the main view, you are unable to tell whether the scroll view hit the bottom or not.
if (scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height)) {
// reach bottom
}
So when it hit; you basically set
[contentScrollView setScrollEnabled:NO];
and other way around for your tableView.
The other thing, which is more precise I think, is to add Gesture to your views.
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(respondToTapGesture:)];
// Specify that the gesture must be a single tap
tapRecognizer.numberOfTapsRequired = 1;
// Add the tap gesture recognizer to the view
[self.view addGestureRecognizer:tapRecognizer];
// Do any additional setup after loading the view, typically from a nib
So when you add Gesture, you can simply control the active view by changing setScrollEnabled in the respondToTapGesture.
I found an awesome library
MXParallaxHeader
In Storyboard just set UIScrollView class to MXScrollView then magic happens.
I used this class to handle my UIScrollView when I embed a UIPageViewController container view. even you can insert a parallax header view for more detail.
Also, this library provides Cocoapods and Carthage
I attached an image below which represent UIViewHierarchy.
MXScrollView Hierarchy
SWIFT 5
I had some trouble using Vineet's answer for when I could not guarantee the scrollView content offset (Y) due to various different screen sizes. To resolve this, I changed the first trigger event of when the tableView's scroll gets enabled.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.contains(button.frame) {
tableView.isScrollEnabled = true
}
if scrollView == tableView {
self.tableView.isScrollEnabled = (tableView.contentOffset.y > 0)
}
}
The scrollView.bounds.contains will check if a given element's frame is FULLY within the scrollView's visible content. I set this to a button that I have below the tableView. You could set this to your tableVIew's frame instead if your only condition is that your tableView is fully visible.
I left the original implementation of when to disable the tableView's scroll and it works very well.
I tried the solution marked as the correct answer, but it was not working properly. The user need to click two times on the table view for scroll and after that I was not able to scroll the entire screen again. So I just applied the following code in viewDidLoad():
tableView.addGestureRecognizer(UISwipeGestureRecognizer(target: self, action: #selector(tableViewSwiped)))
scrollView.addGestureRecognizer(UISwipeGestureRecognizer(target: self, action: #selector(scrollViewSwiped)))
And the code below is the implementation of the actions:
func tableViewSwiped(){
scrollView.isScrollEnabled = false
tableView.isScrollEnabled = true
}
func scrollViewSwiped(){
scrollView.isScrollEnabled = true
tableView.isScrollEnabled = false
}
One easy trick, if you want to achieve it is replacing parent scrollview with normal container view.
Adding a pan gesture on container view, you can play with top constraint of first view to assign negative values. You can keep a check of page View's origin if it achieves to top you can start assigning that value on content offset of the pageView's child view. Until user achieves the table view in a state of top most view in container view, you can keep page tableView's scrolling disabled and allow scrolling manually by setting content offset.
So initially the page view height will be collapsed (or say out of screen) or less at bottom. Later on scrolling down it will expand to take more space.
Gesture will automatically stop responding if out of frames say on nav bar or other view outside container view.
Gestures are a key to user interactive transitions used in many apps. You can mimic scroll for a certain time with it.
In my case I'm using constraint for height like that:
self.heightTableViewConstraint.constant = self.tableView.contentSize.height
self.scrollView.contentInset.bottom = self.tableView.contentSize.height
Below code works great for me
As I wanted to show some header after some scroll and table view supposed to scroll
And in ViewDidLoad add
override func viewDidLoad() {
super.viewDidLoad()
mainScrollView.delegate = self
}
Change 265 to whatever number you want to stop upper scroll
extension AccountViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(notebookTableView.contentOffset.y)
if notebookTableView.contentOffset.y < 265 {
if notebookTableView.contentOffset.y > 0 {
mainScrollView.setContentOffset(notebookTableView.contentOffset, animated: false)
} else {
mainScrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false)
}
} else {
mainScrollView.setContentOffset(CGPoint(x: 0.0, y: 265), animated: false)
}
}
}
CGFloat tableHeight = 0.0f;
YourArray =[response valueForKey:#"result"];
tableHeight = 0.0f;
for (int i = 0; i < [YourArray count]; i ++) {
tableHeight += [self tableView:self.aTableviewDoc heightForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
}
self.aTableviewDoc.frame = CGRectMake(self.aTableviewDoc.frame.origin.x, self.aTableviewDoc.frame.origin.y, self.aTableviewDoc.frame.size.width, tableHeight);
Maybe brute-force, but working perfectly if cell heights are the same: by the way, I use auto layout.
for the tableView (or collectionView or whatever), set an arbitrary height in storyboard, and make an outlet to class. Wherever appropriate, (viewDidLoad() or...) set the tableView's height big enough so that tableView doesn't need to scroll. (need to know the number of rows in advance) Then only the outer scrollView will scroll nicely.
I have created a screen in which I have a full-screen UITableView, I set UIEdgeInsets to it, which I have configured as follows:
categoriesTableView.contentInset = UIEdgeInsets.init(top: HEADER_VIEW_HEIGHT, left: 0, bottom: 0, right: 0)
where HEADER_VIEW_HEIGHT is CGFloat = 160.
This allowed me to add a "header view" to the UITableView which is getting covered when I start scrolling the UITableView (...and not getting stuck above the UITableView, as a real header view will).
The Problem: The problem is that I need to have 3 clickable views in the header view, So I designed 3 views in the storyboard and configured Tap Gesture Recognizers on them. But when I try to use them the tap gestures are not passed throw the UITableView even though I see those views on screen (as a result of the contentInset configuration). The only way I can make them tapable/clickable is if I set User Interaction Enabled to false on the UITableView (which I can't do because I need the UITableView to be draggable and clickable as well).
The Question: How would I pass the tap events to the lower "header view" clickable parts when it's not covered by the UITableView as a result of the contentInset setting?
Here is the UI image, in it you can see that there is a full-screen UITableView, behind it there a view with 3 subviews that contains 3 favorite items which I can present to the user for an easier access. when the screen starts, there is a contentInset for the UITableView hence the user can see those easy access options and press them (which he can't do right now). When the user starts scrolling, the UITableView goes on top of the layout with the 3 views and the user able to scroll the list in a full screen. kind of like a parallax effect.
I have a very ugly solution for this..
Now you have a table view and below that there is a header view right? Add one more view on top of table view which contains 3 sub views in it and all transparent in colour.
Position this newly added subview same as that of header view (Either by programmatically giving same frame as that of header view or by connecting its top, left, bottom and right constraints to the header view). Similarly position the 3 sub views of this new view as same as that of the subviews inside the header view. And give tap gesture to the subviews of this new view. So our user will think he is tapping the header view, whereas he is actually tapping in this invisible view.
And if you want to get touch in the table view once it scrolls up and cover the header view, then you can use one of these two..
Call the UIScrollViewDelegate delegate method scrollViewDidScroll() and inside if scrollView.contentOffset.y >= 160, set the User Interaction Enabled to false for this newly added view and revert that back to true if scrollView.contentOffset.y < 160.
Or from inside scrollViewDidScroll(), assign outletOfTopConstraintOfYourNewView.constant = -(scrollView.contentOffset.y), so that the new view will also move up according to the scroll, thereby changing its visible tappable area.
Another not so elegant idea is..
Instead of giving contentInset, add one more section in this table view at the 0th index with only one cell whose height is 160, user interaction enabled is false and colour is transparent. Then you don't have to worry about the logic in scrollViewDidScroll().
Update:
Below solution is not perfect but it may give you a direction to start with.
A) Put the top constraint of tableView top to bottom constraint of headerview.
B) Create IBOutlet of height constraint of headerview in your ViewController
C) Listen to tableview's ScrollViewDidScroll and put code like this
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let y = scrollView.contentOffset.y
if y > 50{
if heightConstant.constant != 0{
view.layoutIfNeeded()
heightConstant.constant = 0
UIView.animate(withDuration: 0.5, delay: 0, options: .allowUserInteraction, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}else{
view.layoutIfNeeded()
heightConstant.constant = 160
UIView.animate(withDuration: 0.5, delay: 0, options: .allowUserInteraction, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
Here 50 is a point form where it should start animating. It is somewhat similar to Collapsable Toolbar in Android. Just make sure headerView.isClipsToBounds = true.
Convert your headerview into a UITableViewCell and in change your UITableViewDataSource as
extension ViewController: UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == 0 ? 1 : yourList.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0{
// set your header view here
}else{
// Your normal cell configuration
}
}
}
With this, your headerview will become a part of table view but as a UITableViewCell thus it will not behave like sticky header like the normal one.
I have an UITableView which consists of prototype cells. I want to put an UIButton inside the bottom of the UITableView using Interface Builder.
I added the UIButton in the footer of the UITableView:
I added a purple background for the Footer View and a green background colour for the UITableView. In the picture above it shows the Button at the bottom of the footer. However this isn't equal to the bottom of the UITableView.
The GIF below displays that the button is placed bellow the cells but not inside the bottom of the UITableView. I want it to appear at the bottom in the UITableView. Not under the UITableView. The following GIF displays this problem:
My question is: How do I set an UIButton inside an UITableView at the bottom of the UITableView using Interface Builder?
This is what I want to achieve (From Apple's ResearchKit):
Edit: The UIButton should be inside the UITableView. Suggestions where the UIButton is placed outside the TableView and pinned underneath don't achieve my goal.
You are setting footer width wrong.Set it fixed height so that button sticks to that particular height(Should be Fixed like 60px)
Check Demo Code for Storyboard structure and constraints
So I had to slightly swizzle it, but got it working by doing the below things:
Pull the UIButton out to the same level in the view heirarcy as
the tableview.
Embed the tableview and the button inside a view
Embed the above view inside another view
Pin edges of view #3 (Pinned View) to superview
Pin top, left & right edges of view #2 (Resizing View) to view #3 edges. And set a constraint of equal height to view #3.
Set an outlet in the view controller for the equal height constraint
The view heirarcy in IB should look like this:
Now in the view controller code, you need to do the following things:
Create instance var for the keyboard offset value
var keyboardOffset: CGFloat = 0
set notifications and observers for the keyboard willShow and
willHide
notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(_:)), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(_:)), name:NSNotification.Name.UIKeyboardWillHide, object: nil)
In keyboardWillShow, cache the keyboard height value.
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
keyboardOffset = keyboardSize.height
}
Create didSet method on the keyboardOffset var, and animate the height of the view by that value each time it is set
var keyboardOffset: CGFloat = 0 {
didSet {
resizingViewHeight.constant = -keyboardOffset
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}
}
}
Make sure you set the offset back to 0 in keyboardWillHide
keyboardOffset = 0
Every time the keyboard now appears, the view that is containing the tableview will reduce in size and therefore pull the contents up with it, providing the shrinking tableview effect that you are hoepfully looking for!
Add a view that contains the UIButton to the bottom of the UIViewController where the UITableView is. Give it the constraints to attach to left, right and bottom side of super view and probably a fixed height.
Then attach the UITableView's bottom constraint to the top of the view that contains the UIButton.
You should get the effect you're looking for.
NOTE: For the button you can give centered Y and X in superview constraints to keep it centered.
Footer is apperead always after the last cell of your table view so your output is correct.
If you wanted the button bottom of tableview then add button below the tableview in hierarchy not as a footer. But it makes your button static that means it didn't matter how much cells you have, button is always button of the tableView but it is not a scrollable like as it is now.
I tried the accepted answer, but couldn't get it to work. I found that the footer view always stayed pinned to the bottom of the screen, regardless of the size of the TableView (just as if it were a sibling of the TableView). I ended up following an approach suggested here: https://stackoverflow.com/a/18047772/5778751 The basic idea is that you programmatically determine the height of the TableView and depending on the result, you EITHER display a footer internal to the TableView OR display a view which is a sibling of the TableView.
I have a perfect solution for this problem. Using default was never that meaningful in my life.
The button under the view is also a table view cell from another section but its configuration of header height and interior design is just different from the above cells.
So I have five different sections. The first three of them are standard table view cells(SettingTableViewCell) but the last two(cache and version) are custom buttons. In the header title, I init for those empty titles.
enum Section: Int {
case adjustSettings
case about
case agreements
case cache
case version
static var numberOfSections: Int { return 5 }
var reuseIdentifier: String { return "SettingTableCell" }
var headerTitle: String? {
switch self {
case .adjustSettings: return "settings.adjust.section.title".localized
case .about: return "settings.headertitle.about".localized
case .agreements: return "agreement.title".localized
case .cache: return ""
case .version: return ""
}
}
Then I configured with cell will be in which section with below code. Cache and version have only one cell which will be our buttons.
var cells: [CellType] {
switch self {
case .adjustSettings:return [.notification,.language ]
case .about: return [.rate, .contact, .invite]
case .agreements: return [.membership, .kvkk, .illuminate]
case .cache: return [.cache]
case .version: return [.version]
}
}
I have three different set functions inside my settingsTableViewCell.
For setting up standard table view cell -> .setDefault(text: text)
For setting up my clean cache button -> .setCache(text: text)
Last for shoving version info -> .setVersion(version: version)
with the above cellForRowAt, I am switching rows and setting them up accordingly. My default is .setDefault
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else {
assertionFailure()
return UITableViewCell()
}
let row = section.cells[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: section.reuseIdentifier) as! SettingTableCell
switch row {
case .version:
cell.setVersion(version: getVersion())
case .cache:
ImageCache.default.calculateDiskCacheSize(completion: { size in
if size == 0 {
cell.setCache(text: "settings.clear.data".localized)
} else {
let byte = Int64(size)
let fileSizeWithUnit = ByteCountFormatter.string(fromByteCount: byte, countStyle: .file)
cell.setCache(text: "settings.cler.data.with.string".localized + "(\(String(describing: fileSizeWithUnit)))")
}
})
default:
cell.setDefault(text: row.text)
}
return cell
}
You can adjust button heights as below by switching section.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let section = Section(rawValue: indexPath.section) else { return 0 }
switch section {
case .cache: return 44
case .version: return 44
default: return 56.0
}
You can adjust the gap between each button as below.
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
guard let section = Section(rawValue: section) else { return 0 }
switch section {
case .adjustSettings: return 46
case .about: return 46
case .agreements: return 46
case .cache: return 9
case .version: return 0.5
default: return 46
}
And finally, this is my cell where I set .set functions to customize each cell as I pleased.
class SettingTableCell: UITableViewCell {
#IBOutlet weak var line: UIView!
#IBOutlet weak var content: UIView!
#IBOutlet weak var arrowView: UIView!
#IBOutlet weak var labelSetting: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
func setVersion(version: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = .clear
labelSetting.label(textStr: version, textColor: KSColor.neutral400.getColor(), textFont: .sfProTextRegular(size: 13), fontSize: 13, lineSpacing: -0.13, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setCache(text: String) {
arrowView.isHidden = true
line.isHidden = true
content.backgroundColor = KSColor.neutral100.getColor()
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: .sfProTextMedium(size: 14), fontSize: 14, lineSpacing: -0.14, paragraphStyle: NSMutableParagraphStyle())
labelSetting.textAlignment = .center
self.accessoryType = .none
}
func setDefault(text: String) {
labelSetting.label(textStr: text, textColor: KSColor.neutral700.getColor(), textFont: UIFont.sfProTextMedium(size: 16), fontSize: 16, lineSpacing: -0.16, paragraphStyle: NSMutableParagraphStyle())
}
}
And the outcome is I have 5 sections but the last two are buttons.
I've a UITableViewController and I placed a UILabel beneath the tableview in interface builder. However, I can't seem to control the actual Y position of the label. There's a big gap between the tableview and the label that I'd like to reduce by moving the label up a bit. I tried moving the labels origin.y programmatically but it remains in the same spot.
Is there any way to do this?
If you want it at the bottom of the table view, you can just add it as the tableFooterView. For example:
override func viewDidLoad() {
super.viewDidLoad()
let footerLabel = UILabel()
footerLabel.text = "Some Text"
footerLabel.textAlignment = .Center
footerLabel.textColor = UIColor.redColor()
footerLabel.sizeToFit()
self.tableView.tableFooterView = footerLabel
}
You can use either a table view added to a UIViewController or a UITableViewController.
The tableFooterView directly abuts the section footer for the last section of the table view.
override func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 0.1 // adjust this value to determine how close the label is to the last table view cell. Don't return 0 unless you want the default value for grouped style table views
}
I am having trouble getting my UITableView to appear full height in my Stack View.
My view tree looks as follows:
- View
- Scroll View
- Stack View
- Table View
- Image View
- Map View
The table view is dynamically populated with data, which works fine. The issue is that only one row is visible at a time and I have to scroll through the list. What I would like to see happen is for the table view to take as much vertical room as it needs to display all the cells.
I did try adjusting table height as follows, but that just ends up with table that no longer scrolls, though even if it did work I would rather have something more dynamic:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.detailsTableView.frame.size.height = 200
}
I am suspecting that it is probably an aspect of the 'stack view' that needs adjusting, but I am not sure at this point. Can anyone suggest an appropriate way?
I had been encountering the same issue and realized you need a self sizing table view. I stumbled on this answer and created a subclass like #MuHAOS suggested. I did not encounter any issues.
final class IntrinsicTableView: UITableView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
A UIStackView will compress views wherever it can, to counteract this set a height anchor and width anchor to the UITableView or a priority for its height and width. Here is a working example of how we can be in charge of the dimensions of a table within a stack view.
An extension to instantiate and centrally position the UIStackView
First of all I've written a UIStackView extension so that I don't need to include all the code inside the view controller. Your positioning and setup will be different because you are placing your stack view inside a scroll view, but separating this code out means you can make your own adjustments.
extension UIStackView {
convenience init(axis:UILayoutConstraintAxis, spacing:CGFloat) {
self.init()
self.axis = axis
self.spacing = spacing
self.translatesAutoresizingMaskIntoConstraints = false
}
func anchorStackView(toView view:UIView, anchorX:NSLayoutXAxisAnchor, equalAnchorX:NSLayoutXAxisAnchor, anchorY:NSLayoutYAxisAnchor, equalAnchorY:NSLayoutYAxisAnchor) {
view.addSubview(self)
anchorX.constraintEqualToAnchor(equalAnchorX).active = true
anchorY.constraintEqualToAnchor(equalAnchorY).active = true
}
}
We don't set a size for the UIStackView only a position, it is the things contained within it that determine its size. Also note the setting of translatesAutoresizingMaskIntoConstraints to false in the UIStackView extension. (It is only required that we set this property for the stack view, its subviews simply inherit the behaviour.)
UITableView class with data source code
Next I've created a basic table class for demo purposes.
class MyTable: UITableView, UITableViewDataSource {
let data = ["January","February","March","April","May","June","July","August","September","October","November","December"]
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SauceCell", forIndexPath: indexPath)
cell.textLabel?.text = data[indexPath.row]
return cell
}
}
Setup of stack view and table in view controller
Finally, the important stuff. As soon as we add our table to the stack view all the frame information is disregarded. So we need the final two lines of code to set the width and height for the table in terms that Auto Layout can understand.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let table = MyTable(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
table.registerClass(UITableViewCell.self, forCellReuseIdentifier: "SauceCell")
table.dataSource = table
let stack = UIStackView(axis: .Vertical, spacing: 10)
stack.anchorStackView(toView: view, anchorX: stack.centerXAnchor, equalAnchorX: view.centerXAnchor, anchorY: stack.centerYAnchor, equalAnchorY: view.centerYAnchor)
stack.addArrangedSubview(table)
table.widthAnchor.constraintEqualToAnchor(view.widthAnchor, multiplier: 1).active = true
table.heightAnchor.constraintEqualToAnchor(view.heightAnchor, multiplier: 0.5).active = true
}
}
Note that we use addArrangedSubview: not addSubview: when adding views to the stack view.
(I've written blogposts about UIStackView as well as others about Auto Layout in general that might help too.)