diff --git a/ios/Platform/Platform/Features/Fitness/Views/GoalsView.swift b/ios/Platform/Platform/Features/Fitness/Views/GoalsView.swift index f1a47f0..5b2b57a 100644 --- a/ios/Platform/Platform/Features/Fitness/Views/GoalsView.swift +++ b/ios/Platform/Platform/Features/Fitness/Views/GoalsView.swift @@ -24,6 +24,10 @@ struct GoalsView: View { .padding(.top, 8) } .background(Color.canvas) + .scrollDismissesKeyboard(.interactively) + .onTapGesture { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } .task { await vm.load() } diff --git a/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift b/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift index f5e9a14..e1f0a40 100644 --- a/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/EntryListView.swift @@ -3,6 +3,8 @@ import SwiftUI struct EntryListView: View { @Bindable var vm: ReaderViewModel var isCardView: Bool = true + @State private var visibleEntryIDs: Set = [] + @State private var readByScrollIDs: Set = [] var body: some View { if vm.isLoading && vm.entries.isEmpty { @@ -42,12 +44,8 @@ struct EntryListView: View { } .buttonStyle(.plain) .contentShape(Rectangle()) - .onDisappear { - // Mark as read when card scrolls off the top - if !entry.isRead { - Task { await vm.markAsRead(entry) } - } - } + .onAppear { visibleEntryIDs.insert(entry.id) } + .onDisappear { markReadIfScrolled(entry) } .contextMenu { entryContextMenu(entry: entry, vm: vm) } @@ -71,11 +69,8 @@ struct EntryListView: View { } .buttonStyle(.plain) .contentShape(Rectangle()) - .onDisappear { - if !entry.isRead { - Task { await vm.markAsRead(entry) } - } - } + .onAppear { visibleEntryIDs.insert(entry.id) } + .onDisappear { markReadIfScrolled(entry) } .contextMenu { entryContextMenu(entry: entry, vm: vm) } @@ -90,6 +85,24 @@ struct EntryListView: View { } } + // MARK: - Mark as read on scroll + + /// Only mark as read if the entry was visible before AND has scrolled off + /// while other entries are still visible (i.e. user is scrolling, not navigating away) + private func markReadIfScrolled(_ entry: ReaderEntry) { + // Remove from visible set + visibleEntryIDs.remove(entry.id) + + // If there are still visible entries, user is scrolling (not navigating) + // and this entry scrolled off → mark as read + guard !visibleEntryIDs.isEmpty else { return } + guard !entry.isRead else { return } + guard !readByScrollIDs.contains(entry.id) else { return } + + readByScrollIDs.insert(entry.id) + Task { await vm.markAsRead(entry) } + } + private var loadMoreTrigger: some View { Group { if vm.isLoadingMore { @@ -113,7 +126,6 @@ struct EntryCardView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - // Thumbnail if let thumbURL = entry.thumbnailURL { AsyncImage(url: thumbURL) { phase in switch phase { @@ -129,7 +141,6 @@ struct EntryCardView: View { } } - // Content VStack(alignment: .leading, spacing: 8) { HStack(spacing: 6) { if !entry.isRead {