diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index 3776647b7..0ff39e4e4 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -13,7 +13,8 @@ struct FeedCardNavigationLink: View { var body: some View { let destination = LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)) #if os(iOS) - let modifiedDestination = destination.navigationBarHidden(true) + let modifiedDestination = destination + .navigationTitle("") #else let modifiedDestination = destination #endif @@ -51,7 +52,8 @@ struct GridCardNavigationLink: View { var body: some View { let destination = LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)) #if os(iOS) - let modifiedDestination = destination.navigationBarHidden(true) + let modifiedDestination = destination + .navigationTitle("") #else let modifiedDestination = destination #endif diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 9c1b17f0e..62bd31f04 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -27,8 +27,7 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone loadItems(isRefresh: true) } .searchable( - text: $viewModel.searchTerm, - placement: .navigationBarDrawer + text: $viewModel.searchTerm ) { if viewModel.searchTerm.isEmpty { Text("Inbox").searchCompletion("in:inbox ") @@ -136,23 +135,28 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone var body: some View { VStack(spacing: 0) { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - TextChipButton.makeAddLabelButton { - showLabelsSheet = true + ZStack(alignment: .bottom) { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + TextChipButton.makeAddLabelButton { + showLabelsSheet = true + } + ForEach(viewModel.selectedLabels, id: \.self) { label in + TextChipButton.makeRemovableLabelButton(feedItemLabel: label) { + viewModel.selectedLabels.removeAll { $0.id == label.id } + } + } + Spacer() } - ForEach(viewModel.selectedLabels, id: \.self) { label in - TextChipButton.makeRemovableLabelButton(feedItemLabel: label) { - viewModel.selectedLabels.removeAll { $0.id == label.id } + .padding(.horizontal) + .sheet(isPresented: $showLabelsSheet) { + ApplyLabelsView(mode: .list(viewModel.selectedLabels)) { + viewModel.selectedLabels = $0 } } - Spacer() } - .padding(.horizontal) - .sheet(isPresented: $showLabelsSheet) { - ApplyLabelsView(mode: .list(viewModel.selectedLabels)) { - viewModel.selectedLabels = $0 - } + if viewModel.showLoadingBar { + ShimmeringLoader() } } if prefersListLayout || !enableGrid { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 4c08a1dcd..1d46f3235 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -17,6 +17,7 @@ import Views @Published var snoozePresented = false @Published var itemToSnoozeID: String? @Published var selectedLinkItem: LinkedItem? + @Published var showLoadingBar = false var cursor: String? @@ -47,6 +48,7 @@ import Views searchIdx += 1 isLoading = true + showLoadingBar = true // Cache the viewer if dataService.currentViewer == nil { @@ -81,6 +83,7 @@ import Views receivedIdx = thisSearchIdx cursor = queryResult.cursor await dataService.prefetchPages(itemIDs: newItems.map(\.unwrappedID)) + showLoadingBar = false } else if searchTermIsEmpty { await dataService.viewContext.perform { let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() @@ -94,6 +97,7 @@ import Views self.isLoading = false } } + showLoadingBar = false } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift index 1fbbe840b..b025250da 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift @@ -9,7 +9,7 @@ struct HomeView: View { NavigationView { HomeFeedContainerView(viewModel: viewModel) } - .navigationViewStyle(StackNavigationViewStyle()) + .navigationViewStyle(.stack) .accentColor(.appGrayTextContrast) } else { HomeFeedContainerView(viewModel: viewModel) diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index 6230a4cfb..1b5124b36 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -136,14 +136,27 @@ struct LinkItemDetailView: View { ) } + // We always want this hidden but setting it to false initially + // fixes a bug where SwiftUI searchable will always show the nav bar + // if the search field is active when pushing. + @State var hideNavBar = false + var body: some View { #if os(iOS) if viewModel.item.isPDF { fixedNavBarReader - .task { viewModel.trackReadEvent() } + .navigationBarHidden(hideNavBar) + .task { + hideNavBar = true + viewModel.trackReadEvent() + } } else { WebReaderContainerView(item: viewModel.item, homeFeedViewModel: viewModel.homeFeedViewModel) - .task { viewModel.trackReadEvent() } + .navigationBarHidden(hideNavBar) + .task { + hideNavBar = true + viewModel.trackReadEvent() + } } #else fixedNavBarReader diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift index 96690b7f6..52a477c39 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/HomeFeedCardView.swift @@ -50,7 +50,7 @@ public struct FeedCard: View { if case let AsyncImageStatus.loaded(image) = imageStatus { image .resizable() - .aspectRatio(1, contentMode: .fill) + .aspectRatio(contentMode: .fill) .frame(width: 80, height: 80) .cornerRadius(6) } else if case AsyncImageStatus.loading = imageStatus { diff --git a/apple/OmnivoreKit/Sources/Views/ShimmerView.swift b/apple/OmnivoreKit/Sources/Views/ShimmerView.swift new file mode 100644 index 000000000..b977259f7 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/ShimmerView.swift @@ -0,0 +1,54 @@ +import SwiftUI + +public struct ShimmeringLoader: View { + @State private var phase: CGFloat = 0 + + public init() {} + + public var body: some View { + ZStack { + Color.systemBackground + Color.appGraySolid + .contentShape(Rectangle()) + .modifier(AnimatedMask(phase: phase).animation( + Animation.linear(duration: 2.0) + .repeatForever(autoreverses: false) + )) + .onAppear { phase = 0.8 } + } + .frame(height: 2) + .frame(maxWidth: .infinity) + } + + /// An animatable modifier to interpolate between `phase` values. + struct AnimatedMask: AnimatableModifier { + var phase: CGFloat = 0 + + var animatableData: CGFloat { + get { phase } + set { phase = newValue } + } + + func body(content: Content) -> some View { + content + .mask(GradientMask(phase: phase).scaleEffect(3)) + } + } + + /// An animatable gradient between transparent and opaque to use as mask. + /// The `phase` parameter shifts the gradient, moving the opaque band. + struct GradientMask: View { + let phase: CGFloat + let centerColor = Color.appGraySolid + let edgeColor = Color.clear + + var body: some View { + LinearGradient(gradient: + Gradient(stops: [ + .init(color: edgeColor, location: phase), + .init(color: centerColor, location: phase + 0.1), + .init(color: edgeColor, location: phase + 0.2) + ]), startPoint: .leading, endPoint: .trailing) + } + } +}