ScrollViewReader doesn't scroll on small changes until after user interaction

I'm running into an odd issue where on iOS 16, the ScrollViewReaderProxy does not always scroll. It seems as though small content changes won't cause the view to scroll unless the user interacts with the view, or if there was a large content change first. I'm wondering if this is just a bug that exists in iOS 16 (as it works fine in iOS 17) or if there's something I've overlooked.

I want to create a view that can be pinned to the trailing content as more text is added but I also want to have a fading mask on the left and right of the view (hence the off -15 and 15 padding).

TLDR; I have a struct PinnableScrollView<EquatableView: View & Equatable>: View which takes a @ViewBuilder public let content: EquatableView so that when the content changes it calls scrollViewReaderProxy.scrollTo.

Here's the full class that I have:

struct PinnableScrollView<EquatableView: View & Equatable, GenericView: View>: View {
    public enum Pinning {
        case leading
        case trailing

        var alignment: Alignment {
            switch self {
            case .leading: return .leading
            case .trailing: return .trailing
            }
        }
    }
    public let pinning: Pinning
    public let debugViews: Bool

    @ViewBuilder public let content: EquatableView
    @ViewBuilder public let transformer: (EquatableView) -> GenericView

    @State private var contentWidth: CGFloat = .zero
    @State private var scrollWidth: CGFloat = .zero
    @State private var offsetValue: CGFloat = .zero
    @Namespace var element

    var canScroll: Bool {
        scrollWidth > contentWidth
    }

    public var body: some View {
        if debugViews {
            VStack(alignment: .leading, spacing: 10) {
                Text("contentWidth: \(contentWidth)").font(.caption2)
                Text("scrollWidth: \(scrollWidth)").font(.caption2)
                Text("offsetValue: \(offsetValue)").font(.caption2)
                scrollContent
            }
        } else {
            scrollContent
        }
    }

    var scrollContent: some View {
        ScrollViewReader { scrollViewReaderProxy in
            ScrollView(.horizontal) {
                HStack(spacing: 0) {
                    if pinning == .trailing {
                        Spacer(minLength: 0)
                    }

                    transformer(content)
                        .padding(.horizontal, canScroll ? 15 : 0)
                        .id(element)

                    if pinning == .leading {
                        Spacer(minLength: 0)
                    }
                }
                .frame(minWidth: contentWidth)
                .background(GeometryReader { proxy in
                    Color.clear
                        .preference(
                            key: ScrollSizePreferenceKey.self,
                            value: proxy.size.width
                        ).onPreferenceChange(ScrollSizePreferenceKey.self, perform: { value in
                            self.scrollWidth = value
                        }).preference(
                            key: OffsetPreferenceKey.self,
                            value: proxy.frame(in: .named("scrollViewCoordinateSpaceLayer")).minX
                        ).onPreferenceChange(OffsetPreferenceKey.self, perform: { value in
                            self.offsetValue = value
                        })
                })
            }
            .padding(.horizontal, canScroll ? -15 : 0)
            .background(GeometryReader { proxy in
                Color.clear
                    .preference(
                        key: WidthPreferenceKey.self,
                        value: proxy.size.width
                    ).onPreferenceChange(WidthPreferenceKey.self, perform: { value in
                        self.contentWidth = value
                    })
            })
            .mask(
                HStack(spacing: 0) {
                    LinearGradient(gradient: Gradient(colors: [.black.opacity(0.0), .black]), startPoint: .leading, endPoint: .trailing)
                        .frame(width: 15.0)
                    Color.black
                        .frame(width: contentWidth)
                    LinearGradient(gradient: Gradient(colors: [.black, .black.opacity(0.0)]), startPoint: .leading, endPoint: .trailing)
                        .frame(width: 15.0)
                }
            )
            .coordinateSpace(name: "scrollViewCoordinateSpaceLayer")
            .onAppear(perform: {
                if pinning == .trailing {
                    scrollViewReaderProxy.scrollTo(element, anchor: .trailing)
                }
            })
            .onChange(of: content, perform: { _ in
                if pinning == .trailing {
                    print("attempting to scroll: \(Date.now)")
                    withAnimation {
                        scrollViewReaderProxy.scrollTo(element, anchor: .trailing)
                    }
                }
            })
        }
    }
}

private struct ScrollSizePreferenceKey: PreferenceKey {
  static var defaultValue: CGFloat = .zero
  static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() }
}

private struct OffsetPreferenceKey: PreferenceKey {
  static var defaultValue: CGFloat = .zero
  static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() }
}

private struct WidthPreferenceKey: PreferenceKey {
  static var defaultValue: CGFloat = .zero
  static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() }
}

And here's a basic preview provider:

struct PinnableScrollView_Previews: PreviewProvider {
    struct TestView: View {
        @State var string: String = "Hello"

        var body: some View {
            VStack(alignment: .leading, spacing: 10) {
                Button("Add Longer Text") {
                    string = string + " world, how are you? Are doing well??"
                }

                Button("Add Short Text") {
                    string = string + " hello!"
                }

                PinnableScrollView(pinning: .trailing, debugViews: true) {
                    Text(string)
                } transformer: { view in
                    view.padding()
                }
            }
        }
    }

    static var previews: some View {
        TestView()
            .padding()
    }
}
ScrollViewReader doesn't scroll on small changes until after user interaction
 
 
Q