Sticky Menu in iOS Table/List
up vote
0
down vote
favorite
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
.
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
add a comment |
up vote
0
down vote
favorite
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
.
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
add a comment |
up vote
0
down vote
favorite
up vote
0
down vote
favorite
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
.
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
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
.
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
ios swift uitableview uicollectionview
asked Nov 21 at 19:54
yothenberg
5631614
5631614
add a comment |
add a comment |
3 Answers
3
active
oldest
votes
up vote
1
down vote
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.
add a comment |
up vote
1
down vote
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)
I hadn't thought about doing it like that. I was trying to move theUITableViewCell
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 at 7:16
add a comment |
up vote
0
down vote
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:
- Find the frame of the empty cell so I can position the menu over it
- 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.
- 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 - When the table scrolls in the other direction reset the tableView.contentInsets.top
- 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.
add a comment |
3 Answers
3
active
oldest
votes
3 Answers
3
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
1
down vote
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.
add a comment |
up vote
1
down vote
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.
add a comment |
up vote
1
down vote
up vote
1
down vote
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.
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.
answered Nov 21 at 20:10
Sven
20.1k44567
20.1k44567
add a comment |
add a comment |
up vote
1
down vote
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)
I hadn't thought about doing it like that. I was trying to move theUITableViewCell
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 at 7:16
add a comment |
up vote
1
down vote
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)
I hadn't thought about doing it like that. I was trying to move theUITableViewCell
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 at 7:16
add a comment |
up vote
1
down vote
up vote
1
down vote
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)
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)
answered Nov 21 at 20:23
EmilioPelaez
8,79632336
8,79632336
I hadn't thought about doing it like that. I was trying to move theUITableViewCell
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 at 7:16
add a comment |
I hadn't thought about doing it like that. I was trying to move theUITableViewCell
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 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 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 at 7:16
add a comment |
up vote
0
down vote
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:
- Find the frame of the empty cell so I can position the menu over it
- 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.
- 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 - When the table scrolls in the other direction reset the tableView.contentInsets.top
- 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.
add a comment |
up vote
0
down vote
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:
- Find the frame of the empty cell so I can position the menu over it
- 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.
- 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 - When the table scrolls in the other direction reset the tableView.contentInsets.top
- 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.
add a comment |
up vote
0
down vote
up vote
0
down vote
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:
- Find the frame of the empty cell so I can position the menu over it
- 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.
- 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 - When the table scrolls in the other direction reset the tableView.contentInsets.top
- 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.
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:
- Find the frame of the empty cell so I can position the menu over it
- 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.
- 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 - When the table scrolls in the other direction reset the tableView.contentInsets.top
- 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.
answered Nov 22 at 7:15
yothenberg
5631614
5631614
add a comment |
add a comment |
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53419605%2fsticky-menu-in-ios-table-list%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown