ios: loading progress moved to chat list (#5639)

* ios: loading progress moved to chat list

* place

* changes

* large spinner, smaller timeout

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko 2025-02-19 23:13:20 +07:00 committed by GitHub
parent ec519afb3f
commit dc980ae88f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 196 additions and 122 deletions

View file

@ -61,7 +61,10 @@ class ItemsModel: ObservableObject {
// Publishes directly to `objectWillChange` publisher,
// this will cause reversedChatItems to be rendered without throttling
@Published var isLoading = false
@Published var showLoadingProgress = false
@Published var showLoadingProgress: ChatId? = nil
private var navigationTimeoutTask: Task<Void, Never>? = nil
private var loadChatTask: Task<Void, Never>? = nil
init() {
publisher
@ -71,32 +74,26 @@ class ItemsModel: ObservableObject {
}
func loadOpenChat(_ chatId: ChatId, willNavigate: @escaping () -> Void = {}) {
let navigationTimeout = Task {
navigationTimeoutTask?.cancel()
loadChatTask?.cancel()
navigationTimeoutTask = Task {
do {
try await Task.sleep(nanoseconds: 250_000000)
try await Task.sleep(nanoseconds: 100_000000)
await MainActor.run {
willNavigate()
ChatModel.shared.chatId = chatId
willNavigate()
}
} catch {}
}
let progressTimeout = Task {
do {
try await Task.sleep(nanoseconds: 1500_000000)
await MainActor.run { showLoadingProgress = true }
} catch {}
}
Task {
loadChatTask = Task {
await MainActor.run { self.isLoading = true }
// try? await Task.sleep(nanoseconds: 5000_000000)
// try? await Task.sleep(nanoseconds: 1000_000000)
await loadChat(chatId: chatId)
navigationTimeout.cancel()
progressTimeout.cancel()
await MainActor.run {
self.isLoading = false
self.showLoadingProgress = false
willNavigate()
// ChatModel.shared.chatId = id
if !Task.isCancelled {
await MainActor.run {
self.isLoading = false
self.showLoadingProgress = nil
}
}
}
}

View file

@ -335,7 +335,7 @@ func apiGetChat(chatId: ChatId, pagination: ChatPagination, search: String = "")
}
func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async {
await loadChat(chatId: chat.chatInfo.id)
await loadChat(chatId: chat.chatInfo.id, search: search, clearItems: clearItems)
}
func loadChat(chatId: ChatId, search: String = "", clearItems: Bool = true) async {

View file

@ -32,7 +32,7 @@ func apiLoadMessages(
// For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes
let paginationIsInitial = switch pagination { case .initial: true; default: false }
let paginationIsLast = switch pagination { case .last: true; default: false }
if ((chatModel.chatId != chat.id || chat.chatItems.isEmpty) && !paginationIsInitial && !paginationIsLast) {
if ((chatModel.chatId != chat.id || chat.chatItems.isEmpty) && !paginationIsInitial && !paginationIsLast) || Task.isCancelled {
return
}
@ -50,7 +50,6 @@ func apiLoadMessages(
chatModel.chatItemStatuses.removeAll()
ItemsModel.shared.reversedChatItems = chat.chatItems.reversed()
chatModel.updateChatInfo(chat.chatInfo)
chatModel.chatId = chat.chatInfo.id
chatState.splits = newSplits
if !chat.chatItems.isEmpty {
chatState.unreadAfterItemId = chat.chatItems.last!.id

View file

@ -16,7 +16,7 @@ private let memberImageSize: CGFloat = 34
struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var im = ItemsModel.shared
@State var mergedItems: BoxedValue<MergedItems> = BoxedValue(MergedItems(items: [], splits: [], indexInParentItems: [:]))
@State var mergedItems: BoxedValue<MergedItems> = BoxedValue(MergedItems.create(ItemsModel.shared.reversedChatItems, [], ItemsModel.shared.chatState))
@State var revealedItems: Set<Int64> = Set()
@State var theme: AppTheme = buildTheme()
@Environment(\.dismiss) var dismiss
@ -55,7 +55,7 @@ struct ChatView: View {
private let useItemsUpdateTask = false
@State private var scrollView: EndlessScrollView<MergedItem> = EndlessScrollView(frame: .zero)
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
var body: some View {
@ -120,6 +120,9 @@ struct ChatView: View {
)
}
}
if im.showLoadingProgress == chat.id {
ProgressView().scaleEffect(2)
}
}
.safeAreaInset(edge: .top) {
VStack(spacing: .zero) {
@ -201,6 +204,16 @@ struct ChatView: View {
selectedChatItems = nil
revealedItems = Set()
initChatView()
if im.isLoading {
Task {
try? await Task.sleep(nanoseconds: 500_000000)
await MainActor.run {
if im.isLoading {
im.showLoadingProgress = chat.id
}
}
}
}
}
.onChange(of: chatModel.chatId) { cId in
showChatInfoSheet = false
@ -214,10 +227,10 @@ struct ChatView: View {
scrollView.listState.onUpdateListener = onChatItemsUpdated
initChatView()
theme = buildTheme()
Task {
if let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() }) {
await scrollView.scrollToItem(unreadIndex, animated: false)
}
if let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() }) {
scrollView.scrollToItem(unreadIndex)
} else {
scrollView.scrollToBottom()
}
} else {
dismiss()
@ -290,7 +303,6 @@ struct ChatView: View {
}
}
ToolbarItem(placement: .navigationBarTrailing) {
let isLoading = im.isLoading && im.showLoadingProgress
if selectedChatItems != nil {
Button {
withAnimation {
@ -313,23 +325,19 @@ struct ChatView: View {
}
}
Menu {
if !isLoading {
if callsPrefEnabled && chatModel.activeCall == nil {
Button {
CallController.shared.startCall(contact, .video)
} label: {
Label("Video call", systemImage: "video")
}
.disabled(!contact.ready || !contact.active)
if callsPrefEnabled && chatModel.activeCall == nil {
Button {
CallController.shared.startCall(contact, .video)
} label: {
Label("Video call", systemImage: "video")
}
searchButton()
ToggleNtfsButton(chat: chat)
.disabled(!contact.ready || !contact.active)
.disabled(!contact.ready || !contact.active)
}
searchButton()
ToggleNtfsButton(chat: chat)
.disabled(!contact.ready || !contact.active)
} label: {
Image(systemName: "ellipsis")
.tint(isLoading ? Color.clear : nil)
.overlay { if isLoading { ProgressView() } }
}
}
case let .group(groupInfo):
@ -351,14 +359,10 @@ struct ChatView: View {
}
}
Menu {
if !isLoading {
searchButton()
ToggleNtfsButton(chat: chat)
}
searchButton()
ToggleNtfsButton(chat: chat)
} label: {
Image(systemName: "ellipsis")
.tint(isLoading ? Color.clear : nil)
.overlay { if isLoading { ProgressView() } }
}
}
case .local:
@ -370,7 +374,7 @@ struct ChatView: View {
}
}
}
private func initChatView() {
let cInfo = chat.chatInfo
// This check prevents the call to apiContactInfo after the app is suspended, and the database is closed.
@ -420,14 +424,14 @@ struct ChatView: View {
index = mergedItems.boxedValue.indexInParentItems[itemId]
}
if let index {
await scrollView.scrollToItem(min(ItemsModel.shared.reversedChatItems.count - 1, index), animated: true)
await scrollView.scrollToItemAnimated(min(ItemsModel.shared.reversedChatItems.count - 1, index))
}
} catch {
logger.error("Error scrolling to item: \(error)")
}
}
}
private func searchToolbar() -> some View {
HStack(spacing: 12) {
HStack(spacing: 4) {
@ -458,7 +462,7 @@ struct ChatView: View {
.padding(.horizontal)
.padding(.vertical, 8)
}
private func voiceWithoutFrame(_ ci: ChatItem) -> Bool {
ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil && ci.meta.itemForwarded == nil
}
@ -514,25 +518,13 @@ struct ChatView: View {
.id(ci.id) // Required to trigger `onAppear` on iOS15
}
.onAppear {
Task {
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() })
let unreadItemId: Int64? = if let unreadIndex { mergedItems.boxedValue.items[unreadIndex].newest().item.id } else { nil }
await MainActor.run {
// this helps to speed up initial process of setting scroll position and reduce time needed
// to layout items on screen
if let unreadIndex, let unreadItemId {
scrollView.setScrollPosition(unreadIndex, unreadItemId)
}
scrollView.updateItems(mergedItems.boxedValue.items)
}
if let unreadIndex {
await scrollView.scrollToItem(unreadIndex, animated: false)
}
if !im.isLoading {
updateWithInitiallyLoadedItems()
}
loadLastItems($loadingMoreItems, chat)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
allowLoadMoreItems = true
}
.onChange(of: im.isLoading) { loading in
if !loading {
updateWithInitiallyLoadedItems()
}
}
.onChange(of: im.reversedChatItems) { items in
@ -565,18 +557,24 @@ struct ChatView: View {
allowLoadMoreItems = true
}
}
.opacity(ItemsModel.shared.isLoading ? 0 : 1)
.padding(.vertical, -100)
.onTapGesture { hideKeyboard() }
.onChange(of: searchText) { s in
Task {
await loadChat(chat: chat, search: s)
if s.isEmpty {
await scrollView.scrollToItem(0, animated: false, top: false)
loadLastItems($loadingMoreItems, chat)
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
await MainActor.run {
scrollView.updateItems(mergedItems.boxedValue.items)
}
if !s.isEmpty {
scrollView.scrollToBottom()
} else if let index = scrollView.listState.items.lastIndex(where: { $0.hasUnread() }) {
// scroll to the top unread item
await scrollView.scrollToItem(index, animated: false)
scrollView.scrollToItem(index)
loadLastItems($loadingMoreItems, chat)
} else {
scrollView.scrollToBottom()
loadLastItems($loadingMoreItems, chat)
}
}
}
@ -584,7 +582,7 @@ struct ChatView: View {
if added {
im.itemAdded = false
if scrollView.listState.firstVisibleItemIndex < 2 {
scrollView.scrollToBottom()
scrollView.scrollToBottomAnimated()
} else {
scrollView.scroll(by: 34)
}
@ -607,6 +605,27 @@ struct ChatView: View {
}
}
private func updateWithInitiallyLoadedItems() {
if mergedItems.boxedValue.items.isEmpty {
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, ItemsModel.shared.chatState)
}
let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() })
let unreadItemId: Int64? = if let unreadIndex { mergedItems.boxedValue.items[unreadIndex].newest().item.id } else { nil }
// this helps to speed up initial process of setting scroll position and reduce time needed
// to layout items on screen
if let unreadIndex, let unreadItemId {
scrollView.setScrollPosition(unreadIndex, unreadItemId)
}
scrollView.updateItems(mergedItems.boxedValue.items)
if let unreadIndex {
scrollView.scrollToItem(unreadIndex)
}
loadLastItems($loadingMoreItems, chat)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
allowLoadMoreItems = true
}
}
class FloatingButtonModel: ObservableObject {
@Published var unreadAbove: Int = 0
@Published var unreadBelow: Int = 0
@ -638,7 +657,7 @@ struct ChatView: View {
it.unreadBelow = unreadBelow
it.date = date
}
// set floating button indication mode
let nearBottom = listState.firstVisibleItemIndex < 4
if nearBottom != self.isNearBottom {
@ -646,7 +665,7 @@ struct ChatView: View {
self?.isNearBottom = nearBottom
}
}
// hide Date indicator after 1 second of no scrolling
hideDateWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
@ -710,7 +729,7 @@ struct ChatView: View {
.onTapGesture {
if let index = listState.items.lastIndex(where: { $0.hasUnread() }) {
// scroll to the top unread item
Task { await scrollView.scrollToItem(index, animated: true) }
Task { await scrollView.scrollToItemAnimated(index) }
} else {
logger.debug("No more unread items, total: \(listState.items.count)")
}
@ -737,7 +756,7 @@ struct ChatView: View {
.foregroundColor(theme.colors.primary)
}
.onTapGesture {
scrollView.scrollToBottom()
scrollView.scrollToBottomAnimated()
}
}
} else if !model.isNearBottom {
@ -747,7 +766,7 @@ struct ChatView: View {
circleButton {
Image(systemName: "chevron.down").foregroundColor(theme.colors.primary)
}
.onTapGesture { scrollView.scrollToBottom() }
.onTapGesture { scrollView.scrollToBottomAnimated() }
}
}
}
@ -949,7 +968,7 @@ struct ChatView: View {
)
}
}
func forwardAction(_ items: [Int64]) -> UIAlertAction {
UIAlertAction(
title: NSLocalizedString("Forward messages", comment: "alert action"),
@ -1078,7 +1097,7 @@ struct ChatView: View {
private func reveal(_ yes: Bool) -> Void {
merged.revealItems(yes, $revealedItems)
}
func getItemSeparation(_ chatItem: ChatItem, _ prevItem: ChatItem?) -> ItemSeparation {
guard let prevItem else {
return ItemSeparation(timestamp: true, largeGap: true, date: nil)
@ -1189,7 +1208,7 @@ struct ChatView: View {
let im = ItemsModel.shared
var unreadItems: [ChatItem.ID] = []
var unreadMentions: Int = 0
for i in range {
let ci = im.reversedChatItems[i]
if ci.isRcvNew {
@ -1199,10 +1218,10 @@ struct ChatView: View {
}
}
}
return (unreadItems, unreadMentions)
}
private func waitToMarkRead(_ op: @Sendable @escaping () async -> Void) {
markReadTask = Task {
do {
@ -1215,7 +1234,7 @@ struct ChatView: View {
}
}
}
@available(iOS 16.0, *)
struct MemberLayout: Layout {
@ -1892,7 +1911,7 @@ struct ChatView: View {
)
}
}
private func archiveReportButton(_ cItem: ChatItem) -> Button<some View> {
Button {
archivingReports = [cItem.id]
@ -1940,7 +1959,7 @@ struct ChatView: View {
)
}
}
private func reportButton(_ ci: ChatItem) -> Button<some View> {
Button(role: .destructive) {
var buttons: [ActionSheet.Button] = ReportReason.supportedReasons.map { reason in
@ -1954,9 +1973,9 @@ struct ChatView: View {
}
}
}
buttons.append(.cancel())
actionSheet = SomeActionSheet(
actionSheet: ActionSheet(
title: Text("Report reason?"),
@ -1971,7 +1990,7 @@ struct ChatView: View {
)
}
}
var deleteMessagesTitle: LocalizedStringKey {
let n = deletingItems.count
return n == 1 ? "Delete message?" : "Delete \(n) messages?"
@ -2044,7 +2063,7 @@ struct ChatView: View {
}
}
}
@ViewBuilder private func contactReactionMenu(_ contact: Contact, _ r: CIReactionCount) -> some View {
if !r.userReacted || r.totalReacted > 1 {
Button { showChatInfoSheet = true } label: {

View file

@ -20,12 +20,12 @@ struct NativeTextEditor: UIViewRepresentable {
@Binding var selectedRange: NSRange
let onImagesAdded: ([UploadContent]) -> Void
private let minHeight: CGFloat = 37
static let minHeight: CGFloat = 39
private let defaultHeight: CGFloat = {
let field = CustomUITextField(parent: nil, height: Binding.constant(0))
field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4)
return min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, 37), 360).rounded(.down)
return min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, NativeTextEditor.minHeight), 360).rounded(.down)
}()
func makeUIView(context: Context) -> UITextView {
@ -81,7 +81,7 @@ struct NativeTextEditor: UIViewRepresentable {
// When having emoji in text view and then removing it, sizeThatFits shows previous size (too big for empty text view), so using work around with default size
let newHeight = field.text == ""
? defaultHeight
: min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, minHeight), maxHeight).rounded(.down)
: min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, NativeTextEditor.minHeight), maxHeight).rounded(.down)
if field.frame.size.height != newHeight {
field.frame.size = CGSizeMake(field.frame.size.width, newHeight)

View file

@ -33,7 +33,7 @@ struct SendMessageView: View {
@Namespace var namespace
@Binding var keyboardVisible: Bool
var sendButtonColor = Color.accentColor
@State private var teHeight: CGFloat = 42
@State private var teHeight: CGFloat = NativeTextEditor.minHeight
@State private var teFont: Font = .body
@State private var sendButtonSize: CGFloat = 29
@State private var sendButtonOpacity: CGFloat = 1

View file

@ -115,6 +115,8 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
/// Adds content padding to bottom
var insetBottom: CGFloat = 100
var scrollToItemIndexDelayed: Int? = nil
/// The second scroll view that is used only for purpose of displaying scroll bar with made-up content size and scroll offset that is gathered from main scroll view, see [estimatedContentHeight]
let scrollBarView: UIScrollView = UIScrollView(frame: .zero)
@ -260,6 +262,10 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
}
oldScreenHeight = newScreenHeight
adaptItems(listState.items, false)
if let index = scrollToItemIndexDelayed {
scrollToItem(index)
scrollToItemIndexDelayed = nil
}
}
private func setup() {
@ -284,7 +290,7 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
return
}
adaptItems(items, forceReloadVisible)
snapToContent()
snapToContent(animated: false)
}
/// [forceReloadVisible]: reloads every item that was visible regardless of hashValue changes
@ -454,21 +460,70 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
listState.firstVisibleItemOffset = offset == 0 ? -bounds.height + insetTop + insetBottom : offset
}
func scrollToItem(_ index: Int, animated: Bool, top: Bool = true) async {
func scrollToItem(_ index: Int, top: Bool = true) {
if index >= listState.items.count || listState.isScrolling {
return
}
if bounds.height == 0 {
scrollToItemIndexDelayed = index
return
}
listState.isScrolling = true
defer {
listState.isScrolling = false
}
if !animated {
// just a faster way to set top item as requested index
listState.firstVisibleItemIndex = index
listState.firstVisibleItemId = listState.items[index].id
listState.firstVisibleItemOffset = -bounds.height + insetTop + insetBottom
scrollBarView.flashScrollIndicators()
adaptItems(listState.items, false)
// just a faster way to set top item as requested index
listState.firstVisibleItemIndex = index
listState.firstVisibleItemId = listState.items[index].id
listState.firstVisibleItemOffset = -bounds.height + insetTop + insetBottom
scrollBarView.flashScrollIndicators()
adaptItems(listState.items, false)
var adjustedOffset = self.contentOffset.y
var i = 0
var upPrev = index > listState.firstVisibleItemIndex
//let firstOrLastIndex = upPrev ? listState.visibleItems.last?.index ?? 0 : listState.firstVisibleItemIndex
//let step: CGFloat = max(0.1, CGFloat(abs(index - firstOrLastIndex)) * scrollStepMultiplier)
var stepSlowdownMultiplier: CGFloat = 1
while true {
let up = index > listState.firstVisibleItemIndex
if upPrev != up {
stepSlowdownMultiplier = stepSlowdownMultiplier * 0.5
upPrev = up
}
// these two lines makes scrolling's finish non-linear and NOT overscroll visually when reach target index
let firstOrLastIndex = up ? listState.visibleItems.last?.index ?? 0 : listState.firstVisibleItemIndex
let step: CGFloat = max(0.1, CGFloat(abs(index - firstOrLastIndex)) * scrollStepMultiplier) * stepSlowdownMultiplier
let offsetToScroll = (up ? -averageItemHeight : averageItemHeight) * step * stepSlowdownMultiplier
adjustedOffset += offsetToScroll
if let item = listState.visibleItems.first(where: { $0.index == index }) {
let y = if top {
min(estimatedContentHeight.bottomOffsetY - bounds.height, item.view.frame.origin.y - insetTop)
} else {
max(estimatedContentHeight.topOffsetY - insetTop, item.view.frame.origin.y + item.view.bounds.height - bounds.height + insetBottom)
}
setContentOffset(CGPointMake(contentOffset.x, y), animated: false)
scrollBarView.flashScrollIndicators()
break
}
contentOffset = CGPointMake(contentOffset.x, adjustedOffset)
i += 1
}
estimatedContentHeight.update(contentOffset, listState, averageItemHeight, true)
}
func scrollToItemAnimated(_ index: Int, top: Bool = true) async {
if index >= listState.items.count || listState.isScrolling {
return
}
listState.isScrolling = true
defer {
listState.isScrolling = false
}
var adjustedOffset = self.contentOffset.y
var i = 0
@ -497,28 +552,32 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
} else {
max(estimatedContentHeight.topOffsetY - insetTop, item.view.frame.origin.y + item.view.bounds.height - bounds.height + insetBottom)
}
setContentOffset(CGPointMake(contentOffset.x, y), animated: animated)
setContentOffset(CGPointMake(contentOffset.x, y), animated: true)
scrollBarView.flashScrollIndicators()
break
}
contentOffset = CGPointMake(contentOffset.x, adjustedOffset)
if animated {
// skipping unneded relayout if this offset is already processed
if prevProcessedOffset - contentOffset.y != 0 {
adaptItems(listState.items, false)
snapToContent(animated: false)
}
// let UI time to update to see the animated position change
await MainActor.run {}
// skipping unneded relayout if this offset is already processed
if prevProcessedOffset - contentOffset.y != 0 {
adaptItems(listState.items, false)
snapToContent(animated: false)
}
// let UI time to update to see the animated position change
await MainActor.run {}
i += 1
}
estimatedContentHeight.update(contentOffset, listState, averageItemHeight, true)
}
func scrollToBottom(animated: Bool = true) {
func scrollToBottom() {
scrollToItem(0, top: false)
}
func scrollToBottomAnimated() {
Task {
await scrollToItem(0, animated: animated, top: false)
await scrollToItemAnimated(0, top: false)
}
}
@ -528,12 +587,12 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
if !listState.items.isEmpty {
scrollToBottom()
scrollToBottomAnimated()
}
return false
}
private func snapToContent(animated: Bool = true) {
private func snapToContent(animated: Bool) {
let topBlankSpace = estimatedContentHeight.height < bounds.height ? bounds.height - estimatedContentHeight.height : 0
if topY < estimatedContentHeight.topOffsetY - topBlankSpace {
setContentOffset(CGPointMake(0, estimatedContentHeight.topOffsetY - topBlankSpace), animated: animated)
@ -567,7 +626,7 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
snapToContent()
snapToContent(animated: true)
}
}
@ -582,7 +641,7 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
} else {
DispatchQueue.main.async {
self.setContentOffset(newValue, animated: false)
self.snapToContent()
self.snapToContent(animated: true)
}
}
} else if contentOffset.y > 0 && newOffset.y + bounds.height > estimatedContentHeight.bottomOffsetY && contentOffset.y < newOffset.y {
@ -591,7 +650,7 @@ class EndlessScrollView<ScrollItem>: UIScrollView, UIScrollViewDelegate, UIGestu
} else {
DispatchQueue.main.async {
self.setContentOffset(newValue, animated: false)
self.snapToContent()
self.snapToContent(animated: true)
}
}
}