From 27eb891acc6d3f679687f14d3549e2c0fc280644 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 29 May 2023 14:32:56 +0800 Subject: [PATCH 1/2] Sort the filter labels on iOS --- .../Labels/FilterByLabelsViewModel.swift | 66 ++++++++++++++++--- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/FilterByLabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/FilterByLabelsViewModel.swift index 255a06920..70cbfcd6c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/FilterByLabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/FilterByLabelsViewModel.swift @@ -1,3 +1,4 @@ +import CoreData import Models import Services import SwiftUI @@ -11,6 +12,14 @@ import Views @Published var unselectedLabels = [LinkedItemLabel]() @Published var labelSearchFilter = "" + func setLabels(_ labels: [LinkedItemLabel]) { + self.labels = labels.sorted { left, right in + let aTrimmed = left.unwrappedName.trimmingCharacters(in: .whitespaces) + let bTrimmed = right.unwrappedName.trimmingCharacters(in: .whitespaces) + return aTrimmed.caseInsensitiveCompare(bTrimmed) == .orderedAscending + } + } + func loadLabels( dataService: DataService, initiallySelectedLabels: [LinkedItemLabel], @@ -18,22 +27,59 @@ import Views ) async { isLoading = true - if let labelIDs = try? await dataService.labels() { - dataService.viewContext.performAndWait { - self.labels = labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel } + await loadLabelsFromStore(dataService: dataService) + for label in labels { + if initiallySelectedLabels.contains(label) { + selectedLabels.append(label) + } else if initiallyNegatedLabels.contains(label) { + negatedLabels.append(label) + } else { + unselectedLabels.append(label) } + } - for label in labels { - if initiallySelectedLabels.contains(label) { - selectedLabels.append(label) - } else if initiallyNegatedLabels.contains(label) { - negatedLabels.append(label) - } else { - unselectedLabels.append(label) + Task.detached(priority: .userInitiated) { + if let labelIDs = try? await dataService.labels() { + DispatchQueue.main.async { + dataService.viewContext.performAndWait { + self.setLabels(labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel }) + } + for label in self.labels { + if initiallySelectedLabels.contains(label) { + self.selectedLabels.append(label) + } else if initiallyNegatedLabels.contains(label) { + self.negatedLabels.append(label) + } else { + self.unselectedLabels.append(label) + } + } } } } isLoading = false } + + func loadLabelsFromStore(dataService: DataService) async { + let fetchRequest: NSFetchRequest = LinkedItemLabel.fetchRequest() + + let fetchedLabels = await dataService.viewContext.perform { + try? fetchRequest.execute() + } + + setLabels(fetchedLabels ?? []) + unselectedLabels = fetchedLabels ?? [] + } + + func fetchLabelsFromNetwork(dataService: DataService) async { + let labelIDs = try? await dataService.labels() + guard let labelIDs = labelIDs else { return } + + let fetchedLabels = await dataService.viewContext.perform { + labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel } + } + + setLabels(fetchedLabels) + unselectedLabels = fetchedLabels + } } From 50bbdfa44111eff591c378b4504324dd58c21d6f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 29 May 2023 15:02:51 +0800 Subject: [PATCH 2/2] Use the same sorting of labels on iOS as Web --- .../Sources/Models/DataModels/FeedItem.swift | 33 +++++++- .../Views/FeedItem/LabelsFlowLayout.swift | 75 +++++++++++++++++++ .../Views/FeedItem/LibraryItemCard.swift | 15 +--- 3 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Views/FeedItem/LabelsFlowLayout.swift diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift index ac57b4290..92d4eda80 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift @@ -127,9 +127,38 @@ public extension LinkedItem { } var sortedLabels: [LinkedItemLabel] { - labels.asArray(of: LinkedItemLabel.self).sorted { - ($0.name ?? "").lowercased() < ($1.name ?? "").lowercased() + sortedLabels(labels: labels.asArray(of: LinkedItemLabel.self)) + } + + func sortedLabels(labels: [LinkedItemLabel]?) -> [LinkedItemLabel] { + guard let labels = labels else { + return [] } + + var colors = [String: [LinkedItemLabel]]() + for label in labels { + if let color = label.color { + var list = colors[color] ?? [] + list.append(label) + colors[color] = list.sorted(by: { ($0.name ?? "").localizedCompare($1.name ?? "") == .orderedAscending }) + } + } + + let sortedColors = Array(colors.keys).sorted(by: { leftLabel, rightLabel -> Bool in + let aname = colors[leftLabel]?.first?.name ?? leftLabel + let bname = colors[rightLabel]?.first?.name ?? rightLabel + return aname.localizedCompare(bname) == .orderedAscending + }) + + var result = [LinkedItemLabel]() + for key in sortedColors { + if let items = colors[key] { + let sorted = items.sorted(by: { ($0.name ?? "").localizedCompare($1.name ?? "") == .orderedAscending }) + result.append(contentsOf: sorted) + } + } + + return result } var sortedHighlights: [Highlight] { diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LabelsFlowLayout.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LabelsFlowLayout.swift new file mode 100644 index 000000000..60eb7cadb --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LabelsFlowLayout.swift @@ -0,0 +1,75 @@ +// +// based on: LabelsMasonaryView.swift we should try to combine the two + +import Foundation +import Models +import SwiftUI + +struct LabelsFlowLayout: View { + @State private var totalHeight = CGFloat.zero + private var labelItems: [LinkedItemLabel] + + init( + labels: [LinkedItemLabel] + ) { + self.labelItems = labels + } + + var body: some View { + VStack { + GeometryReader { geometry in + self.generateContent(in: geometry) + } + }.padding(5) + .frame(height: totalHeight) + } + + private func generateContent(in geom: GeometryProxy) -> some View { + var width = CGFloat.zero + var height = CGFloat.zero + + return ZStack(alignment: .topLeading) { + ForEach(self.labelItems, id: \.self) { label in + self.item(for: label) + .padding(.horizontal, 1) + .padding(.vertical, 1) + .alignmentGuide(.leading, computeValue: { dim in + if abs(width - dim.width) > geom.size.width { + width = 0 + height -= dim.height + } + let result = width + if label == self.labelItems.last! { + width = 0 // last item + } else { + width -= dim.width + } + return result + }) + .alignmentGuide(.top, computeValue: { _ in + let result = height + if label == self.labelItems.last! { + height = 0 // last item + } + return result + }) + } + } + .background(viewHeightReader($totalHeight)) + } + + private func item(for item: LinkedItemLabel) -> some View { + let chip = TextChip(feedItemLabel: item, padded: false, onTap: nil) + return chip + } + + private func viewHeightReader(_ binding: Binding) -> some View { + GeometryReader { geometry -> Color in + let rect = geometry.frame(in: .local) + DispatchQueue.main.async { + binding.wrappedValue = rect.size.height + } + return .clear + } + } +} diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift index f42d86e05..965404147 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift @@ -213,19 +213,8 @@ public struct LibraryItemCard: View { } var labels: some View { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - ForEach(item.sortedLabels, id: \.self) { - TextChip(feedItemLabel: $0) - } - Spacer() - } - }.introspectScrollView { scrollView in - #if os(iOS) - scrollView.bounces = false - #endif - } - .padding(.top, 0) + LabelsFlowLayout(labels: item.sortedLabels) + .padding(.top, 0) #if os(macOS) .onTapGesture { tapHandler()