Merge pull request #578 from omnivore-app/feature/horizontal-loading-indicator
Shimmering loading indicator [iOS]
This commit is contained in:
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
@ -94,6 +97,7 @@ import Views
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
showLoadingBar = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ struct HomeView: View {
|
||||
NavigationView {
|
||||
HomeFeedContainerView(viewModel: viewModel)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.navigationViewStyle(.stack)
|
||||
.accentColor(.appGrayTextContrast)
|
||||
} else {
|
||||
HomeFeedContainerView(viewModel: viewModel)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
54
apple/OmnivoreKit/Sources/Views/ShimmerView.swift
Normal file
54
apple/OmnivoreKit/Sources/Views/ShimmerView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user