ios - Animated expanding TableView (tap on first row) inside TableView
I’m building something like a todo app where I have EXPANDABLE “slave” UITableView
inside “master” UITableViewCell
(reason is “material design of expandable “slave” table). Maybe is also relevant that this is all inside a container UIView
inside UIScrollView
embed in NavigationViewController
and TabViewController
. Pretty complex... Let me explain:
“Master”
UITableViewControler
with 2 section (this year/long term) with custom headers and customTableViewCell
Custom
TableViewCell
has aUIView
and “slave”UITableView
inside - underlyingUIView
is constrained to “slave”UITableView
and makes it’s “material” design with shadow (cropToBounds
prevents shadow onUITableView
)“Slave”
UITableView
should be with only one expanding section (I followed logic of this guy: https://www.youtube.com/watch?v=ClrSpJ3txAs) – tap on first row hides/show subviews and footer“Slave”
UITableView
has 3 customTableViewCell
(“header” populated always at first row, “subtask” starting on second and populated based on number of subtasks, “footer” always the last)
Picture of so far ugly UI might make it more clear:
Interface Builder setup
UI design
I am trying to use Interface Builder as much as possible (for me as a beginner it saves a lot of code and makes things more clear).
Code wise it is a bit complex architecture since I have a “Goal” realm object that has every time a list of “Subtask” objects. So the “master” UITableViewController
dataSource grabs a goal and pass it to “master’s” TableViewCell
(GoalMainCell) cell.goal = goals?[indexPath.row]
that is dataSource and Delegate for its outlet “slave” UITableView
. This way I can populate “slave” UITableView
with its correct subtasks from realm.
When I tried to have “master” UITableViewController
a dataSource & delegate of both tables I wasn’t able to populate subtasks properly (even setting tableView.tag
for each and bunch of if…else
statements – indexPath.row
can’t be taken as a goal's index since it starts from 0 for each “slave” tableView.row
)
class GoalsTableViewController: UITableViewController: (master tableviewcontroller)
let realm = try! Realm()
var goals: Results<Goal>?
var parentVc : OurTasksViewController?
var numberOfSubtasks: Int?
let longTermGoalsCount = 0
@IBOutlet var mainTableView: UITableView!
@IBOutlet weak var noGoalsLabel: UILabel!
override func viewDidLoad()
super.viewDidLoad()
loadGoals()
override func viewDidAppear(_ animated: Bool) // MUST be viewDidAppear
super.viewDidAppear(true)
parentVc = self.parent as? OurTasksViewController
func loadGoals()
override func numberOfSections(in tableView: UITableView) -> Int
if longTermGoalsCount > 0
//TODO: split goals to "this year" and "future" and show second section if future has some goals
return 2
else
return 1
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return goals?.count ?? 0
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalMainCell", for: indexPath) as! GoalsMainCell
cell.goal = goals?[indexPath.row] //send goal in to the GoalMainCell controller
cell.layoutIfNeeded()
return cell
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
if section == 0
let cell = tableView.dequeueReusableCell(withIdentifier: "ThisYearGoalsHeaderTableViewCell") as! ThisYearGoalsHeaderTableViewCell
cell.layoutIfNeeded()
return cell
else
let cell = tableView.dequeueReusableCell(withIdentifier: "LongTermGoalsHeaderCell")!
cell.layoutIfNeeded()
return cell
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
return UITableView.automaticDimension
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
return 44
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
return UITableView.automaticDimension
override func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
return 44
class GoalsMainCell: UITableViewCell (custom cell of a master table)
@IBOutlet weak var goalsSlaveTableView: GoalsSlaveTableView!
let realm = try! Realm()
var subtasks: Results<GoalSubtask>?
var numberOfRows: Int = 0
var goal: Goal? //goal passed from master tableView
didSet
loadSubtasks()
func loadSubtasks()
subtasks = goal?.subtasks.sorted(byKeyPath: "targetDate", ascending: true)
guard let expanded = goal?.expanded else
numberOfRows = 1
return
if expanded
numberOfRows = (subtasks?.count ?? 0) + 2
else
numberOfRows = 1
extension GoalsMainCell: UITableViewDataSource (custom cell of a master table)
func numberOfSections(in tableView: UITableView) -> Int return 1
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return numberOfRows
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
if indexPath.row == 0
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewHeaderCell", for: indexPath) as! GoalsSlaveTableViewHeaderCell
cell.goalNameLabel.text = goal?.name ?? "No task added"
cell.layoutIfNeeded()
return cell
else if indexPath.row > 0 && indexPath.row < numberOfRows - 1
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewCell", for: indexPath) as! GoalsSlaveTableViewCell
cell.subTaskNameLabel.text = subtasks?[indexPath.row - 1].name //because first row is main goal name
cell.layoutIfNeeded()
return cell
else
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewFooterCell", for: indexPath) as! GoalsSlaveTableViewFooterCell
cell.layoutIfNeeded()
return cell
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
return UITableView.automaticDimension
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
return 44
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
return 0
func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
return 0
extension GoalsMainCell: UITableViewDelegate (custom cell of a master table)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
if indexPath.row == 0
if goal != nil
do
try realm.write
goal!.expanded = !goal!.expanded
catch
print("Error saving done status, (error)")
let section = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(section, with: .none)
tableView.deselectRow(at: indexPath, animated: true)
class GoalsSlaveTableView: UITableView (slave tableViewController)
override func layoutSubviews()
super.layoutSubviews()
self.layer.cornerRadius = cornerRadius
override var intrinsicContentSize: CGSize
self.layoutIfNeeded()
return self.contentSize
override var contentSize: CGSize
didSet
self.invalidateIntrinsicContentSize()
class GoalsSlaveTableViewCell: UITableViewCell (cell of the slave table)
@IBOutlet weak var subTaskNameLabel: UILabel!
@IBOutlet weak var subTaskTargetDate: UILabel!
@IBOutlet weak var subTaskDoneImage: UIImageView!
class GoalsSlaveTableViewHeaderCell: UITableViewCell (header of the slave table - actually also a custom cell)
@IBOutlet weak var goalNameLabel: UILabel!
@IBOutlet weak var goalTargetDate: UILabel!
@IBOutlet weak var goalProgressBar: UIProgressView!
class GoalsSlaveTableViewFooterCell: UITableViewCell (and footer for the slave)
@IBAction func deleteGoalButtonTapped(_ sender: UIButton)
print("Delete goal")
@IBAction func editGoalButtonTapped(_ sender: UIButton)
print("Edit goal")
Question: How to call reload data with animation (for both if necessary) table views after expanding/collapsing?
I got it working pretty much as I want from the look. The only and probably the most tricky thing missing is the auto-reload / adjust of the “master” cell and “slave” tableView size upon expanding/collapsing. In other words: When I tap the first row in “slave” table the data in Realm get updated (“expanded” bool property) but I have to terminate the app and launch again to get the layout set up (I believe viewDidLoad has to run). Just a few links I used for inspiration, but I found none that would explain how to expand/colapse and call reload of both in terms of size with a nice animation during a runtime:
Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
Is it possible to implement tableview inside tableview cell in swift 3?
Is it possible to add UITableView within a UITableViewCell
TableView inside tableview cell swift 3
TableView Automatic Dimension Tableview Inside Tableview
Reload a tableView inside viewController
As a beginner I might be doing some really simple mistake like missing some auto-layout constrains in IB and therefore calling invalidateIntrinsicContentSize()
… Or maybe with this architecture it is not possible to do animated smooth table reloads since there will be always conflict of "reloads"...? Hopefully there is someone out there to help me. Thank you for any help!
ios swift uitableview
add a comment |
I’m building something like a todo app where I have EXPANDABLE “slave” UITableView
inside “master” UITableViewCell
(reason is “material design of expandable “slave” table). Maybe is also relevant that this is all inside a container UIView
inside UIScrollView
embed in NavigationViewController
and TabViewController
. Pretty complex... Let me explain:
“Master”
UITableViewControler
with 2 section (this year/long term) with custom headers and customTableViewCell
Custom
TableViewCell
has aUIView
and “slave”UITableView
inside - underlyingUIView
is constrained to “slave”UITableView
and makes it’s “material” design with shadow (cropToBounds
prevents shadow onUITableView
)“Slave”
UITableView
should be with only one expanding section (I followed logic of this guy: https://www.youtube.com/watch?v=ClrSpJ3txAs) – tap on first row hides/show subviews and footer“Slave”
UITableView
has 3 customTableViewCell
(“header” populated always at first row, “subtask” starting on second and populated based on number of subtasks, “footer” always the last)
Picture of so far ugly UI might make it more clear:
Interface Builder setup
UI design
I am trying to use Interface Builder as much as possible (for me as a beginner it saves a lot of code and makes things more clear).
Code wise it is a bit complex architecture since I have a “Goal” realm object that has every time a list of “Subtask” objects. So the “master” UITableViewController
dataSource grabs a goal and pass it to “master’s” TableViewCell
(GoalMainCell) cell.goal = goals?[indexPath.row]
that is dataSource and Delegate for its outlet “slave” UITableView
. This way I can populate “slave” UITableView
with its correct subtasks from realm.
When I tried to have “master” UITableViewController
a dataSource & delegate of both tables I wasn’t able to populate subtasks properly (even setting tableView.tag
for each and bunch of if…else
statements – indexPath.row
can’t be taken as a goal's index since it starts from 0 for each “slave” tableView.row
)
class GoalsTableViewController: UITableViewController: (master tableviewcontroller)
let realm = try! Realm()
var goals: Results<Goal>?
var parentVc : OurTasksViewController?
var numberOfSubtasks: Int?
let longTermGoalsCount = 0
@IBOutlet var mainTableView: UITableView!
@IBOutlet weak var noGoalsLabel: UILabel!
override func viewDidLoad()
super.viewDidLoad()
loadGoals()
override func viewDidAppear(_ animated: Bool) // MUST be viewDidAppear
super.viewDidAppear(true)
parentVc = self.parent as? OurTasksViewController
func loadGoals()
override func numberOfSections(in tableView: UITableView) -> Int
if longTermGoalsCount > 0
//TODO: split goals to "this year" and "future" and show second section if future has some goals
return 2
else
return 1
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return goals?.count ?? 0
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalMainCell", for: indexPath) as! GoalsMainCell
cell.goal = goals?[indexPath.row] //send goal in to the GoalMainCell controller
cell.layoutIfNeeded()
return cell
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
if section == 0
let cell = tableView.dequeueReusableCell(withIdentifier: "ThisYearGoalsHeaderTableViewCell") as! ThisYearGoalsHeaderTableViewCell
cell.layoutIfNeeded()
return cell
else
let cell = tableView.dequeueReusableCell(withIdentifier: "LongTermGoalsHeaderCell")!
cell.layoutIfNeeded()
return cell
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
return UITableView.automaticDimension
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
return 44
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
return UITableView.automaticDimension
override func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
return 44
class GoalsMainCell: UITableViewCell (custom cell of a master table)
@IBOutlet weak var goalsSlaveTableView: GoalsSlaveTableView!
let realm = try! Realm()
var subtasks: Results<GoalSubtask>?
var numberOfRows: Int = 0
var goal: Goal? //goal passed from master tableView
didSet
loadSubtasks()
func loadSubtasks()
subtasks = goal?.subtasks.sorted(byKeyPath: "targetDate", ascending: true)
guard let expanded = goal?.expanded else
numberOfRows = 1
return
if expanded
numberOfRows = (subtasks?.count ?? 0) + 2
else
numberOfRows = 1
extension GoalsMainCell: UITableViewDataSource (custom cell of a master table)
func numberOfSections(in tableView: UITableView) -> Int return 1
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return numberOfRows
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
if indexPath.row == 0
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewHeaderCell", for: indexPath) as! GoalsSlaveTableViewHeaderCell
cell.goalNameLabel.text = goal?.name ?? "No task added"
cell.layoutIfNeeded()
return cell
else if indexPath.row > 0 && indexPath.row < numberOfRows - 1
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewCell", for: indexPath) as! GoalsSlaveTableViewCell
cell.subTaskNameLabel.text = subtasks?[indexPath.row - 1].name //because first row is main goal name
cell.layoutIfNeeded()
return cell
else
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewFooterCell", for: indexPath) as! GoalsSlaveTableViewFooterCell
cell.layoutIfNeeded()
return cell
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
return UITableView.automaticDimension
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
return 44
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
return 0
func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
return 0
extension GoalsMainCell: UITableViewDelegate (custom cell of a master table)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
if indexPath.row == 0
if goal != nil
do
try realm.write
goal!.expanded = !goal!.expanded
catch
print("Error saving done status, (error)")
let section = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(section, with: .none)
tableView.deselectRow(at: indexPath, animated: true)
class GoalsSlaveTableView: UITableView (slave tableViewController)
override func layoutSubviews()
super.layoutSubviews()
self.layer.cornerRadius = cornerRadius
override var intrinsicContentSize: CGSize
self.layoutIfNeeded()
return self.contentSize
override var contentSize: CGSize
didSet
self.invalidateIntrinsicContentSize()
class GoalsSlaveTableViewCell: UITableViewCell (cell of the slave table)
@IBOutlet weak var subTaskNameLabel: UILabel!
@IBOutlet weak var subTaskTargetDate: UILabel!
@IBOutlet weak var subTaskDoneImage: UIImageView!
class GoalsSlaveTableViewHeaderCell: UITableViewCell (header of the slave table - actually also a custom cell)
@IBOutlet weak var goalNameLabel: UILabel!
@IBOutlet weak var goalTargetDate: UILabel!
@IBOutlet weak var goalProgressBar: UIProgressView!
class GoalsSlaveTableViewFooterCell: UITableViewCell (and footer for the slave)
@IBAction func deleteGoalButtonTapped(_ sender: UIButton)
print("Delete goal")
@IBAction func editGoalButtonTapped(_ sender: UIButton)
print("Edit goal")
Question: How to call reload data with animation (for both if necessary) table views after expanding/collapsing?
I got it working pretty much as I want from the look. The only and probably the most tricky thing missing is the auto-reload / adjust of the “master” cell and “slave” tableView size upon expanding/collapsing. In other words: When I tap the first row in “slave” table the data in Realm get updated (“expanded” bool property) but I have to terminate the app and launch again to get the layout set up (I believe viewDidLoad has to run). Just a few links I used for inspiration, but I found none that would explain how to expand/colapse and call reload of both in terms of size with a nice animation during a runtime:
Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
Is it possible to implement tableview inside tableview cell in swift 3?
Is it possible to add UITableView within a UITableViewCell
TableView inside tableview cell swift 3
TableView Automatic Dimension Tableview Inside Tableview
Reload a tableView inside viewController
As a beginner I might be doing some really simple mistake like missing some auto-layout constrains in IB and therefore calling invalidateIntrinsicContentSize()
… Or maybe with this architecture it is not possible to do animated smooth table reloads since there will be always conflict of "reloads"...? Hopefully there is someone out there to help me. Thank you for any help!
ios swift uitableview
add a comment |
I’m building something like a todo app where I have EXPANDABLE “slave” UITableView
inside “master” UITableViewCell
(reason is “material design of expandable “slave” table). Maybe is also relevant that this is all inside a container UIView
inside UIScrollView
embed in NavigationViewController
and TabViewController
. Pretty complex... Let me explain:
“Master”
UITableViewControler
with 2 section (this year/long term) with custom headers and customTableViewCell
Custom
TableViewCell
has aUIView
and “slave”UITableView
inside - underlyingUIView
is constrained to “slave”UITableView
and makes it’s “material” design with shadow (cropToBounds
prevents shadow onUITableView
)“Slave”
UITableView
should be with only one expanding section (I followed logic of this guy: https://www.youtube.com/watch?v=ClrSpJ3txAs) – tap on first row hides/show subviews and footer“Slave”
UITableView
has 3 customTableViewCell
(“header” populated always at first row, “subtask” starting on second and populated based on number of subtasks, “footer” always the last)
Picture of so far ugly UI might make it more clear:
Interface Builder setup
UI design
I am trying to use Interface Builder as much as possible (for me as a beginner it saves a lot of code and makes things more clear).
Code wise it is a bit complex architecture since I have a “Goal” realm object that has every time a list of “Subtask” objects. So the “master” UITableViewController
dataSource grabs a goal and pass it to “master’s” TableViewCell
(GoalMainCell) cell.goal = goals?[indexPath.row]
that is dataSource and Delegate for its outlet “slave” UITableView
. This way I can populate “slave” UITableView
with its correct subtasks from realm.
When I tried to have “master” UITableViewController
a dataSource & delegate of both tables I wasn’t able to populate subtasks properly (even setting tableView.tag
for each and bunch of if…else
statements – indexPath.row
can’t be taken as a goal's index since it starts from 0 for each “slave” tableView.row
)
class GoalsTableViewController: UITableViewController: (master tableviewcontroller)
let realm = try! Realm()
var goals: Results<Goal>?
var parentVc : OurTasksViewController?
var numberOfSubtasks: Int?
let longTermGoalsCount = 0
@IBOutlet var mainTableView: UITableView!
@IBOutlet weak var noGoalsLabel: UILabel!
override func viewDidLoad()
super.viewDidLoad()
loadGoals()
override func viewDidAppear(_ animated: Bool) // MUST be viewDidAppear
super.viewDidAppear(true)
parentVc = self.parent as? OurTasksViewController
func loadGoals()
override func numberOfSections(in tableView: UITableView) -> Int
if longTermGoalsCount > 0
//TODO: split goals to "this year" and "future" and show second section if future has some goals
return 2
else
return 1
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return goals?.count ?? 0
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalMainCell", for: indexPath) as! GoalsMainCell
cell.goal = goals?[indexPath.row] //send goal in to the GoalMainCell controller
cell.layoutIfNeeded()
return cell
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
if section == 0
let cell = tableView.dequeueReusableCell(withIdentifier: "ThisYearGoalsHeaderTableViewCell") as! ThisYearGoalsHeaderTableViewCell
cell.layoutIfNeeded()
return cell
else
let cell = tableView.dequeueReusableCell(withIdentifier: "LongTermGoalsHeaderCell")!
cell.layoutIfNeeded()
return cell
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
return UITableView.automaticDimension
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
return 44
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
return UITableView.automaticDimension
override func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
return 44
class GoalsMainCell: UITableViewCell (custom cell of a master table)
@IBOutlet weak var goalsSlaveTableView: GoalsSlaveTableView!
let realm = try! Realm()
var subtasks: Results<GoalSubtask>?
var numberOfRows: Int = 0
var goal: Goal? //goal passed from master tableView
didSet
loadSubtasks()
func loadSubtasks()
subtasks = goal?.subtasks.sorted(byKeyPath: "targetDate", ascending: true)
guard let expanded = goal?.expanded else
numberOfRows = 1
return
if expanded
numberOfRows = (subtasks?.count ?? 0) + 2
else
numberOfRows = 1
extension GoalsMainCell: UITableViewDataSource (custom cell of a master table)
func numberOfSections(in tableView: UITableView) -> Int return 1
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return numberOfRows
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
if indexPath.row == 0
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewHeaderCell", for: indexPath) as! GoalsSlaveTableViewHeaderCell
cell.goalNameLabel.text = goal?.name ?? "No task added"
cell.layoutIfNeeded()
return cell
else if indexPath.row > 0 && indexPath.row < numberOfRows - 1
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewCell", for: indexPath) as! GoalsSlaveTableViewCell
cell.subTaskNameLabel.text = subtasks?[indexPath.row - 1].name //because first row is main goal name
cell.layoutIfNeeded()
return cell
else
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewFooterCell", for: indexPath) as! GoalsSlaveTableViewFooterCell
cell.layoutIfNeeded()
return cell
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
return UITableView.automaticDimension
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
return 44
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
return 0
func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
return 0
extension GoalsMainCell: UITableViewDelegate (custom cell of a master table)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
if indexPath.row == 0
if goal != nil
do
try realm.write
goal!.expanded = !goal!.expanded
catch
print("Error saving done status, (error)")
let section = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(section, with: .none)
tableView.deselectRow(at: indexPath, animated: true)
class GoalsSlaveTableView: UITableView (slave tableViewController)
override func layoutSubviews()
super.layoutSubviews()
self.layer.cornerRadius = cornerRadius
override var intrinsicContentSize: CGSize
self.layoutIfNeeded()
return self.contentSize
override var contentSize: CGSize
didSet
self.invalidateIntrinsicContentSize()
class GoalsSlaveTableViewCell: UITableViewCell (cell of the slave table)
@IBOutlet weak var subTaskNameLabel: UILabel!
@IBOutlet weak var subTaskTargetDate: UILabel!
@IBOutlet weak var subTaskDoneImage: UIImageView!
class GoalsSlaveTableViewHeaderCell: UITableViewCell (header of the slave table - actually also a custom cell)
@IBOutlet weak var goalNameLabel: UILabel!
@IBOutlet weak var goalTargetDate: UILabel!
@IBOutlet weak var goalProgressBar: UIProgressView!
class GoalsSlaveTableViewFooterCell: UITableViewCell (and footer for the slave)
@IBAction func deleteGoalButtonTapped(_ sender: UIButton)
print("Delete goal")
@IBAction func editGoalButtonTapped(_ sender: UIButton)
print("Edit goal")
Question: How to call reload data with animation (for both if necessary) table views after expanding/collapsing?
I got it working pretty much as I want from the look. The only and probably the most tricky thing missing is the auto-reload / adjust of the “master” cell and “slave” tableView size upon expanding/collapsing. In other words: When I tap the first row in “slave” table the data in Realm get updated (“expanded” bool property) but I have to terminate the app and launch again to get the layout set up (I believe viewDidLoad has to run). Just a few links I used for inspiration, but I found none that would explain how to expand/colapse and call reload of both in terms of size with a nice animation during a runtime:
Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
Is it possible to implement tableview inside tableview cell in swift 3?
Is it possible to add UITableView within a UITableViewCell
TableView inside tableview cell swift 3
TableView Automatic Dimension Tableview Inside Tableview
Reload a tableView inside viewController
As a beginner I might be doing some really simple mistake like missing some auto-layout constrains in IB and therefore calling invalidateIntrinsicContentSize()
… Or maybe with this architecture it is not possible to do animated smooth table reloads since there will be always conflict of "reloads"...? Hopefully there is someone out there to help me. Thank you for any help!
ios swift uitableview
I’m building something like a todo app where I have EXPANDABLE “slave” UITableView
inside “master” UITableViewCell
(reason is “material design of expandable “slave” table). Maybe is also relevant that this is all inside a container UIView
inside UIScrollView
embed in NavigationViewController
and TabViewController
. Pretty complex... Let me explain:
“Master”
UITableViewControler
with 2 section (this year/long term) with custom headers and customTableViewCell
Custom
TableViewCell
has aUIView
and “slave”UITableView
inside - underlyingUIView
is constrained to “slave”UITableView
and makes it’s “material” design with shadow (cropToBounds
prevents shadow onUITableView
)“Slave”
UITableView
should be with only one expanding section (I followed logic of this guy: https://www.youtube.com/watch?v=ClrSpJ3txAs) – tap on first row hides/show subviews and footer“Slave”
UITableView
has 3 customTableViewCell
(“header” populated always at first row, “subtask” starting on second and populated based on number of subtasks, “footer” always the last)
Picture of so far ugly UI might make it more clear:
Interface Builder setup
UI design
I am trying to use Interface Builder as much as possible (for me as a beginner it saves a lot of code and makes things more clear).
Code wise it is a bit complex architecture since I have a “Goal” realm object that has every time a list of “Subtask” objects. So the “master” UITableViewController
dataSource grabs a goal and pass it to “master’s” TableViewCell
(GoalMainCell) cell.goal = goals?[indexPath.row]
that is dataSource and Delegate for its outlet “slave” UITableView
. This way I can populate “slave” UITableView
with its correct subtasks from realm.
When I tried to have “master” UITableViewController
a dataSource & delegate of both tables I wasn’t able to populate subtasks properly (even setting tableView.tag
for each and bunch of if…else
statements – indexPath.row
can’t be taken as a goal's index since it starts from 0 for each “slave” tableView.row
)
class GoalsTableViewController: UITableViewController: (master tableviewcontroller)
let realm = try! Realm()
var goals: Results<Goal>?
var parentVc : OurTasksViewController?
var numberOfSubtasks: Int?
let longTermGoalsCount = 0
@IBOutlet var mainTableView: UITableView!
@IBOutlet weak var noGoalsLabel: UILabel!
override func viewDidLoad()
super.viewDidLoad()
loadGoals()
override func viewDidAppear(_ animated: Bool) // MUST be viewDidAppear
super.viewDidAppear(true)
parentVc = self.parent as? OurTasksViewController
func loadGoals()
override func numberOfSections(in tableView: UITableView) -> Int
if longTermGoalsCount > 0
//TODO: split goals to "this year" and "future" and show second section if future has some goals
return 2
else
return 1
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return goals?.count ?? 0
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalMainCell", for: indexPath) as! GoalsMainCell
cell.goal = goals?[indexPath.row] //send goal in to the GoalMainCell controller
cell.layoutIfNeeded()
return cell
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
if section == 0
let cell = tableView.dequeueReusableCell(withIdentifier: "ThisYearGoalsHeaderTableViewCell") as! ThisYearGoalsHeaderTableViewCell
cell.layoutIfNeeded()
return cell
else
let cell = tableView.dequeueReusableCell(withIdentifier: "LongTermGoalsHeaderCell")!
cell.layoutIfNeeded()
return cell
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
return UITableView.automaticDimension
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
return 44
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
return UITableView.automaticDimension
override func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
return 44
class GoalsMainCell: UITableViewCell (custom cell of a master table)
@IBOutlet weak var goalsSlaveTableView: GoalsSlaveTableView!
let realm = try! Realm()
var subtasks: Results<GoalSubtask>?
var numberOfRows: Int = 0
var goal: Goal? //goal passed from master tableView
didSet
loadSubtasks()
func loadSubtasks()
subtasks = goal?.subtasks.sorted(byKeyPath: "targetDate", ascending: true)
guard let expanded = goal?.expanded else
numberOfRows = 1
return
if expanded
numberOfRows = (subtasks?.count ?? 0) + 2
else
numberOfRows = 1
extension GoalsMainCell: UITableViewDataSource (custom cell of a master table)
func numberOfSections(in tableView: UITableView) -> Int return 1
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
return numberOfRows
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
if indexPath.row == 0
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewHeaderCell", for: indexPath) as! GoalsSlaveTableViewHeaderCell
cell.goalNameLabel.text = goal?.name ?? "No task added"
cell.layoutIfNeeded()
return cell
else if indexPath.row > 0 && indexPath.row < numberOfRows - 1
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewCell", for: indexPath) as! GoalsSlaveTableViewCell
cell.subTaskNameLabel.text = subtasks?[indexPath.row - 1].name //because first row is main goal name
cell.layoutIfNeeded()
return cell
else
let cell = tableView.dequeueReusableCell(withIdentifier: "GoalsSlaveTableViewFooterCell", for: indexPath) as! GoalsSlaveTableViewFooterCell
cell.layoutIfNeeded()
return cell
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
return UITableView.automaticDimension
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
return 44
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
return 0
func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
return 0
extension GoalsMainCell: UITableViewDelegate (custom cell of a master table)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
if indexPath.row == 0
if goal != nil
do
try realm.write
goal!.expanded = !goal!.expanded
catch
print("Error saving done status, (error)")
let section = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(section, with: .none)
tableView.deselectRow(at: indexPath, animated: true)
class GoalsSlaveTableView: UITableView (slave tableViewController)
override func layoutSubviews()
super.layoutSubviews()
self.layer.cornerRadius = cornerRadius
override var intrinsicContentSize: CGSize
self.layoutIfNeeded()
return self.contentSize
override var contentSize: CGSize
didSet
self.invalidateIntrinsicContentSize()
class GoalsSlaveTableViewCell: UITableViewCell (cell of the slave table)
@IBOutlet weak var subTaskNameLabel: UILabel!
@IBOutlet weak var subTaskTargetDate: UILabel!
@IBOutlet weak var subTaskDoneImage: UIImageView!
class GoalsSlaveTableViewHeaderCell: UITableViewCell (header of the slave table - actually also a custom cell)
@IBOutlet weak var goalNameLabel: UILabel!
@IBOutlet weak var goalTargetDate: UILabel!
@IBOutlet weak var goalProgressBar: UIProgressView!
class GoalsSlaveTableViewFooterCell: UITableViewCell (and footer for the slave)
@IBAction func deleteGoalButtonTapped(_ sender: UIButton)
print("Delete goal")
@IBAction func editGoalButtonTapped(_ sender: UIButton)
print("Edit goal")
Question: How to call reload data with animation (for both if necessary) table views after expanding/collapsing?
I got it working pretty much as I want from the look. The only and probably the most tricky thing missing is the auto-reload / adjust of the “master” cell and “slave” tableView size upon expanding/collapsing. In other words: When I tap the first row in “slave” table the data in Realm get updated (“expanded” bool property) but I have to terminate the app and launch again to get the layout set up (I believe viewDidLoad has to run). Just a few links I used for inspiration, but I found none that would explain how to expand/colapse and call reload of both in terms of size with a nice animation during a runtime:
Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
Is it possible to implement tableview inside tableview cell in swift 3?
Is it possible to add UITableView within a UITableViewCell
TableView inside tableview cell swift 3
TableView Automatic Dimension Tableview Inside Tableview
Reload a tableView inside viewController
As a beginner I might be doing some really simple mistake like missing some auto-layout constrains in IB and therefore calling invalidateIntrinsicContentSize()
… Or maybe with this architecture it is not possible to do animated smooth table reloads since there will be always conflict of "reloads"...? Hopefully there is someone out there to help me. Thank you for any help!
ios swift uitableview
ios swift uitableview
asked Nov 12 '18 at 16:28
Lukas SmilekLukas Smilek
613
613
add a comment |
add a comment |
1 Answer
1
active
oldest
votes
I solved the animation/update.
1) I forgot to reload data after changing .expanded
property:
extension GoalsMainCell: UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
let goalNotificationInfo = ["index" : goalIndex ]
if indexPath.row == 0
if goal != nil
do
try realm.write
goal!.expanded = !goal!.expanded
catch
print("Error saving done status, (error)")
loadSubtasks()
tableView.deselectRow(at: indexPath, animated: true)
let section = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(section, with: .none)
NotificationCenter.default.post(name: .goalNotKey, object: nil, userInfo: goalNotificationInfo as [AnyHashable : Any])
2) I added notification observer to the slave table delegate and call .beginUpdate()
+ .endUpdate()
@objc func updateGoalSection(_ notification: Notification)
tableView.beginUpdates()
tableView.endUpdates()
Now the update works with a smooth transition. Of coarse solving this revealed another issues with indexing and main call automatic dimensions but that is another topic...
Hope it helpes other to struggle less.
add a comment |
Your Answer
StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");
StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "1"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);
else
createEditor();
);
function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);
);
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53266333%2fios-animated-expanding-tableview-tap-on-first-row-inside-tableview%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
I solved the animation/update.
1) I forgot to reload data after changing .expanded
property:
extension GoalsMainCell: UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
let goalNotificationInfo = ["index" : goalIndex ]
if indexPath.row == 0
if goal != nil
do
try realm.write
goal!.expanded = !goal!.expanded
catch
print("Error saving done status, (error)")
loadSubtasks()
tableView.deselectRow(at: indexPath, animated: true)
let section = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(section, with: .none)
NotificationCenter.default.post(name: .goalNotKey, object: nil, userInfo: goalNotificationInfo as [AnyHashable : Any])
2) I added notification observer to the slave table delegate and call .beginUpdate()
+ .endUpdate()
@objc func updateGoalSection(_ notification: Notification)
tableView.beginUpdates()
tableView.endUpdates()
Now the update works with a smooth transition. Of coarse solving this revealed another issues with indexing and main call automatic dimensions but that is another topic...
Hope it helpes other to struggle less.
add a comment |
I solved the animation/update.
1) I forgot to reload data after changing .expanded
property:
extension GoalsMainCell: UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
let goalNotificationInfo = ["index" : goalIndex ]
if indexPath.row == 0
if goal != nil
do
try realm.write
goal!.expanded = !goal!.expanded
catch
print("Error saving done status, (error)")
loadSubtasks()
tableView.deselectRow(at: indexPath, animated: true)
let section = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(section, with: .none)
NotificationCenter.default.post(name: .goalNotKey, object: nil, userInfo: goalNotificationInfo as [AnyHashable : Any])
2) I added notification observer to the slave table delegate and call .beginUpdate()
+ .endUpdate()
@objc func updateGoalSection(_ notification: Notification)
tableView.beginUpdates()
tableView.endUpdates()
Now the update works with a smooth transition. Of coarse solving this revealed another issues with indexing and main call automatic dimensions but that is another topic...
Hope it helpes other to struggle less.
add a comment |
I solved the animation/update.
1) I forgot to reload data after changing .expanded
property:
extension GoalsMainCell: UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
let goalNotificationInfo = ["index" : goalIndex ]
if indexPath.row == 0
if goal != nil
do
try realm.write
goal!.expanded = !goal!.expanded
catch
print("Error saving done status, (error)")
loadSubtasks()
tableView.deselectRow(at: indexPath, animated: true)
let section = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(section, with: .none)
NotificationCenter.default.post(name: .goalNotKey, object: nil, userInfo: goalNotificationInfo as [AnyHashable : Any])
2) I added notification observer to the slave table delegate and call .beginUpdate()
+ .endUpdate()
@objc func updateGoalSection(_ notification: Notification)
tableView.beginUpdates()
tableView.endUpdates()
Now the update works with a smooth transition. Of coarse solving this revealed another issues with indexing and main call automatic dimensions but that is another topic...
Hope it helpes other to struggle less.
I solved the animation/update.
1) I forgot to reload data after changing .expanded
property:
extension GoalsMainCell: UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
let goalNotificationInfo = ["index" : goalIndex ]
if indexPath.row == 0
if goal != nil
do
try realm.write
goal!.expanded = !goal!.expanded
catch
print("Error saving done status, (error)")
loadSubtasks()
tableView.deselectRow(at: indexPath, animated: true)
let section = IndexSet.init(integer: indexPath.section)
tableView.reloadSections(section, with: .none)
NotificationCenter.default.post(name: .goalNotKey, object: nil, userInfo: goalNotificationInfo as [AnyHashable : Any])
2) I added notification observer to the slave table delegate and call .beginUpdate()
+ .endUpdate()
@objc func updateGoalSection(_ notification: Notification)
tableView.beginUpdates()
tableView.endUpdates()
Now the update works with a smooth transition. Of coarse solving this revealed another issues with indexing and main call automatic dimensions but that is another topic...
Hope it helpes other to struggle less.
answered Nov 14 '18 at 19:04
Lukas SmilekLukas Smilek
613
613
add a comment |
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53266333%2fios-animated-expanding-tableview-tap-on-first-row-inside-tableview%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown