Even though SwiftUI offers a lot of features for building a UI, I found my self missing something. I'm trying to display a list with calendar information, one item pr. day. I want to scroll this fluently and by the nature of a calendar, there is not a specific start or end to the list, so a standard ScrollView or List does not suit the task.
I've therefore tried to put a custom control together. My approach is to have 2 items more that needed, initially positioned off screen (at the top and bottom). All items have the same height to keep the layout simple. I scroll by offsetting the items. Once the offset is greater than the height of an item, I shift the contents by 1 and reposition the items so I again have the first and last item placed off screen.
This method works quite well, as long a the item views are simple. When the individual items get complex, I see a stuttering effect when I drag it up and down. I suspect that the delay causing the stutter comes from the view being regenerated.
Is this approach good, and if so, what can I do to optimize it to reduce/remove the stuttering effect? If not, are there any alternate ideas for a solution?
I included a rough version of the source code here to give an idea of the design:
struct InfinityScroller<Item:View> : View
{
@State private var offset : CGFloat = 0
@State private var itemIndex : Int = 0
@State private var itemOffset : CGFloat
private let itemHeight : CGFloat
private let zero : CGFloat
private let item : (Int,CGFloat,CGFloat) -> Item
private let itemCount : Int = 7
private let offScreenCount : Int = 1
init(location:ObservedObject<InfinityLocation>,zero:CGFloat,_ height:CGFloat=121,@ViewBuilder item : @escaping (Int,CGFloat,CGFloat) -> Item)
{
self._location = location
self.zero = zero
self.itemHeight = height
self.item = item
self._itemOffset = State(initialValue: zero)
}
var body : some View
{
GeometryReader
{
g in
ZStack(alignment:.top)
{
Color.clear.frame(width:0)
Color.clear.frame(height:0)
self.makeItems(g.size.width)
}
.contentShape(Rectangle())
.gesture(DragGesture(minimumDistance: 0)
.onChanged
{ g in
self.updatePosition(g.translation.height)
}
.onEnded
{ g in
self.offset += g.translation.height
self.updatePosition()
}
)
}
}
func updatePosition(_ scroll:CGFloat=0)
{
let off = offset + scroll + zero
itemIndex = -Int((off/itemHeight).rounded(.down))
itemOffset = off.mod(itemHeight)
}
func makeItems(_ w:CGFloat) -> some View
{
return ForEach (-offScreenCount...itemCount+offScreenCount,id:\.self)
{ i in
self.item(i+self.itemIndex,w,self.itemHeight)
.position(x:0,y:CGFloat(i) * self.itemHeight + self.itemOffset)
.offset(x:w/2,y:self.itemHeight/2)
}
}
}