ios - Animated expanding TableView (tap on first row) inside TableView










1















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 custom TableViewCell


  • Custom TableViewCell has a UIView and “slave” UITableView inside - underlying UIView is constrained to “slave” UITableView and makes it’s “material” design with shadow (cropToBounds prevents shadow on UITableView)


  • “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 custom TableViewCell (“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!










share|improve this question


























    1















    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 custom TableViewCell


    • Custom TableViewCell has a UIView and “slave” UITableView inside - underlying UIView is constrained to “slave” UITableView and makes it’s “material” design with shadow (cropToBounds prevents shadow on UITableView)


    • “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 custom TableViewCell (“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!










    share|improve this question
























      1












      1








      1








      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 custom TableViewCell


      • Custom TableViewCell has a UIView and “slave” UITableView inside - underlying UIView is constrained to “slave” UITableView and makes it’s “material” design with shadow (cropToBounds prevents shadow on UITableView)


      • “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 custom TableViewCell (“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!










      share|improve this question














      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 custom TableViewCell


      • Custom TableViewCell has a UIView and “slave” UITableView inside - underlying UIView is constrained to “slave” UITableView and makes it’s “material” design with shadow (cropToBounds prevents shadow on UITableView)


      • “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 custom TableViewCell (“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






      share|improve this question













      share|improve this question











      share|improve this question




      share|improve this question










      asked Nov 12 '18 at 16:28









      Lukas SmilekLukas Smilek

      613




      613






















          1 Answer
          1






          active

          oldest

          votes


















          0














          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.






          share|improve this answer






















            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
            );



            );













            draft saved

            draft discarded


















            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









            0














            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.






            share|improve this answer



























              0














              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.






              share|improve this answer

























                0












                0








                0







                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.






                share|improve this answer













                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.







                share|improve this answer












                share|improve this answer



                share|improve this answer










                answered Nov 14 '18 at 19:04









                Lukas SmilekLukas Smilek

                613




                613



























                    draft saved

                    draft discarded
















































                    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.




                    draft saved


                    draft discarded














                    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





















































                    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







                    Popular posts from this blog

                    Use pre created SQLite database for Android project in kotlin

                    Darth Vader #20

                    Ondo