I have a very long list in my app. Each cell has multiple lines of text and a large picture, making scrolling through this list of 200+ items very tedious.
I wanted to implement a solution that would make scrolling easier, without implementing alphabetical scrolling as shown in the contacts app (since this would prevent me from allowing fast scrolling).
I wanted to implement a solution where, if you scrolling on the right side (to the point where your finger is hovering over the scroll bar on the right), than it would work the same as if you were clicking and dragging on the scroll bar on a desktop (meaning you could drag the scroll bar to the top or bottom very very quickly).
Is this a viable design for iOS? If so, what would be the best way to go about implementing it?
You could use a UISlider for that purpose. Here I created an example of a small tableview with 1000 rows and a slider to scroll across them: example
You can customize the UISlider to make it smaller, vertical, or whatever you want. Here's the viewControllers code, in case the file goes offline:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var slider: UISlider!
#IBOutlet weak var tableView: UITableView!
private let numberOfRows = 1000
override func viewDidLoad() {
super.viewDidLoad()
slider.maximumValue = Float(numberOfRows - 1) //due to rows goes from 0 to 999
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfRows
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier") as! UITableViewCell
cell.textLabel?.text = "\(indexPath.row)"
return cell;
}
#IBAction func sliderValueChanged(sender: UISlider) {
let positionToScroll = sender.value
tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: Int(positionToScroll), inSection: 0), atScrollPosition: UITableViewScrollPosition.None, animated: false)
}
}
Related
Hello, So I have implemented this behavior where UITableView PanGesture get dragged to fullscreen being in front of view behind (Pink View) by adding a PanGesture to UITableView gestures and I remove this gesture when the UITableView becomes fullscreen. After FullScreen TableView content can be scrolled.
What I want is the smooth transition when TableView gets dragged to FullScreen with the same drag movement that drag should automatically scroll the TableView content. Similarly when scroll downward if the tableview is showing the top row it should start scrolling down with the same gesture.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var bottomViewTopConstraint: NSLayoutConstraint!
#IBOutlet weak var bottomView: UIView!
let height = (UIScreen.main.bounds.height * 0.6) * -1;
var panGesture: UIPanGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
panGesture = UIPanGestureRecognizer(target: self, action: #selector(ViewController.draggedView(_:)))
bottomView.isUserInteractionEnabled = true
tableView.addGestureRecognizer(panGesture)
tableView.delegate = self
tableView.dataSource = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
bottomView.clipsToBounds = true
bottomView.layer.cornerRadius = 20
bottomView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
}
#objc func draggedView(_ sender:UIPanGestureRecognizer){
self.view.bringSubviewToFront(bottomView)
let translation = sender.translation(in: self.view)
bottomViewTopConstraint.constant += translation.y
if bottomViewTopConstraint.constant <= height {
print("getting here \(height)")
bottomViewTopConstraint.constant = height
tableView.removeGestureRecognizer(panGesture)
}
else if bottomViewTopConstraint.constant >= -24{
bottomViewTopConstraint.constant = -24
}
sender.setTranslation(CGPoint.zero, in: self.view)
view.layoutIfNeeded()
print(bottomViewTopConstraint.constant)
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 100
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y == -50 {
tableView.addGestureRecognizer(panGesture)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell : UITableViewCell!
cell = tableView.dequeueReusableCell(withIdentifier: "cell")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
}
cell.textLabel?.text = "Row \(indexPath.row)"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print ("Cell selected \(indexPath.row)")
}
}
In draggedView I am removing PanGesture so table could scroll its content but for this to work user has to let go off his current gesture and do drag gesture again and same is the case with scrollViewWillBeginDragging where I re-add the pan gesture. I want the scroll behavior to switch seamlessly like its one scroll.
I want to implement the behavior like this dribble animation https://dribbble.com/shots/6571883-Food-Delivery-App
What you need in this case is a nested scroll views structure. You should have total 3 scroll views.
Here is a link to such project that does the same thing as yours
Twitter Profile.
I will try to explain in a nutshell what you have to do here.
Scroll view 1 should be on first half of screen, 2nd scroll view (in your case, a table view) on second half of screen. 3rd scroll view is behind these two scroll views & will cover whole screen.
Scrolling on 1st & 2nd scroll views should be disabled, all scroll events are to be handled with 3rd scroll view.
3rd scroll view content size should be equal to sum of 1st & 2nd scroll views. Upon scrollViewDidScroll event, 3rd scroll view will update content offset of 1st or 2nd scroll view depending upon the position of content offset.
I had a similar thing to implement in the past. My advice is to first try to understand this nested scroll view structure. Once you completely understand this, you will be able to better solve any issues faced.
So I have 2 Views that are shown inside a UIView, based on what is selected on the SegmentViewController. I create dummy data, returning 20 row of a custom cell. This works great.
Everything is fine, till I interact with the TableView.
Bellow is my code:
GoalsViewController.swift
import UIKit
class GoalsViewController: UIViewController {
#IBOutlet weak var goalsTableView: UITableView!
let goalCellIdentifier = "goalCell"
override func viewDidLoad() {
super.viewDidLoad()
goalsTableView.delegate = self
goalsTableView.dataSource = self
goalsTableView.register(UINib(nibName: "GoalsViewCell", bundle: nil), forCellReuseIdentifier: goalCellIdentifier)
goalsTableView.reloadData()
}
}
extension GoalsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = goalsTableView.dequeueReusableCell(withIdentifier: goalCellIdentifier, for: indexPath) as! GoalsViewCell
cell.goalTitle.text = "aaaaa"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected \(indexPath.row)")
}
}
After any of the empty rows is selected, the didSelectRowAt is not called, so the cells are not there at all. I tried to find a solution, but I was only to find issues about empty lists, before being populated.
What could be the reason for the empty tableview?
I might be wrong here but one thing that I've noticed is that you are not implementing a function which sets the height of each cell.
// Specify the height of your cells
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100 // Or your given cell height.
}
So here is my theory: If you are using constraints something is missing and your cell's height can't be identified by constraints alone or you are not using constraints at all thus you must use heightForRowAt function to specify each cell's height.
I will explain what was the issue for people who probably did not know (like me).
So my UITableView is inside a UIView that changes based on what the user is selecting. In total I had 2 different Views that where switching. The reason that it was emptying it was because my parent ViewController, could not access the delegate for UITableView. To fix that, after adding a subview to the UIView, you need also to move the ViewController to the parent controller. In code it goes like this.
// Empty array of UIViewControllers
var views: [UIViewController]!
// Add the UIViewControllers to the array
views = [UIViewController()]
views.append(EventsViewController())
views.append(GoalsViewController())
for view in views {
// Needed to adjust the size of Subview to size of View
view.view.frame = containerView.bounds
// Add the subviews
containerView.addSubview(view.view)
}
// Bring the view in position 1 to the front of the UIView
containerView.bringSubviewToFront(views[1].view)
// Add Views[1] UIViewController as a child to the parent controller
self.addChild(views[1])
views[1].didMove(toParent: self)
// After done with everything with the UIViewController remove it
views[1].removeFromParent()
addChild Apple.com
didMove Apple.com
I have a tableview inside my UIViewController to display comments. The height of this tableview depends on the number and the size of comments. The cells are dynamic.
I use autolayout, so my tableview has a height constraint. I set this constraint programmatically :
override func viewDidLayoutSubviews() {
self.heightCommentsTableView.constant = self.commentsTableView.contentSize.height + 50
}
It works if I display all the comments at once.
BUT, I would like display the comments 5 per 5, using this method : load more for UITableView in swift because of performance issue to display my view
(all my comments are loaded before pushing the view)
I noticed when I set my constraint, it calls cellForRowAtIndexPath for all the comments, not only the first 5.
I don't know how to do.
EDIT
var allCommentsArray: NSMutableArray = []
var elements: NSMutableArray = []
var range = 5
var currentPage = 0
var nextpage = 0
override func viewDidLoad() {
super.viewDidLoad()
allCommentsArray = NSMutableArray(array: comments)
elements.addObjectsFromArray(allCommentsArray.subarrayWithRange(NSMakeRange(0, range)))
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return comments.count
}
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
self.nextpage = self.elements.count - 5
if indexPath.row == nextpage {
self.currentPage++
self.nextpage = self.elements.count - 5
self.elements.addObjectsFromArray(self.allCommentsArray.subarrayWithRange(NSMakeRange(self.currentPage, self.range)))
tableView.reloadData()
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CommentCustomTableViewCell", forIndexPath: indexPath) as! CommentCustomTableViewCell
self.configureCell(cell, atIndexPath: indexPath)
return cell
}
The table view will call tableView(_:cellForRowAtIndexPath:) for every visible cell it is about to create, the amount of it will determine based on how many sections and how many cells in each section. You let the table vie know these amounts by implementing the tableView(_:numberOfRowsInSection:) and numberOfSectionsInTableView(_:) methods of UITableViewDataSource. So, if you want to control how many cells could possibly be created and visible at a given time, you'd have to manage that state in your data source by adding and removing according to whatever logic you desire. In the answer that you linked, you can see that he is called elements.addObjectsFromArray to progressively add more elements in batches.
I've scoured through all of the questions here regarding a similar issue and I still seem to have this problem no matter what I do. I'm on iOS 8.3.
You can find the full code at: https://github.com/DanielRakh/CellHeightTest.git
I have a cell that dynamically resizes based on the content of a UILabel that varies from single to multi-line text. Everything seems to be fine initially but when I start scrolling fast and quickly switch directions
I get a warning in the debugger:
2015-03-10 02:02:00.630 CellHeight[21115:3711275] Warning once only:
Detected a case where constraints ambiguously suggest a height of zero
for a tableview cell's content view. We're considering the collapse
unintentional and using standard height instead.
Here's how it looks in the simulator. I've marked the discrepancies in red:
Here's the simple setup in IB where I have a UILabel pinned top(11), bottom(11), leading(8), and trailing(8) with a custom cell at 45pts:
Here is my code. The cell is a custom subclass thats pretty much empty except for the IBOutlet for the label:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var arr = [String]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
for _ in 1...10 {
arr.append("This is an amount of text.")
arr.append("This is an even larget amount of text. This should move to other lines of the label and mace the cell grow.")
arr.append("This is an even larger amount of text. That should move to other lines of the label and make the cell grow. Crossing fingers. This is an even larger amount of text. That should move to other lines of the label and make the cell grow. Crossing fingers.")
}
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 45.0
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
tableView.reloadData()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arr.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! TstTableViewCell
cell.tstLabel.text = arr[indexPath.row]
return cell
}
}
EDIT: I've found that if I created the cells programmatically and set the constraints programmatically everything seems to work as intended. Weird. This seems to be an IB + AL issue.
Update: My Problem was that the timer was not firing during the scroll and the fact that for a reason now unclear to me i was under the impression that scrollViewDidScroll would not fire all the time.
I have a UITableView with the same height as the containing view. While the UITableView content is moving i need to know which cell is closest to the vertical center of the containing view.
Things which could make a potential solution:
1. The number of items is fix
2. The cell-heights are all equal and known
I tried a number of solutions including reading the contentOffset, rectForRowAtIndexPath and indexPathForRowAtPoint.
Also this idea which seems to be the same problem:
Determine coordinates of a UITableViewCell while scrolling
But all this solution suffer from the fact that the values of those (contentOffset, rectForRowAtIndexPath and indexPathForRowAtPoint) are only updated after the UITableView stopped moving. But since i want to read data related to the cell in the middle of the screen also while it moves this is of no help.
Sidenote: I use a NSTimer Interval to check the offset.
Update (Code):
This is the code which atleast works after the table came to a rest. The setup is simple. iPad sized ViewController in Landscape, Table with the same height as the ViewController wired to self.tableView. That is all.
import UIKit
class ViewControllerTwoScroll: UIViewController {
let cellIdentifier = "cellIdentifier"
let cellHeight : CGFloat = 768 / 5
var tableData = [String]()
var magicNumber = 0
#IBOutlet var tableView: UITableView?
#IBOutlet var devLabelOffset: UILabel?
override func viewDidLoad() {
super.viewDidLoad()
self.tableView?.registerClass(UITableViewCell.self, forCellReuseIdentifier: self.cellIdentifier)
self.tableData.append("");
self.tableData.append("");
for index in 0...100 {
self.tableData.append("Item \(index)")
}
self.tableData.append("");
self.tableData.append("");
// Choose what you like more
self.tableView?.decelerationRate = UIScrollViewDecelerationRateNormal
//self.tableView?.decelerationRate = UIScrollViewDecelerationRateFast
/*** Timer to check the middel cell in a interval ***/
var timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("checkIndexFokus"), userInfo: nil, repeats: true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
/*** Table Basic-Stuff ***/
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
func tableView(tableView: UITableView!, willDisplayCell cell: UITableViewCell!, forRowAtIndexPath indexPath: NSIndexPath!) {
cell.backgroundColor = UIColor.clearColor()
cell.backgroundView = UIView()
cell.selectedBackgroundView = UIView()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier(self.cellIdentifier) as UITableViewCell
cell.textLabel?.text = self.tableData[indexPath.row]
return cell
}
func tableView( tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return cellHeight;
}
/*** Observe change of cell closest to the center ***/
func checkIndexFokus() {
/*** no solution yet. But if i would know the position of the first cell that
would be enough to calculate the cell that is actually in the middle ***/
var rectInTableView = self.tableView?.rectForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0))
rectInTableView = self.tableView?.convertRect(rectInTableView!, toView: self.tableView?.superview)
/*** this shows the correct value for the first cell as i can say but only after the table came to a rest ***/
self.devLabelOffset?.text = "\(rectInTableView!.origin.y)"
}
/*** Table snapping ***/
func scrollViewWillEndDragging(scrollView: UIScrollView!, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.memory.y = floor(targetContentOffset.memory.y / cellHeight) * cellHeight
}
}
contentOffset is updated during scrolling, so in the scrollViewDidScroll method you can get the value. You also say that all of the rows are the same height so a quick little bit of maths can find the row at that content offset (plus half the view height).
If your current attempts aren't working then it's more likely to be an issue with the timer, probably that it isn't firing during the scrolling. It isn't clear why you're using a timer, but you need to configure it appropriately (run loop) to fire at the same time as the scroll happens.
It's probably best not to use a timer and just do the calculation in the scroll delegate method (possibly with a conditional on doing it too often).
I expect that in the scroll delegate method indexPathForRowAtPoint should work and should be the least lines of code solution to the problem (because you always use the same point and don't worry directly about the content offset).