Sticky Menu in iOS Table/List












0















I'm trying to create an interface similar to the Home scene in the meetup app. You can see it in action below. I want to recreate the [All, Going, ...] menu behavior. I want it to start in the middle of the list and scroll up until it reaches the top of the list and stick there. Very similar to how section headers work in a UITableView.



enter image description here



Creating the menu is not the issue. My problem is creating the sticky behavior and have it work well with the rest of the list.



I've tried using a UITableView but I couldn't get the menu cell to stick. I can't put the menu in a section header because I want to use section headers for the data below the menu and UITableView's behavior is to push a section header up when the next section reaches the top of the list. I can't put the menu in the UITableView.tableHeader because the menu starts below some other data in the list.



UITableView
- UITableViewCell -> Label
- UITableViewCell -> UICollectionView of UIImageViews
- UITableViewCell -> Label
- UITableViewCell -> MyMenu (Sticky)
- UITableViewHeaderFooterView - Section 1
- UITableViewCell -> Data
- UITableViewCell -> Data
- UITableViewHeaderFooterView - Section 1
- UITableViewCell -> Data
- UITableViewCell -> Data


I've tried using a UIScrollView containing the menu and a UITableView below it but using a UITableView (which is a UIScrollView) inside a UIScrollView is painful. I couldn't get the scrolling behavior to feel natural.



UIScrollView
- UIView -> (Container)
- Label
- UICollectionView of UIImageViews
- Label
- MyMenu (Sticky)
- UITableView - Data


I'm about to try and write a UICollectionViewLayout to do what I want but I feel like I will have to recreate functionality that I get for free with UITableView.



Any idea how to approach this? Perhaps there is a reliable method to make a UITableViewCell stick and for subsequent section headers to stick under it?










