I am using Swift 3. I have expandable table view cells but is it possible to get a different row height depending on which cell was clicked? For example, if the first cell is clicked, I want it to return 420 for height and if other cells are clicked, I want it to return 300 for height.
Here is my cell class.
class ResultsCell: UITableViewCell {
#IBOutlet weak var introPara : UITextView!
#IBOutlet weak var section_heading : UILabel!
class var expandedHeight : CGFloat = { get { return 420.0 } }
class var defaultHeight : CGFloat { get { return 44.0 } }
var frameAdded = false
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
section_heading.translatesAutoresizingMaskIntoConstraints = false
}
func checkHeight() {
introPara.isHidden = (frame.size.height < ResultsCell.expandedHeight)
}
func watchFrameChanges() {
if(!frameAdded) {
addObserver(self, forKeyPath: "frame", options: .new, context: nil)
checkHeight()
}
}
func ignoreFrameChanges() {
if(frameAdded){
removeObserver(self, forKeyPath: "frame")
}
}
deinit {
print("deinit called");
ignoreFrameChanges()
}
// when our frame changes, check if the frame height is appropriate and make it smaller or bigger depending
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "frame" {
checkHeight()
}
}
}
What I have tried is something like this.
var _expandedHeight : CGFloat = 420.0
class var expandedHeight : CGFloat { get { return _expandedHeight } set (newHeight) { _expandedHeight = newHeight } }
var isRow0 = false
override func awakeFromNib() {
super.awakeFromNib()
section_heading.translatesAutoresizingMaskIntoConstraints = false
if isRow0 {
ResultsCell.expandedHeight = 300.0
}
else {
ResultsCell.expandedHeight = 420.0
}
}
And then in the TableViewController...
// return the actual view for the cell
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let resultcell = tableView.dequeueReusableCell(withIdentifier: "resultCellTemplate", for: indexPath) as! ResultsCell
if indexPath.row == 0 {
resultcell.isRow0 = true
} else {
resultcell.isRow0 = false
}
return resultcell
}
But I'm getting errors on the getter/setter line: instance member _expandedHeight cannot be used on type ResultsCell
How can I achieve the behavior that I want?
Use tableview update methods to reevaluate each cell's height
self.tableView.beginUpdates()
self.tableView.endUpdates()
Changes to the tableview cells' heights will automatically be calculated with heightForRowAtIndexPath
try setting your height w.r.t indexpath
make user you mention the heightForRowAtIndexPath to calculate the right height.
You should do this in the delegate of your TableView.
Your TableView has a delegate private instance variable, whose value should implement protocol UITableViewDelegate. This protocol contains many functions which the UITableView calls in order to set its own appearance.
For more info:
The UITableViewDelegate protocol:
https://developer.apple.com/reference/uikit/uitableviewdelegate
The UITableView delegate instance variable:
https://developer.apple.com/reference/uikit/uitableview#delegate
What you want to do is to implement the tableView(_:heightForRowAt:) method on your UITableViewDelegate object. This method gets two parameters: The first is the UITableView itself, and the second is the IndexPath of the row being prepared for rendering. An IndexPath is an object which has section and row properties corresponding to the cell being rendered.
What you want to do is have some way of computing whether a cell is expanded or not based on its IndexPath.
Swift IndexPath reference: https://developer.apple.com/reference/foundation/indexpath
Trying to do this based on the UITableViewCell itself is an incorrect approach, as you usually should not analyze the cell before it is rendered.
A way to do this would be to have your UITableView-inheriting class keep a (possibly nested) array of booleans indicating which cells should be expanded or not. Then you could use the IndexPath's row and possibly section as indices into this array in order to determine whether the cell is expanded or not. Something like this:
class MyTableView: UITableView, UITableViewDelegate {
/* ... */
private var expanded: [Bool] = []
init() {
super.init()
self.delegate = self
}
func setData(data: [SomeType]) {
//set the data for the table here
expanded = [Bool](repeating: false, count: data.count)
}
/* ...logic to expand and collapse cells... */
func tableView(_ view: UITableView,
heightForRowAt indexPath: IndexPath) {
if expanded[indexPath.row] {
return expandedHeight
} else {
return collapsedHeight
}
}
/* ... */
}
The proper way to set your data would be to call a method on your UITableViewDataSource, but I'm assuming you'd set data on the TableView itself for the sake of simplicity.
Don't try to set the UITableView's properties by hand. The delegate and dataSource-based workflow of UITableViews is set up so that your table view works efficiently and only updates itself when it needs to.
Related
I am using TableView and on swiping the cell with editing style .delete but the size of cell has been customized as per the requirements of app. the delete Button height is little bigger how can we customize that.
Check the screenshot please:
#averydev update my code in swift you can try this
class CustomTableViewCell: UITableViewCell {
override func didTransition(to state: UITableViewCellStateMask) {
super.willTransition(to: state)
if state == .showingDeleteConfirmationMask {
let deleteButton: UIView? = subviews.first(where: { (aView) -> Bool in
return String(describing: aView).contains("Delete")
})
if deleteButton != nil {
deleteButton?.frame.size.height = 50.0
}
}
}
}
Currently, in iOS 16, the way to do it is by calling the willBeginEditingRowAt table view delegate method in your view controller class and changing the height property of the UISwipeActionPullView. I'm going to refer to your view controller class as NotesViewController here:
class NotesViewController: UIViewController {
...
extension NotesViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
// Get the subviews of the table view
let subviews = tableView.subviews
// Iterate through subviews to find _UITableViewCellSwipeContainerView
for subview in subviews {
if String(describing: type(of: subview)) == "_UITableViewCellSwipeContainerView" {
// Get the UISwipeActionPullView
if let swipeActionPullView = subview.subviews.first {
if String(describing: type(of: swipeActionPullView)) == "UISwipeActionPullView" {
// Set the height of the swipe action pull view (set to whatever height your custom cell is)
swipeActionPullView.frame.size.height = 50
}
}
}
}
}
}
}
I have a cell class 'NewsCell' (subclass of UITableViewCell) that I use for two different kinds of news: OrganizationNews and ProjectNews. These news has common things, but some of elements are different. Namely, when my cell is used for ProjectNews I want to hide Organization's logo, when it is for OrganizationNews I want to hide Project's name button.
I have 'configureCell(_, forNews, ofProject)' method. I call it in 'NewsViewController'. I used 'removeFromSuperview' method, because I need to rearrange my elements in 'NewsCell'. Changing 'isHidden' value won't give me that effect.
So, that is the issue. I have 'Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value' exception in the lines projectNameButton.removeFromSuperview() or logoImageView.removeFromSuperview().
What should I do?
// NewsViewController.swift
func configureCell(_ cell: NewsCell, forNews news: News, ofProject project: Project? = nil) {
//...
if news is OrganizationNews {
cell.projectNameButton.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView.removeFromSuperview()
}
// ...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell, for: indexPath) as! NewsCell
configureCell(cell, forNews: news)
cell.delegate = self
return cell
}
A UITableView or UICollectionView are built on the reuse concept, where the cells are reused and repopulated when you work on it.
When you try to call dequeReusableCell(withIdentifier:), it sometimes returns something that is created before. So, suppose you dequed before something which had all controls, then removed one (removeFromSuperview), then tried to deque again, the new dequed one may NOT have the subview.
I think the best solution for you is making two different cells.
Example:
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// Put here things that are ONLY for OrganizationNewsCell
}
class ProjectNewsCell: BaseNewsCell {
// Put here things that are ONLY for ProjectNewsCell
}
Then deque them from 2 different identifier by two different storyboard cells, xibs.
Or
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// This happens when this kind of cell is created for the first time
override func awakeFromNib() {
super.awakeFromNib()
someNonCommon.removeFromSuperview()
}
}
class ProjectNewsCell: BaseNewsCell {
override func awakeFromNib() {
super.awakeFromNib()
someOtherNonCommon.removeFromSuperview()
}
}
Note: This violates Liskov's principle (one of the SOLID principles), because you remove functionality from superclass in the subclass.
Change the removing lines as below,
if news is OrganizationNews {
cell.projectNameButton?.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView?.removeFromSuperview()
}
This will fix the issue. But a good approach would be to create separate classes for each cell. You can create a base class to keep common logic there.
You shouldn't remove the subview from the outside of the cell. Let's refactor your code.
NewsCell.swift
final class NewsCell: UITableViewCell {
enum Kind {
case organization
case project
}
var logoImageView: UIImageView?
let nameLabel = UILabel()
var kind: NewsCell.Kind {
didSet {
if kind != oldValue {
setupLogoImageView()
self.setNeedsLayout()
}
}
}
init(kind: NewsCell.Kind, reuseIdentifier: String?) {
self.kind = kind
super.init(style: .default, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Positioning
extension NewsCell {
override func layoutSubviews() {
super.layoutSubviews()
// Your layouting
switch kind {
case .organization:
// Setup frame for organization typed NewsCell
case .project:
// Setup frame for project typed NewsCell
}
}
}
// MARK: - Setup
extension NewsCell {
private func setupLogoImageView() {
logoImageView = kind == .organization ? UIImageView() : nil
}
}
How to use:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
var cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell) as? NewsCell
if cell == nil {
cell = NewsCell(kind: .organization, reuseIdentifier: TableViewCellIdentifiers.newsCell)
}
cell!.kind = news is Organization ? .organization: .project
return cell!
}
Can anyone help me to give reason to use delegate/protocol oriented or get superview, as I know swift use protocol oriented on code but for grab parent view or controller we still can use get superview like
Get superview example:
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
Use delegate example:
protocol SomeDelegate {
func didClick()
}
class Child {
var delegate: SomeDelegate?
}
What Pros and Cons to use delegate or get superview ?
Example for parentView:
class Cell {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.parentViewController.view.makeToast("Something !")
}
}
Example for delegate:
class Parent: SomeDelegate {
func didClick() {
self.view.makeToast("Something !")
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableview.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? Cell
cell.delegate = self
return cell
}
}
class Cell {
var label: UILabel
var delegate: SomeDelegate?
func configure() {
label.addGestureRecognizer(UILongPressGestureRecognizer(
target: self,
action: #selector(copyAction(_:))
))
}
#objc private func copyAction(_ sender: UILongPressGestureRecognizer) {
guard let delegate = self.delegate else {
return
}
delegate.didClick()
}
}
Delegate is preferred, not the superview. some reasons below
A view added in stack is not always retained in memory after its addition. especially when no strong reference maintained (this differs when view added from XIB or SB). So in this case calling superview might sometime crash with an unrecognized selector sent on some random instance.
One can create a view and never add to another view. ex for sake of removing you might comment just addsubview line leaving other code as is. At this time also the superview is nil.
Usage of custom views under uicontrols with own reusable view stack like Collectionview,TableView etc. would change superviews in runtime. so not always guaranteed to call same superview instance.
I have a tableView with cells populated by UIStackViews, with buttons as arrangedSubviews, that are created with a few functions.
The top view in the stackView is visible and all the other views are hidden, the button in the top views has an action, and when its called the other views should toggle between visible and hidden.
func generateButtons() -> [[UIButton]]{
var topButtonArray = [UIButton]()
var finalButtonArray = [[UIButton]]()
for title in array1 {
topButtonArray.append(createButton(title: title , action: "buttonPressed"))
}
for button in topButtonArray {
var buttonArray = [UIButton]()
buttonArray.append(button)
for title in array2 {
buttonArray.append(createButton(title: title, action: "moveOn"))
}
finalButtonArray.append(buttonArray)
}
return finalButtonArray
}
func generateStackViews() -> [UIStackView] {
stackViewArray = [UIStackView]()
let finalButtonArray = generateButtons()
for buttons in finalButtonArray{
stackViewArray.append(createStackView(subViews: buttons))
}
for stackView in stackViewArray{
let views = stackView.arrangedSubviews
let hiddenViews = views[1..<views.count]
for views in hiddenViews{
views.isHidden = true
}
}
return stackViewArray
}
func buttonPressed(){
//let stackViewArray = generateStackViews()
for stackView in stackViewArray{
let views = stackView.arrangedSubviews
let hiddenViews = views[1..<views.count]
for view in hiddenViews {
if view.isHidden == true{showViews(view: view)} else{hideViews(view: view)}
}
}
}
func showViews(view : UIView){
UIView.animate(withDuration: 0.3) {
view.isHidden = false
}
}
func hideViews(view : UIView) {
UIView.animate(withDuration: 0.2) {
view.isHidden = true
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "First")!
let stackViewArray = generateStackViews()
cell.contentView.addSubview(stackViewArray[indexPath.row])
return cell
}
Right now whats happening is that only the hidden views in the last cell are toggling between visible and hidden(no matter which cell i click) - I guess I need to instantiate the toggling on all cells but i cant figure out a way to do that.
another problem is that i want a top view to open only the hidden views in its cell, i figure i need to use indexPath.row somehow outside of 'cellForRowAt indexPath'.
You'll thank your sanity if you move a lot of this logic into a UITableViewCell subclass.
Not a complete rewrite of your snippet (hinting at setting up some of the views via storyboard, but no big difference to doing in code except without storyboard you'll also need to override the cell's init and set up the subviews), but here's a starting point that you could investigate:
class StackViewCell: UITableViewCell {
// these could be set up in code in the `init` method if
// you don't want to use storyboards
#IBOutlet var stackView: UIStackView!
#IBOutlet var toggleButton: UIButton!
var optionButtons: [UIButton] = [] {
didSet {
for button in optionButtons {
button.isHidden = optionsAreHidden
stackView.addArrangedSubview(button)
}
}
}
// iterates over buttons to change hidden property based on `optionsAreHidden` property
var optionsAreHidden: Bool = true {
didSet {
optionButtons.forEach({ $0.isHidden = optionsAreHidden })
}
}
#IBAction func toggleButtonPressed(button: UIButton) {
optionsAreHidden = !optionsAreHidden
}
// set up stackview and toggle button here if not setting up in storyboard
//init?(coder aDecoder: NSCoder) { }
}
Then the view controller becomes a lot simpler. It's not clear to me if each cell's stack view has the same set of option buttons, or if the option buttons are somehow contextual based on which row they're in.
If they're all the same I'd also move the generateOptionsButtons() logic into the StackViewCell (or actually if they're the same for each cell I'd probably set them up in the storyboard).
class OptionsViewController: UITableViewController {
func generateOptionsButtons() -> [UIButton] {
// create and return buttons for a cell
// potentially move this into `StackViewCell` too...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "StackViewCellIdentifier", for: indexPath)
if let stackViewCell = cell as? StackViewCell {
stackViewCell.optionButtons = generateOptionsButtons()
}
return cell
}
}
I want to implement a method that looks something like this:
setCellHeightForIndexPath(someIndexPath, 80)
and then the table view cell at that index path will suddenly have a height of 80.
The reason I want to do this is because I want the height of the cell to be set to the height of the web view's content after it has finished loading the HTML. Since I can only get the web view's content size after it has finished loading, I can't just set the cell height right away.
See this question for more info.
So in the webViewDidFinishLoad method, I can just get the web view's content height, set the web view's height to that, and call this method to set the cell's height.
It seems like that the cell height can only change when heightForRowAtIndexPath is called. I think the method would use a similar approach as my answer to this question. I think I need to store an array of heights maybe? I just can't think of a way to implement this!
How can I implement such a method?
Note: don't tell me this is not possible. In the Instagram app, I can see different images that have different heights fit perfectly in a table view cell. And those images are similar to my web views. They both need time to load.
EDIT:
Let me show some of my attempts at this:
var results: [Entry] = []
var cellHeights: [CGFloat] = []
var webViews: [UIWebView] = []
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return results.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("resultCell")
let webView = cell!.contentView.viewWithTag(1) as! UIWebView
webView.loadHTMLString(results[indexPath.row].htmlDescriptionForSearchMode(.TitleOnly), baseURL: nil)
webView.delegate = self
webView.scrollView.scrollEnabled = false
webViews.append(webView)
cellHeights.append(400)
webView.stringByEvaluatingJavaScriptFromString("highlightSearch(\"day\")")
return cell!
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return indexPath.row < cellHeights.count ? cellHeights[indexPath.row] : 400
}
func webViewDidFinishLoad(webView: UIWebView) {
let height = CGFloat(webView.stringByEvaluatingJavaScriptFromString("document.height")!.toFloat()!)
webView.frame = CGRect(origin: webView.frame.origin, size: CGSizeMake(webView.frame.width, height))
print(height)
if let index = webViews.indexesOf(webView).first {
cellHeights[index] = height
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .None)
tableView.endUpdates()
}
}
results is the stuff that I want to show in the web views. cellHeights is used to store the height of each cell. I put all the web views into the webViews array so I can call indexOf in webViewDidFinishLoad to identify which web view is loaded.
EDIT:
So I wrote this code in my table view controller with reference to Andre's answer:
class SearchResultsController: UITableViewController, UIWebViewDelegate {
var entries: [Entry] = []
lazy var results: [Result] = {
return self.entries.map { Result(entry: $0) }
}()
var cellHeights: [CGFloat] = []
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return results.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let result = results[indexPath.section]
var cell = result.cell
if cell == nil {
print("cellForRow called")
cell = tableView.dequeueReusableCellWithIdentifier("resultCell") as! ResultCell
cell.webView.delegate = self
print(cell == nil)
print("loading \(result.entry.title)...")
cell.webView.loadHTMLString(result.entry.htmlDescriptionForSearchMode(.TitleOnly), baseURL: nil)
result.cell = cell
}
return cell
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return indexPath.row < cellHeights.count ? cellHeights[indexPath.row] : 400
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 169
tableView.rowHeight = UITableViewAutomaticDimension
tableView.tableFooterView = UIView()
}
func webViewDidFinishLoad(webView: UIWebView) {
print("didFinishLoad called")
if webView.loading {
return
}
guard let cell = webView.superview?.superview as? ResultCell else {
print("could not get cell")
return
}
guard let index = results.map({$0.cell}).indexOf(cell) else {
print("could not get index")
return
}
let result = results[index]
print("finished loading \(result.entry.title)...")
guard let heightString = webView.stringByEvaluatingJavaScriptFromString("document.height") else {
print("could not get heightString")
return
}
guard let contentHeight = Float(heightString) else {
print("could not convert heightString")
return
}
cell.webViewHeightConstraint.constant = CGFloat(contentHeight)
tableView.beginUpdates()
tableView.endUpdates()
}
}
class ResultCell: UITableViewCell {
#IBOutlet weak var webView: UIWebView!
#IBOutlet weak var webViewHeightConstraint: NSLayoutConstraint!
}
class Result {
let entry: Entry
var contentHeight: Float?
var cell: ResultCell!
init(entry: Entry) {
self.entry = entry
}
}
You cannot "push" the new cell height onto a table view. Instead, you need to make table view "pull" the new height from your heightForRowAtIndexPath, and be ready to supply the new height.
When the cell load finishes for row r, you need to update your model in such a way that it knows the new height of row r. After that you need to tell your table view to reload itself, like this:
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
tableView.endUpdates()
This will start the process of updating your cell. heightForRowAtIndexPath will be called. Your code will return the new height. After that cellForRowAtIndexPath will be called. Your code should be prepared to return the cell that has finished loading, without initiating a new data load.
i tried implementing it by using automatic autolayout and automatic cell height calculation.
maybe it helps to point you into the right direction:
https://github.com/andreslotta/WebViewCellHeight
just an excerpt:
func webViewDidFinishLoad(webView: UIWebView) {
if webView.loading {
return
}
guard let cell = webView.superview?.superview as? WebViewCell else {
print("could not get cell")
return
}
guard let index = websites.map({$0.cell}).indexOf(cell) else {
print("could not get index")
return
}
// get website
let website = websites[index]
print("finished loading \(website.urlString)...")
// get contentheight from webview
guard let heightString = webView.stringByEvaluatingJavaScriptFromString("document.height") else {
print("could not get heightString")
return
}
guard let contentHeight = Float(heightString) else {
print("could not convert heightString")
return
}
cell.webViewHeightConstraint.constant = CGFloat(contentHeight)
tableView.beginUpdates()
tableView.endUpdates()
}
You can implement like this
Take one global CGFloat for height and one indexpath
now when you need to change height set both values and use
[self.yourTableview beginUpdate]
[self.yourTableview endUpdate]
will update your cell
and in cellForRowAtIndexPath you should use dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *) will make sure you got updated cell every time
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath == yourIndexpath) {
return gloablVariable;
} else {
return defauleHeight
}
}
Hope it helps