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:
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() {
goals = realm.objects(Goal.self)
if goals?.count == 0 || goals?.count == nil { noGoalsLabel.isHidden = false }
tableView.reloadData()
}
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!