share|improve this question



























    0















    I'm trying to create an interface similar to the Home scene in the meetup app. You can see it in action below. I want to recreate the [All, Going, ...] menu behavior. I want it to start in the middle of the list and scroll up until it reaches the top of the list and stick there. Very similar to how section headers work in a UITableView.



    enter image description here



    Creating the menu is not the issue. My problem is creating the sticky behavior and have it work well with the rest of the list.



    I've tried using a UITableView but I couldn't get the menu cell to stick. I can't put the menu in a section header because I want to use section headers for the data below the menu and UITableView's behavior is to push a section header up when the next section reaches the top of the list. I can't put the menu in the UITableView.tableHeader because the menu starts below some other data in the list.



    UITableView
    - UITableViewCell -> Label
    - UITableViewCell -> UICollectionView of UIImageViews
    - UITableViewCell -> Label
    - UITableViewCell -> MyMenu (Sticky)
    - UITableViewHeaderFooterView - Section 1
    - UITableViewCell -> Data
    - UITableViewCell -> Data
    - UITableViewHeaderFooterView - Section 1
    - UITableViewCell -> Data
    - UITableViewCell -> Data


    I've tried using a UIScrollView containing the menu and a UITableView below it but using a UITableView (which is a UIScrollView) inside a UIScrollView is painful. I couldn't get the scrolling behavior to feel natural.



    UIScrollView
    - UIView -> (Container)
    - Label
    - UICollectionView of UIImageViews
    - Label
    - MyMenu (Sticky)
    - UITableView - Data


    I'm about to try and write a UICollectionViewLayout to do what I want but I feel like I will have to recreate functionality that I get for free with UITableView.



    Any idea how to approach this? Perhaps there is a reliable method to make a UITableViewCell stick and for subsequent section headers to stick under it?










    share|improve this question

























      0












      0








      0








      I'm trying to create an interface similar to the Home scene in the meetup app. You can see it in action below. I want to recreate the [All, Going, ...] menu behavior. I want it to start in the middle of the list and scroll up until it reaches the top of the list and stick there. Very similar to how section headers work in a UITableView.



      enter image description here



      Creating the menu is not the issue. My problem is creating the sticky behavior and have it work well with the rest of the list.



      I've tried using a UITableView but I couldn't get the menu cell to stick. I can't put the menu in a section header because I want to use section headers for the data below the menu and UITableView's behavior is to push a section header up when the next section reaches the top of the list. I can't put the menu in the UITableView.tableHeader because the menu starts below some other data in the list.



      UITableView
      - UITableViewCell -> Label
      - UITableViewCell -> UICollectionView of UIImageViews
      - UITableViewCell -> Label
      - UITableViewCell -> MyMenu (Sticky)
      - UITableViewHeaderFooterView - Section 1
      - UITableViewCell -> Data
      - UITableViewCell -> Data
      - UITableViewHeaderFooterView - Section 1
      - UITableViewCell -> Data
      - UITableViewCell -> Data


      I've tried using a UIScrollView containing the menu and a UITableView below it but using a UITableView (which is a UIScrollView) inside a UIScrollView is painful. I couldn't get the scrolling behavior to feel natural.



      UIScrollView
      - UIView -> (Container)
      - Label
      - UICollectionView of UIImageViews
      - Label
      - MyMenu (Sticky)
      - UITableView - Data


      I'm about to try and write a UICollectionViewLayout to do what I want but I feel like I will have to recreate functionality that I get for free with UITableView.



      Any idea how to approach this? Perhaps there is a reliable method to make a UITableViewCell stick and for subsequent section headers to stick under it?










      share|improve this question














      I'm trying to create an interface similar to the Home scene in the meetup app. You can see it in action below. I want to recreate the [All, Going, ...] menu behavior. I want it to start in the middle of the list and scroll up until it reaches the top of the list and stick there. Very similar to how section headers work in a UITableView.



      enter image description here



      Creating the menu is not the issue. My problem is creating the sticky behavior and have it work well with the rest of the list.



      I've tried using a UITableView but I couldn't get the menu cell to stick. I can't put the menu in a section header because I want to use section headers for the data below the menu and UITableView's behavior is to push a section header up when the next section reaches the top of the list. I can't put the menu in the UITableView.tableHeader because the menu starts below some other data in the list.



      UITableView
      - UITableViewCell -> Label
      - UITableViewCell -> UICollectionView of UIImageViews
      - UITableViewCell -> Label
      - UITableViewCell -> MyMenu (Sticky)
      - UITableViewHeaderFooterView - Section 1
      - UITableViewCell -> Data
      - UITableViewCell -> Data
      - UITableViewHeaderFooterView - Section 1
      - UITableViewCell -> Data
      - UITableViewCell -> Data


      I've tried using a UIScrollView containing the menu and a UITableView below it but using a UITableView (which is a UIScrollView) inside a UIScrollView is painful. I couldn't get the scrolling behavior to feel natural.



      UIScrollView
      - UIView -> (Container)
      - Label
      - UICollectionView of UIImageViews
      - Label
      - MyMenu (Sticky)
      - UITableView - Data


      I'm about to try and write a UICollectionViewLayout to do what I want but I feel like I will have to recreate functionality that I get for free with UITableView.



      Any idea how to approach this? Perhaps there is a reliable method to make a UITableViewCell stick and for subsequent section headers to stick under it?







      ios swift uitableview uicollectionview






      share|improve this question













      share|improve this question











      share|improve this question




      share|improve this question










      asked Nov 21 '18 at 19:54









      yothenbergyothenberg

      5631614




      5631614
























          3 Answers
          3






          active

          oldest

          votes


















          1














          One way to implement something like this is with a view hierarchy like this:



          UIView
          - UITableView
          - UIView -> (Container)
          - Label
          - UICollectionView of UIImageViews
          - Label
          - MyMenu (Sticky)


          Your container with your menu is a sibling of the table view, but it overlaps it.



          In the scroll view delegate method scrollViewDidScroll(_:) you can reposition your menu container view so the menu is positioned above the table content. Then you need to tell the table view to reserve some space between the top and the first table cell. For this you can configure the contentInset of the table view.






          share|improve this answer































            1














            I would use a table view.



            Add an empty cell that will be where your control will be placed while it's visible, and to avoid your control covering any content.



            Add your control as a subview of your table view.



            Then override scrollViewDidScroll (UITableView is a subclass of UIScrollView so they share delegate methods).



            In scrollViewDidScroll, which gets called at least every frame while the scroll view is scrolling, update the position of the content, like this:



            let controlFrame = tableView.rectForRow(at: indexPathOfYourBlankCell)
            controlFrame.origin.y = max(0, tableView.contentOffset.y - controlFrame.y)
            control.frame = controlFrame
            tableView.bringSubviewToFront(control)


            Keep in mind that you will have to tweak the second line if your table view has a top inset, for example, if it's under a transparent navigation bar, or you're using an iPhone with a notch.



            I suggest implementing it first o an notch-less iPhone simulator, with no navigation bar, and once it works you can tweak the way the y property is calculated by adding the inset.



            I think something like this would work, but I'm not sure.



            controlFrame.origin.y = max(0, tableView.contentOffset.y + tableView.contentInsets.top - controlFrame.y)





            share|improve this answer
























            • I hadn't thought about doing it like that. I was trying to move the UITableViewCell and running into issues with the cell being resused. Your approach sidesteps that problem. I have got it working but I had to solve a few other issues so I've added an answer of my own but wanted to thank you for the inspiration!

              – yothenberg
              Nov 22 '18 at 7:16



















            0














            I implemented @EmilioPelaez's suggestion of using a separate menu view, positioning it over an empty cell and moving it as the table scrolls. To make it work I had to do the following things:




            1. Find the frame of the empty cell so I can position the menu over it

            2. As the empty view moves outside the visible area of the table view move the menu so it stays inside the visible area of the table view. It should look like it is docked to the top of the table view.

            3. When the empty cell reaches the top adjust the tableView.contentInsets.top so the section headers below look like they stick to the bottom of the menu

            4. When the table scrolls in the other direction reset the tableView.contentInsets.top

            5. Support dynamic type and rotation changes


            I ended up doing everything in viewDidLayoutSubviews because I need to handle rotation and dynamic text changes and scrollViewDidScroll isn't called on every rotation and dynamic text change. viewDidLayoutSubviews is called after almost every scrollViewDidScroll.



            let menuCellPath = IndexPath(row: 1, section: 1)
            var tableViewInsetCached = false
            var cachedTableViewInsetTop: CGFloat = 0.0

            override func viewDidLayoutSubviews() {
            // Cache the starting tableView.contentInset.top because I need to change it later
            // when the menu is docked to the top of the table
            if !tableViewInsetCached {
            cachedTableViewInsetTop = tableView.contentInset.top
            tableViewInsetCached = true
            }

            // Get the frame of the empty cell. Use rectForRow instead of cellForIndexPath so it
            // works even if the cell has been reused.
            let menuCellFrame = tableView.rectForRow(at: menuCellPath)

            // Calculate how far the menu must move to continue to be within the
            // visible area of the scroll view. If the delta is a negative number
            // the cell is within the visible area so clamp it at 0, i.e., don't move it.
            // Use `tableView.safeAreaInsets.top` to take into account the notch, translucent
            // UINavigationBar, and status bar.
            let menuFrameDeltaY = max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y)

            // Add the delta to the menu's frame
            var newMenuFrame = menuCellFrame
            newMenuFrame.origin.y = menuCellFrame.origin.y + menuFrameDeltaY
            menuView.frame = newMenuFrame

            if menuFrameDeltaY > 0 {
            print("cell outside visible area -> change contentInset")
            // Change the contentInset so subsequent section headers
            // stick to the bottom of the menu
            tableView.contentInset.top = menuCellFrame.size.height
            } else {
            print("cell inside visible area -> reset contentInset")
            // The empty cell is inside the visible area so we should
            // reset the contentInset
            tableView.contentInset.top = cachedTableViewInsetTop
            }
            }


            It's important to remember that we are dealing with a UIScrollView under the hood. The frames of its subviews don't change as the table is scrolled. Only the contentOffset changes which means that max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y) calculates the amount the menu must move to continue to be within the visible area of the table view. If the delta is less than zero the empty cell is within the visible area of the table view and I don't have to move the menu, just give it the same frame as the empty cell which is why I use max(0, x) to clamp it at zero. If the delta is greater than zero the empty cell is no longer within the visible area of the table view and the menu must be moved to continue to be within the visible area.






            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%2f53419605%2fsticky-menu-in-ios-table-list%23new-answer', 'question_page');
              }
              );

              Post as a guest















              Required, but never shown

























              3 Answers
              3






              active

              oldest

              votes








              3 Answers
              3






              active

              oldest

              votes









              active

              oldest

              votes






              active

              oldest

              votes









              1














              One way to implement something like this is with a view hierarchy like this:



              UIView
              - UITableView
              - UIView -> (Container)
              - Label
              - UICollectionView of UIImageViews
              - Label
              - MyMenu (Sticky)


              Your container with your menu is a sibling of the table view, but it overlaps it.



              In the scroll view delegate method scrollViewDidScroll(_:) you can reposition your menu container view so the menu is positioned above the table content. Then you need to tell the table view to reserve some space between the top and the first table cell. For this you can configure the contentInset of the table view.






              share|improve this answer




























                1














                One way to implement something like this is with a view hierarchy like this:



                UIView
                - UITableView
                - UIView -> (Container)
                - Label
                - UICollectionView of UIImageViews
                - Label
                - MyMenu (Sticky)


                Your container with your menu is a sibling of the table view, but it overlaps it.



                In the scroll view delegate method scrollViewDidScroll(_:) you can reposition your menu container view so the menu is positioned above the table content. Then you need to tell the table view to reserve some space between the top and the first table cell. For this you can configure the contentInset of the table view.






                share|improve this answer


























                  1












                  1








                  1







                  One way to implement something like this is with a view hierarchy like this:



                  UIView
                  - UITableView
                  - UIView -> (Container)
                  - Label
                  - UICollectionView of UIImageViews
                  - Label
                  - MyMenu (Sticky)


                  Your container with your menu is a sibling of the table view, but it overlaps it.



                  In the scroll view delegate method scrollViewDidScroll(_:) you can reposition your menu container view so the menu is positioned above the table content. Then you need to tell the table view to reserve some space between the top and the first table cell. For this you can configure the contentInset of the table view.






                  share|improve this answer













                  One way to implement something like this is with a view hierarchy like this:



                  UIView
                  - UITableView
                  - UIView -> (Container)
                  - Label
                  - UICollectionView of UIImageViews
                  - Label
                  - MyMenu (Sticky)


                  Your container with your menu is a sibling of the table view, but it overlaps it.



                  In the scroll view delegate method scrollViewDidScroll(_:) you can reposition your menu container view so the menu is positioned above the table content. Then you need to tell the table view to reserve some space between the top and the first table cell. For this you can configure the contentInset of the table view.







                  share|improve this answer












                  share|improve this answer



                  share|improve this answer










                  answered Nov 21 '18 at 20:10









                  SvenSven

                  20.4k44568




                  20.4k44568

























                      1














                      I would use a table view.



                      Add an empty cell that will be where your control will be placed while it's visible, and to avoid your control covering any content.



                      Add your control as a subview of your table view.



                      Then override scrollViewDidScroll (UITableView is a subclass of UIScrollView so they share delegate methods).



                      In scrollViewDidScroll, which gets called at least every frame while the scroll view is scrolling, update the position of the content, like this:



                      let controlFrame = tableView.rectForRow(at: indexPathOfYourBlankCell)
                      controlFrame.origin.y = max(0, tableView.contentOffset.y - controlFrame.y)
                      control.frame = controlFrame
                      tableView.bringSubviewToFront(control)


                      Keep in mind that you will have to tweak the second line if your table view has a top inset, for example, if it's under a transparent navigation bar, or you're using an iPhone with a notch.



                      I suggest implementing it first o an notch-less iPhone simulator, with no navigation bar, and once it works you can tweak the way the y property is calculated by adding the inset.



                      I think something like this would work, but I'm not sure.



                      controlFrame.origin.y = max(0, tableView.contentOffset.y + tableView.contentInsets.top - controlFrame.y)





                      share|improve this answer
























                      • I hadn't thought about doing it like that. I was trying to move the UITableViewCell and running into issues with the cell being resused. Your approach sidesteps that problem. I have got it working but I had to solve a few other issues so I've added an answer of my own but wanted to thank you for the inspiration!

                        – yothenberg
                        Nov 22 '18 at 7:16
















                      1














                      I would use a table view.



                      Add an empty cell that will be where your control will be placed while it's visible, and to avoid your control covering any content.



                      Add your control as a subview of your table view.



                      Then override scrollViewDidScroll (UITableView is a subclass of UIScrollView so they share delegate methods).



                      In scrollViewDidScroll, which gets called at least every frame while the scroll view is scrolling, update the position of the content, like this:



                      let controlFrame = tableView.rectForRow(at: indexPathOfYourBlankCell)
                      controlFrame.origin.y = max(0, tableView.contentOffset.y - controlFrame.y)
                      control.frame = controlFrame
                      tableView.bringSubviewToFront(control)


                      Keep in mind that you will have to tweak the second line if your table view has a top inset, for example, if it's under a transparent navigation bar, or you're using an iPhone with a notch.



                      I suggest implementing it first o an notch-less iPhone simulator, with no navigation bar, and once it works you can tweak the way the y property is calculated by adding the inset.



                      I think something like this would work, but I'm not sure.



                      controlFrame.origin.y = max(0, tableView.contentOffset.y + tableView.contentInsets.top - controlFrame.y)





                      share|improve this answer
























                      • I hadn't thought about doing it like that. I was trying to move the UITableViewCell and running into issues with the cell being resused. Your approach sidesteps that problem. I have got it working but I had to solve a few other issues so I've added an answer of my own but wanted to thank you for the inspiration!

                        – yothenberg
                        Nov 22 '18 at 7:16














                      1












                      1








                      1







                      I would use a table view.



                      Add an empty cell that will be where your control will be placed while it's visible, and to avoid your control covering any content.



                      Add your control as a subview of your table view.



                      Then override scrollViewDidScroll (UITableView is a subclass of UIScrollView so they share delegate methods).



                      In scrollViewDidScroll, which gets called at least every frame while the scroll view is scrolling, update the position of the content, like this:



                      let controlFrame = tableView.rectForRow(at: indexPathOfYourBlankCell)
                      controlFrame.origin.y = max(0, tableView.contentOffset.y - controlFrame.y)
                      control.frame = controlFrame
                      tableView.bringSubviewToFront(control)


                      Keep in mind that you will have to tweak the second line if your table view has a top inset, for example, if it's under a transparent navigation bar, or you're using an iPhone with a notch.



                      I suggest implementing it first o an notch-less iPhone simulator, with no navigation bar, and once it works you can tweak the way the y property is calculated by adding the inset.



                      I think something like this would work, but I'm not sure.



                      controlFrame.origin.y = max(0, tableView.contentOffset.y + tableView.contentInsets.top - controlFrame.y)





                      share|improve this answer













                      I would use a table view.



                      Add an empty cell that will be where your control will be placed while it's visible, and to avoid your control covering any content.



                      Add your control as a subview of your table view.



                      Then override scrollViewDidScroll (UITableView is a subclass of UIScrollView so they share delegate methods).



                      In scrollViewDidScroll, which gets called at least every frame while the scroll view is scrolling, update the position of the content, like this:



                      let controlFrame = tableView.rectForRow(at: indexPathOfYourBlankCell)
                      controlFrame.origin.y = max(0, tableView.contentOffset.y - controlFrame.y)
                      control.frame = controlFrame
                      tableView.bringSubviewToFront(control)


                      Keep in mind that you will have to tweak the second line if your table view has a top inset, for example, if it's under a transparent navigation bar, or you're using an iPhone with a notch.



                      I suggest implementing it first o an notch-less iPhone simulator, with no navigation bar, and once it works you can tweak the way the y property is calculated by adding the inset.



                      I think something like this would work, but I'm not sure.



                      controlFrame.origin.y = max(0, tableView.contentOffset.y + tableView.contentInsets.top - controlFrame.y)






                      share|improve this answer












                      share|improve this answer



                      share|improve this answer










                      answered Nov 21 '18 at 20:23









                      EmilioPelaezEmilioPelaez

                      9,09832436




                      9,09832436













                      • I hadn't thought about doing it like that. I was trying to move the UITableViewCell and running into issues with the cell being resused. Your approach sidesteps that problem. I have got it working but I had to solve a few other issues so I've added an answer of my own but wanted to thank you for the inspiration!

                        – yothenberg
                        Nov 22 '18 at 7:16



















                      • I hadn't thought about doing it like that. I was trying to move the UITableViewCell and running into issues with the cell being resused. Your approach sidesteps that problem. I have got it working but I had to solve a few other issues so I've added an answer of my own but wanted to thank you for the inspiration!

                        – yothenberg
                        Nov 22 '18 at 7:16

















                      I hadn't thought about doing it like that. I was trying to move the UITableViewCell and running into issues with the cell being resused. Your approach sidesteps that problem. I have got it working but I had to solve a few other issues so I've added an answer of my own but wanted to thank you for the inspiration!

                      – yothenberg
                      Nov 22 '18 at 7:16





                      I hadn't thought about doing it like that. I was trying to move the UITableViewCell and running into issues with the cell being resused. Your approach sidesteps that problem. I have got it working but I had to solve a few other issues so I've added an answer of my own but wanted to thank you for the inspiration!

                      – yothenberg
                      Nov 22 '18 at 7:16











                      0














                      I implemented @EmilioPelaez's suggestion of using a separate menu view, positioning it over an empty cell and moving it as the table scrolls. To make it work I had to do the following things:




                      1. Find the frame of the empty cell so I can position the menu over it

                      2. As the empty view moves outside the visible area of the table view move the menu so it stays inside the visible area of the table view. It should look like it is docked to the top of the table view.

                      3. When the empty cell reaches the top adjust the tableView.contentInsets.top so the section headers below look like they stick to the bottom of the menu

                      4. When the table scrolls in the other direction reset the tableView.contentInsets.top

                      5. Support dynamic type and rotation changes


                      I ended up doing everything in viewDidLayoutSubviews because I need to handle rotation and dynamic text changes and scrollViewDidScroll isn't called on every rotation and dynamic text change. viewDidLayoutSubviews is called after almost every scrollViewDidScroll.



                      let menuCellPath = IndexPath(row: 1, section: 1)
                      var tableViewInsetCached = false
                      var cachedTableViewInsetTop: CGFloat = 0.0

                      override func viewDidLayoutSubviews() {
                      // Cache the starting tableView.contentInset.top because I need to change it later
                      // when the menu is docked to the top of the table
                      if !tableViewInsetCached {
                      cachedTableViewInsetTop = tableView.contentInset.top
                      tableViewInsetCached = true
                      }

                      // Get the frame of the empty cell. Use rectForRow instead of cellForIndexPath so it
                      // works even if the cell has been reused.
                      let menuCellFrame = tableView.rectForRow(at: menuCellPath)

                      // Calculate how far the menu must move to continue to be within the
                      // visible area of the scroll view. If the delta is a negative number
                      // the cell is within the visible area so clamp it at 0, i.e., don't move it.
                      // Use `tableView.safeAreaInsets.top` to take into account the notch, translucent
                      // UINavigationBar, and status bar.
                      let menuFrameDeltaY = max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y)

                      // Add the delta to the menu's frame
                      var newMenuFrame = menuCellFrame
                      newMenuFrame.origin.y = menuCellFrame.origin.y + menuFrameDeltaY
                      menuView.frame = newMenuFrame

                      if menuFrameDeltaY > 0 {
                      print("cell outside visible area -> change contentInset")
                      // Change the contentInset so subsequent section headers
                      // stick to the bottom of the menu
                      tableView.contentInset.top = menuCellFrame.size.height
                      } else {
                      print("cell inside visible area -> reset contentInset")
                      // The empty cell is inside the visible area so we should
                      // reset the contentInset
                      tableView.contentInset.top = cachedTableViewInsetTop
                      }
                      }


                      It's important to remember that we are dealing with a UIScrollView under the hood. The frames of its subviews don't change as the table is scrolled. Only the contentOffset changes which means that max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y) calculates the amount the menu must move to continue to be within the visible area of the table view. If the delta is less than zero the empty cell is within the visible area of the table view and I don't have to move the menu, just give it the same frame as the empty cell which is why I use max(0, x) to clamp it at zero. If the delta is greater than zero the empty cell is no longer within the visible area of the table view and the menu must be moved to continue to be within the visible area.






                      share|improve this answer




























                        0














                        I implemented @EmilioPelaez's suggestion of using a separate menu view, positioning it over an empty cell and moving it as the table scrolls. To make it work I had to do the following things:




                        1. Find the frame of the empty cell so I can position the menu over it

                        2. As the empty view moves outside the visible area of the table view move the menu so it stays inside the visible area of the table view. It should look like it is docked to the top of the table view.

                        3. When the empty cell reaches the top adjust the tableView.contentInsets.top so the section headers below look like they stick to the bottom of the menu

                        4. When the table scrolls in the other direction reset the tableView.contentInsets.top

                        5. Support dynamic type and rotation changes


                        I ended up doing everything in viewDidLayoutSubviews because I need to handle rotation and dynamic text changes and scrollViewDidScroll isn't called on every rotation and dynamic text change. viewDidLayoutSubviews is called after almost every scrollViewDidScroll.



                        let menuCellPath = IndexPath(row: 1, section: 1)
                        var tableViewInsetCached = false
                        var cachedTableViewInsetTop: CGFloat = 0.0

                        override func viewDidLayoutSubviews() {
                        // Cache the starting tableView.contentInset.top because I need to change it later
                        // when the menu is docked to the top of the table
                        if !tableViewInsetCached {
                        cachedTableViewInsetTop = tableView.contentInset.top
                        tableViewInsetCached = true
                        }

                        // Get the frame of the empty cell. Use rectForRow instead of cellForIndexPath so it
                        // works even if the cell has been reused.
                        let menuCellFrame = tableView.rectForRow(at: menuCellPath)

                        // Calculate how far the menu must move to continue to be within the
                        // visible area of the scroll view. If the delta is a negative number
                        // the cell is within the visible area so clamp it at 0, i.e., don't move it.
                        // Use `tableView.safeAreaInsets.top` to take into account the notch, translucent
                        // UINavigationBar, and status bar.
                        let menuFrameDeltaY = max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y)

                        // Add the delta to the menu's frame
                        var newMenuFrame = menuCellFrame
                        newMenuFrame.origin.y = menuCellFrame.origin.y + menuFrameDeltaY
                        menuView.frame = newMenuFrame

                        if menuFrameDeltaY > 0 {
                        print("cell outside visible area -> change contentInset")
                        // Change the contentInset so subsequent section headers
                        // stick to the bottom of the menu
                        tableView.contentInset.top = menuCellFrame.size.height
                        } else {
                        print("cell inside visible area -> reset contentInset")
                        // The empty cell is inside the visible area so we should
                        // reset the contentInset
                        tableView.contentInset.top = cachedTableViewInsetTop
                        }
                        }


                        It's important to remember that we are dealing with a UIScrollView under the hood. The frames of its subviews don't change as the table is scrolled. Only the contentOffset changes which means that max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y) calculates the amount the menu must move to continue to be within the visible area of the table view. If the delta is less than zero the empty cell is within the visible area of the table view and I don't have to move the menu, just give it the same frame as the empty cell which is why I use max(0, x) to clamp it at zero. If the delta is greater than zero the empty cell is no longer within the visible area of the table view and the menu must be moved to continue to be within the visible area.






                        share|improve this answer


























                          0












                          0








                          0







                          I implemented @EmilioPelaez's suggestion of using a separate menu view, positioning it over an empty cell and moving it as the table scrolls. To make it work I had to do the following things:




                          1. Find the frame of the empty cell so I can position the menu over it

                          2. As the empty view moves outside the visible area of the table view move the menu so it stays inside the visible area of the table view. It should look like it is docked to the top of the table view.

                          3. When the empty cell reaches the top adjust the tableView.contentInsets.top so the section headers below look like they stick to the bottom of the menu

                          4. When the table scrolls in the other direction reset the tableView.contentInsets.top

                          5. Support dynamic type and rotation changes


                          I ended up doing everything in viewDidLayoutSubviews because I need to handle rotation and dynamic text changes and scrollViewDidScroll isn't called on every rotation and dynamic text change. viewDidLayoutSubviews is called after almost every scrollViewDidScroll.



                          let menuCellPath = IndexPath(row: 1, section: 1)
                          var tableViewInsetCached = false
                          var cachedTableViewInsetTop: CGFloat = 0.0

                          override func viewDidLayoutSubviews() {
                          // Cache the starting tableView.contentInset.top because I need to change it later
                          // when the menu is docked to the top of the table
                          if !tableViewInsetCached {
                          cachedTableViewInsetTop = tableView.contentInset.top
                          tableViewInsetCached = true
                          }

                          // Get the frame of the empty cell. Use rectForRow instead of cellForIndexPath so it
                          // works even if the cell has been reused.
                          let menuCellFrame = tableView.rectForRow(at: menuCellPath)

                          // Calculate how far the menu must move to continue to be within the
                          // visible area of the scroll view. If the delta is a negative number
                          // the cell is within the visible area so clamp it at 0, i.e., don't move it.
                          // Use `tableView.safeAreaInsets.top` to take into account the notch, translucent
                          // UINavigationBar, and status bar.
                          let menuFrameDeltaY = max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y)

                          // Add the delta to the menu's frame
                          var newMenuFrame = menuCellFrame
                          newMenuFrame.origin.y = menuCellFrame.origin.y + menuFrameDeltaY
                          menuView.frame = newMenuFrame

                          if menuFrameDeltaY > 0 {
                          print("cell outside visible area -> change contentInset")
                          // Change the contentInset so subsequent section headers
                          // stick to the bottom of the menu
                          tableView.contentInset.top = menuCellFrame.size.height
                          } else {
                          print("cell inside visible area -> reset contentInset")
                          // The empty cell is inside the visible area so we should
                          // reset the contentInset
                          tableView.contentInset.top = cachedTableViewInsetTop
                          }
                          }


                          It's important to remember that we are dealing with a UIScrollView under the hood. The frames of its subviews don't change as the table is scrolled. Only the contentOffset changes which means that max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y) calculates the amount the menu must move to continue to be within the visible area of the table view. If the delta is less than zero the empty cell is within the visible area of the table view and I don't have to move the menu, just give it the same frame as the empty cell which is why I use max(0, x) to clamp it at zero. If the delta is greater than zero the empty cell is no longer within the visible area of the table view and the menu must be moved to continue to be within the visible area.






                          share|improve this answer













                          I implemented @EmilioPelaez's suggestion of using a separate menu view, positioning it over an empty cell and moving it as the table scrolls. To make it work I had to do the following things:




                          1. Find the frame of the empty cell so I can position the menu over it

                          2. As the empty view moves outside the visible area of the table view move the menu so it stays inside the visible area of the table view. It should look like it is docked to the top of the table view.

                          3. When the empty cell reaches the top adjust the tableView.contentInsets.top so the section headers below look like they stick to the bottom of the menu

                          4. When the table scrolls in the other direction reset the tableView.contentInsets.top

                          5. Support dynamic type and rotation changes


                          I ended up doing everything in viewDidLayoutSubviews because I need to handle rotation and dynamic text changes and scrollViewDidScroll isn't called on every rotation and dynamic text change. viewDidLayoutSubviews is called after almost every scrollViewDidScroll.



                          let menuCellPath = IndexPath(row: 1, section: 1)
                          var tableViewInsetCached = false
                          var cachedTableViewInsetTop: CGFloat = 0.0

                          override func viewDidLayoutSubviews() {
                          // Cache the starting tableView.contentInset.top because I need to change it later
                          // when the menu is docked to the top of the table
                          if !tableViewInsetCached {
                          cachedTableViewInsetTop = tableView.contentInset.top
                          tableViewInsetCached = true
                          }

                          // Get the frame of the empty cell. Use rectForRow instead of cellForIndexPath so it
                          // works even if the cell has been reused.
                          let menuCellFrame = tableView.rectForRow(at: menuCellPath)

                          // Calculate how far the menu must move to continue to be within the
                          // visible area of the scroll view. If the delta is a negative number
                          // the cell is within the visible area so clamp it at 0, i.e., don't move it.
                          // Use `tableView.safeAreaInsets.top` to take into account the notch, translucent
                          // UINavigationBar, and status bar.
                          let menuFrameDeltaY = max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y)

                          // Add the delta to the menu's frame
                          var newMenuFrame = menuCellFrame
                          newMenuFrame.origin.y = menuCellFrame.origin.y + menuFrameDeltaY
                          menuView.frame = newMenuFrame

                          if menuFrameDeltaY > 0 {
                          print("cell outside visible area -> change contentInset")
                          // Change the contentInset so subsequent section headers
                          // stick to the bottom of the menu
                          tableView.contentInset.top = menuCellFrame.size.height
                          } else {
                          print("cell inside visible area -> reset contentInset")
                          // The empty cell is inside the visible area so we should
                          // reset the contentInset
                          tableView.contentInset.top = cachedTableViewInsetTop
                          }
                          }


                          It's important to remember that we are dealing with a UIScrollView under the hood. The frames of its subviews don't change as the table is scrolled. Only the contentOffset changes which means that max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y) calculates the amount the menu must move to continue to be within the visible area of the table view. If the delta is less than zero the empty cell is within the visible area of the table view and I don't have to move the menu, just give it the same frame as the empty cell which is why I use max(0, x) to clamp it at zero. If the delta is greater than zero the empty cell is no longer within the visible area of the table view and the menu must be moved to continue to be within the visible area.







                          share|improve this answer












                          share|improve this answer



                          share|improve this answer










                          answered Nov 22 '18 at 7:15









                          yothenbergyothenberg

                          5631614




                          5631614






























                              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%2f53419605%2fsticky-menu-in-ios-table-list%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

                              "Incorrect syntax near the keyword 'ON'. (on update cascade, on delete cascade,)

                              Alcedinidae

                              Origin of the phrase “under your belt”?