I've configured a test playground with a UITableView and an instance of UIStackView as its header.
The stackView contains 10 UILabels.
The problem is that after calling stackView.sizeToFit() the stackView doesn't resize itself to fit all the labels and its size is zero.
I have to manually set the size of the UIStackView to fix this issue, which defeats the purpose of the UIStackView.
Here is the code of the test Xcode Playground, so you could reproduce it in your environment:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class TestTableViewController: UITableViewController {
private lazy var searchController = UISearchController(searchResultsController: nil)
private lazy var stackView: UIStackView = {
let s = UIStackView()
s.axis = .vertical
for i in 0...10 {
let l = UILabel()
l.text = "text \(i)"
s.addArrangedSubview(l)
}
return s
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reuseIdentifier")
configureTableView()
configureSearchController()
}
private func configureTableView() {
tableView.tableHeaderView = stackView
stackView.sizeToFit()
tableView.tableHeaderView?.sizeToFit() // Doesn't work!!!!!!!
tableView.tableHeaderView?.frame.size = CGSize(width: 9, height: 100)
}
private func configureSearchController() {
// searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.placeholder = NSLocalizedString("Choose template",
comment: "Choose template")
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 2
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 7
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
}
let nc = UINavigationController(rootViewController: TestTableViewController())
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = nc
Setting size manually achieves the desired result:
private func configureTableView() {
tableView.tableHeaderView = stackView
tableView.tableHeaderView?.frame.size = CGSize(width: 0, height: 400)
}
Table header views need a little extra help... This may work for you.
private func configureTableView() {
tableView.tableHeaderView = stackView
sizeHeaderToFit(tableView: tableView)
}
private func sizeHeaderToFit(tableView: UITableView) {
if let headerView = tableView.tableHeaderView {
let height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
var frame = headerView.frame
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
}
}
Related
I have a weird requirement for UI, and need to scroll a table view in a page view using the scroll view I embed the page view in. For example:
import UIKit
class TableVC: UIViewController {
let tableView = UITableView()
var observer: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
}
func setupTableView() {
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.heightAnchor.constraint(equalToConstant: 2000)
])
tableView.dataSource = self
tableView.delegate = self
tableView.isScrollEnabled = false
tableView.backgroundColor = .blue
let handler = { (tableView: UITableView, change: NSKeyValueObservedChange<CGSize>) in
if let contentSize = change.newValue {
print("contentSize:", contentSize)
}
}
observer = tableView.observe(\UITableView.contentSize, options: [NSKeyValueObservingOptions.new], changeHandler: handler)
}
}
extension TableVC: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
50.0
}
}
extension TableVC: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 40
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.backgroundColor = .green
return cell
}
}
And the main view controller:
import UIKit
class ViewController: UIViewController {
private enum Constants {
static let pageViewControllerOptions: [UIPageViewController.OptionsKey: Any] = [
.interPageSpacing: CGFloat(8.0)
]
}
private let pageVC: UIPageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal,
options: Constants.pageViewControllerOptions
)
let scrollView: UIScrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
scrollView.pin(to: view)
let stackView = UIStackView()
stackView.axis = .vertical
stackView.frame = view.bounds
stackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
scrollView.addSubview(stackView)
let blueView = UIView()
blueView.backgroundColor = .blue
stackView.addArrangedSubview(blueView)
blueView.translatesAutoresizingMaskIntoConstraints = false
blueView.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
addChild(pageVC)
pageVC.didMove(toParent: self)
let pageView = pageVC.view!
stackView.addArrangedSubview(pageView)
pageVC.setViewControllers([TableVC()], direction: .forward, animated: false, completion: nil)
// 50 height with 20 cells works fine
// 50 height with 40 cells won't work beacuse it doesn't seem to load them all
scrollView.contentSize = CGSize(width: view.bounds.width, height: 2000 + 100)
}
}
With 40 cells I am expecting a content size of 2000, I set this above (allowing for the height of the blue view also). The scrolling works but it seems like at some point it has stopped generating the cells:
In the gif above you can see there are less than 40 cells. I have tried using a larger table view height, calling layoutIfNeeded but the result is the same. Is there anything I can do to fix this? I imagine the cells are not loading because I am not using the table view's own scroll view so cellForRow isn't triggered. Is there another way to load the cells?
Objective
When the user clicks on any of the 3 blue buttons, all buttons change to the same color.
Note: this is an abstraction of a shared progress view problem, it's therefore important that only one UIView is shared (or mimicked) across my three rows
Here is a compilable Swift project:
import UIKit
class ToggleButton: UIButton {
var connectedView: UIView?
func onPress() {
self.isHidden = true
self.connectedView?.isHidden = false
}
}
class ViewController : UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView: UITableView = UITableView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
var myView: UIView? = nil
var toggleBtn: ToggleButton? = nil
override func viewDidLoad() {
self.setupTableView()
}
fileprivate func setupTableView() {
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.backgroundColor = UIColor.white
self.tableView.isOpaque = true
self.view.addSubview(self.tableView)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: "CellIdentifier")
let frame = CGRect(x: 10, y: 10, width: 30, height: 30)
if let view = self.myView, let btn = self.toggleBtn {
cell.addSubview(view)
cell.addSubview(btn)
} else {
let myView = UIView(frame: frame)
myView.backgroundColor = UIColor.green
myView.isHidden = true
cell.addSubview(myView)
let toggleBtn = ToggleButton(frame: frame)
toggleBtn.backgroundColor = UIColor.blue
toggleBtn.addTarget(self, action: #selector(onPress), for: .touchUpInside)
toggleBtn.connectedView = myView
cell.addSubview(toggleBtn)
}
return cell
}
#objc func onPress(_ sender: Any) {
if let button = sender as? ToggleButton {
button.onPress()
}
}
}
Any help appreciated.
The concept of UITableViewCell is made to be very independent each other.
So the only thing you can do it having a bool flag in your ViewController, then you init your 3 cells with this flags.
And finally each time the button is pressed you toggle the flag en reload your tableView.
I'm creating a scrollable view that is the checkout section of a shopping cart.
This is the view layout
It's a scrollable view that has a UIView inside, whose content size should adjust itself depending on the size of each table view within, each table view inside the UIView should adjust it's height to it's content size which would vary depending on the number of baskets and products. (Please note that what I want is to adjust the size of the entire tableView not just one of it's cells).
I checked this answer Change UITableViewHeight Dynamically and it offers some suggestions on how to make my UITableViews auto-sizable, but I need both the UIView and the TableViews inside to be dynamically sizeable.
Any help is greatly appreciated.
I don't understand why you don't want to combine them into one table..
Anyway.. UIScrollView doesn't work with Auto-Layout. You have to add a contentView with the same dimensions as the scrollView's parent OR with a fixed size.
Now that is out of the way, you constrain the tableViews to the scrollView's contentView, then override viewDidLayoutSubviews of the controller. In that function, you need to get the contentSize of each tableView and constrain the height of each one to its content size. This will make the table full size and not scrollable.
Since the scrollView auto sizes based on its contents, you don't have to do anything else.
Alternatively, if you don't want to use AutoLayoutScrollView, you can set the UIScrollView contentSize to the sum of the contentSize of the 3 tables in viewDidLayoutSubviews of the controller.
Note: I do not have any sample dynamic content for the tableViews but if you want them to use dynamic row sizes, you need to do:
table.estimatedRowHeight = 300 //Estimation of the average size of the rows.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
return UITableViewAutomaticDimension
}
Now for the actual code with hardcoded row heights (because I don't have dynamic test data):
//
// ViewController.swift
// SO
//
// Created by Brandon T on 2017-01-17.
// Copyright © 2017 XIO. All rights reserved.
//
import UIKit
class AutoLayoutScrollView : UIScrollView {
private(set) weak var contentView: UIView!
private var hConstraint: NSLayoutConstraint!
private var vConstraint: NSLayoutConstraint!
init() {
super.init(frame: .zero)
self.layout()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.layout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layout()
}
private func layout() {
let view = UIView()
self.contentView = view
self.addSubview(self.contentView)
self.contentView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
self.contentView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
self.contentView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
self.contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
self.contentView.translatesAutoresizingMaskIntoConstraints = false
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
if let parent = self.superview {
self.leftAnchor.constraint(equalTo: parent.leftAnchor).isActive = true
self.rightAnchor.constraint(equalTo: parent.rightAnchor).isActive = true
self.topAnchor.constraint(equalTo: parent.topAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: parent.bottomAnchor).isActive = true
self.translatesAutoresizingMaskIntoConstraints = false
self.hConstraint = self.contentView.widthAnchor.constraint(equalTo: parent.widthAnchor)
self.vConstraint = self.contentView.heightAnchor.constraint(equalTo: parent.heightAnchor)
self.hConstraint.isActive = true
self.vConstraint.isActive = true
}
}
func setHorizontalScrollEnabled(enabled: Bool) {
self.hConstraint.isActive = !enabled
}
func setVerticalScrollEnabled(enabled: Bool) {
self.vConstraint.isActive = !enabled
}
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private var scrollView: AutoLayoutScrollView!
private var topTableView: UITableView!
private var middleTableView: UITableView!
private var bottomTableView: UITableView!
private var topTableHeight: NSLayoutConstraint!
private var middleTableHeight: NSLayoutConstraint!
private var bottomTableHeight: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
self.layout()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func layout() {
//Init Views
self.scrollView = AutoLayoutScrollView()
self.topTableView = UITableView(frame: .zero, style: .plain)
self.middleTableView = UITableView(frame: .zero, style: .plain)
self.bottomTableView = UITableView(frame: .zero, style: .plain)
self.registerClasses()
//Add Views
self.view.addSubview(self.scrollView)
self.scrollView.contentView.addSubview(self.topTableView)
self.scrollView.contentView.addSubview(self.middleTableView)
self.scrollView.contentView.addSubview(self.bottomTableView)
//Layout Views
self.topTableView.leftAnchor.constraint(equalTo: self.scrollView.contentView.leftAnchor).isActive = true
self.topTableView.rightAnchor.constraint(equalTo: self.scrollView.contentView.rightAnchor).isActive = true
self.topTableView.topAnchor.constraint(equalTo: self.scrollView.contentView.topAnchor).isActive = true
self.topTableView.translatesAutoresizingMaskIntoConstraints = false
self.middleTableView.leftAnchor.constraint(equalTo: self.scrollView.contentView.leftAnchor).isActive = true
self.middleTableView.rightAnchor.constraint(equalTo: self.scrollView.contentView.rightAnchor).isActive = true
self.middleTableView.topAnchor.constraint(equalTo: self.topTableView.bottomAnchor).isActive = true
self.middleTableView.translatesAutoresizingMaskIntoConstraints = false
self.bottomTableView.leftAnchor.constraint(equalTo: self.scrollView.contentView.leftAnchor).isActive = true
self.bottomTableView.rightAnchor.constraint(equalTo: self.scrollView.contentView.rightAnchor).isActive = true
self.bottomTableView.topAnchor.constraint(equalTo: self.middleTableView.bottomAnchor).isActive = true
self.bottomTableView.bottomAnchor.constraint(equalTo: self.scrollView.contentView.bottomAnchor).isActive = true
self.bottomTableView.translatesAutoresizingMaskIntoConstraints = false
self.topTableHeight = self.topTableView.heightAnchor.constraint(equalToConstant: 0)
self.middleTableHeight = self.middleTableView.heightAnchor.constraint(equalToConstant: 0)
self.bottomTableHeight = self.bottomTableView.heightAnchor.constraint(equalToConstant: 0)
//Set Views Properties
self.scrollView.setVerticalScrollEnabled(enabled: true)
self.topTableView.delegate = self
self.topTableView.dataSource = self
self.middleTableView.delegate = self
self.middleTableView.dataSource = self
self.bottomTableView.delegate = self
self.bottomTableView.dataSource = self
//Display Views
self.topTableView.reloadData()
self.middleTableView.reloadData()
self.bottomTableView.reloadData()
}
func registerClasses() {
self.topTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellID")
self.middleTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellID")
self.bottomTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellID")
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//Update tables constraints to full size.
self.topTableHeight.constant = self.topTableView.contentSize.height
self.middleTableHeight.constant = self.middleTableView.contentSize.height
self.bottomTableHeight.constant = self.bottomTableView.contentSize.height
self.topTableHeight.isActive = true
self.middleTableHeight.isActive = true
self.bottomTableHeight.isActive = true
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (tableView == self.topTableView) {
return 10
}
if (tableView == self.middleTableView) {
return 15
}
if (tableView == self.bottomTableView) {
return 3
}
return 0
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if (tableView == self.topTableView) {
return 100
}
if (tableView == self.middleTableView) {
return 250
}
if (tableView == self.bottomTableView) {
return 500
}
return 0.0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellID", for: indexPath)
if (tableView == self.topTableView) {
cell.contentView.backgroundColor = UIColor.red
}
if (tableView == self.middleTableView) {
cell.contentView.backgroundColor = UIColor.green
}
if (tableView == self.bottomTableView) {
cell.contentView.backgroundColor = UIColor.blue
}
return cell
}
}
I need to put an UIView fixed on top of UITableViewController (like a header). I've tried this:
override func scrollViewDidScroll (scrollView: UIScrollView) {
var fixedFrame: CGRect = self.uiTopView.frame;
fixedFrame.origin.y = scrollView.contentOffset.y;
self.uiTopView.frame = fixedFrame;
}
But it does not work and I don't know why. Someone have any idea?
This can not be done, one way to accomplish this is to add the UITableViewController insideUIContainerView
So the structure will be as follows:
ViewController1 contains aUIContainerView this container view has embedded segue
to your tableViewController.
Then you can add the view to the ViewController1.
Why do you actually use UITableViewController instead of UIViewController with a tableView inside?
Maybe you should add your header view first then add you tableview depending on the header's frame.
for example: `import UIKit
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
var fixedLabel : UILabel!
var tableView : UITableView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.tableView.frame = CGRectMake(0, self.fixedLabel.frame.maxY, self.view.frame.width, self.view.frame.height-70)
self.fixedLabel.frame = CGRectMake(0,0,self.view.bounds.width,70)
}
override func viewDidLoad() {
super.viewDidLoad()
self.fixedLabel = UILabel()
self.fixedLabel.backgroundColor = UIColor.blueColor()
self.fixedLabel.text = "This is a fixedLabel"
self.fixedLabel.textAlignment = .Center
self.tableView = UITableView()
self.tableView.delegate = self
self.tableView.dataSource = self
self.view.addSubview(fixedLabel)
self.view.addSubview(tableView)
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : UITableViewCell? = tableView.dequeueReusableCellWithIdentifier("cell")
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
}
cell?.textLabel?.text = "Your text"
return cell!
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
}
`
I am working on a new application and I have some problems:
I am developing everything programmatically, so I created a SCROLLVIEW and a CONTAINERVIEW for all my pages. containerView is embedded in scrollView.
Every page has a different Controller, in this case "SearchViewController".
The containerView contains the UIView "selectView" that belongs to "SearchViewController". See below:
let scrollView: UIScrollView = UIScrollView()
let containerView: UIView = UIView()
//SCROLLVIEW
scrollView.frame = CGRectMake(0, 0, view.frame.width, view.frame.height)
self.view.addSubview(scrollView)
//CONTAINERVIEW
containerView.frame = CGRectMake(menuWidth, 0, view.frame.width, view.frame.height)
containerView.backgroundColor = UIColor.whiteColor()
scrollView.addSubview(containerView)
let s: SearchViewController = SearchViewController()
s.setup(bannerHeight, containerViewWidth: containerView.frame.width, containerViewHeight: containerView.frame.height)
containerView.addSubview(s.selectView)
import UIKit
class SearchViewController: UIViewController, UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource {
let selectView: UIView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
}
let table: UITableView = UITableView()
var items: [String] = ["A", "B", "C"]
var filteredItems = [String]()
func setup(bannerHeight: CGFloat, containerViewWidth: CGFloat, containerViewHeight: CGFloat){
//SELECT PAGE
selectView.frame = CGRectMake(0, bannerHeight, containerViewWidth, containerViewHeight - bannerHeight)
//UITABLEVIEW
table.frame = CGRectMake(0, 0, selectView.frame.width, selectView.frame.height)
table.delegate = self
table.dataSource = self
table.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
selectView.addSubview(table)
}
func numberOfSectionsInTableView(tableView:UITableView)->Int
{
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("tableViewCOUNT")
return self.items.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
print("tableViewMAIN")
let cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("cell")! as UITableViewCell
cell.textLabel!.text = items[indexPath.row]
cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator
return cell
}
}
So now i have this hierarchy: ScrollView<-containerView<-selectView<-table
The problem is that no data appear! In output I can see "tableViewCOUNT" but never "tableViewMAIN" and except for the empty rows there are no data.
Could somebody help me to understand why?
Am I using the correct way? I would like to have a mainController and for every page a subController, then I will embed the page I need inside the containerView (like an iframe in html).
Thank you in advance!
Try calling
table.reloadData()
in the setup.