From 5d97c7511ff3b79ded8196675a8bee85ec24c091 Mon Sep 17 00:00:00 2001 From: Skitty Date: Tue, 6 Sep 2022 20:23:50 -0500 Subject: [PATCH] Sources: Fix source searching (#8) - Make searching case insensitive - Fix catalog title not hiding when searching an installed source name - Cancelling a search doesn't add an installed source to the catalog - Add dynamic predicate changing for iOS 14 and up instead of restricting to iOS 15 - Migrate updated source fetching to the source model - Change how filtering works to adapt with the dynamic predicate changes Signed-off-by: kingbri Co-authored-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 4 + Ferrite/ViewModels/SourceManager.swift | 18 +- .../CommonViews/DynamicFetchRequest.swift | 26 +++ Ferrite/Views/SourcesView.swift | 157 +++++++++--------- 4 files changed, 122 insertions(+), 83 deletions(-) create mode 100644 Ferrite/Views/CommonViews/DynamicFetchRequest.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 414aeda..0bd3d04 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; }; 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB542890D1BF002BD219 /* UIApplication.swift */; }; 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; }; + 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; }; 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; }; 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; }; 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; }; @@ -91,6 +92,7 @@ 0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; 0C32FB542890D1BF002BD219 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; 0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = ""; }; + 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFetchRequest.swift; sourceTree = ""; }; 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = ""; }; 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = ""; }; @@ -255,6 +257,7 @@ 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */, + 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */, ); path = CommonViews; sourceTree = ""; @@ -458,6 +461,7 @@ 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */, 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */, 0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */, + 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */, 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, 0C794B6B289DACF100DD1CC8 /* SourceCatalogView.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */, diff --git a/Ferrite/ViewModels/SourceManager.swift b/Ferrite/ViewModels/SourceManager.swift index 072a829..d4f83e0 100644 --- a/Ferrite/ViewModels/SourceManager.swift +++ b/Ferrite/ViewModels/SourceManager.swift @@ -7,7 +7,7 @@ import CoreData import Foundation -import UIKit +import SwiftUI public class SourceManager: ObservableObject { var toastModel: ToastViewModel? @@ -52,6 +52,22 @@ public class SourceManager: ObservableObject { } } + func fetchUpdatedSources(installedSources: FetchedResults) -> [SourceJson] { + var updatedSources: [SourceJson] = [] + + for source in installedSources { + if let availableSource = availableSources.first(where: { + source.listId == $0.listId && source.name == $0.name && source.author == $0.author + }), + availableSource.version > source.version + { + updatedSources.append(availableSource) + } + } + + return updatedSources + } + // Checks if the current app version is supported by the source func checkAppVersion(minVersion: String?) -> Bool { // If there's no min version, assume that every version is supported diff --git a/Ferrite/Views/CommonViews/DynamicFetchRequest.swift b/Ferrite/Views/CommonViews/DynamicFetchRequest.swift new file mode 100644 index 0000000..02ccac8 --- /dev/null +++ b/Ferrite/Views/CommonViews/DynamicFetchRequest.swift @@ -0,0 +1,26 @@ +// +// DynamicFetchRequest.swift +// Ferrite +// +// Created by Brian Dashore on 9/6/22. +// + +import CoreData +import SwiftUI + +struct DynamicFetchRequest: View { + @FetchRequest var fetchRequest: FetchedResults + + let content: (FetchedResults) -> Content + + var body: some View { + content(fetchRequest) + } + + init(predicate: NSPredicate?, + @ViewBuilder content: @escaping (FetchedResults) -> Content) + { + _fetchRequest = FetchRequest(sortDescriptors: [], predicate: predicate) + self.content = content + } +} diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index 2d1ab4b..c57e8f5 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -20,24 +20,6 @@ struct SourcesView: View { sortDescriptors: [] ) var sources: FetchedResults - private var updatedSources: [SourceJson] { - var tempSources: [SourceJson] = [] - - for source in sources { - guard let availableSource = sourceManager.availableSources.first(where: { - source.listId == $0.listId && source.name == $0.name && source.author == $0.author - }) else { - continue - } - - if availableSource.version > source.version { - tempSources.append(availableSource) - } - } - - return tempSources - } - @State private var checkedForSources = false @State private var isEditing = false @@ -45,92 +27,103 @@ struct SourcesView: View { @State private var searchText: String = "" @State private var filteredUpdatedSources: [SourceJson] = [] @State private var filteredAvailableSources: [SourceJson] = [] + @State private var sourcePredicate: NSPredicate? var body: some View { NavView { - ZStack { - if !checkedForSources { - ProgressView() - } else if sources.isEmpty, sourceManager.availableSources.isEmpty { - EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings") - } else { - List { - if !filteredUpdatedSources.isEmpty { - Section(header: InlineHeader("Updates")) { - ForEach(filteredUpdatedSources, id: \.self) { source in - SourceUpdateButtonView(updatedSource: source) + DynamicFetchRequest(predicate: sourcePredicate) { (installedSources: FetchedResults) in + ZStack { + if !checkedForSources { + ProgressView() + } else if sources.isEmpty, sourceManager.availableSources.isEmpty { + EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings") + } else { + List { + if !filteredUpdatedSources.isEmpty { + Section(header: InlineHeader("Updates")) { + ForEach(filteredUpdatedSources, id: \.self) { source in + SourceUpdateButtonView(updatedSource: source) + } } } - } - if !sources.isEmpty { - Section(header: InlineHeader("Installed")) { - ForEach(sources, id: \.self) { source in - InstalledSourceView(installedSource: source) + if !installedSources.isEmpty { + Section(header: InlineHeader("Installed")) { + ForEach(installedSources, id: \.self) { source in + InstalledSourceView(installedSource: source) + } } } - } - if !filteredAvailableSources.isEmpty, sourceManager.availableSources.contains(where: { availableSource in - !sources.contains( - where: { - availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author - } - ) - }) { - Section(header: InlineHeader("Catalog")) { - ForEach(filteredAvailableSources, id: \.self) { availableSource in - if !sources.contains( - where: { + if !filteredAvailableSources.isEmpty { + Section(header: InlineHeader("Catalog")) { + ForEach(filteredAvailableSources, id: \.self) { availableSource in + if !installedSources.contains(where: { availableSource.name == $0.name && availableSource.listId == $0.listId && availableSource.author == $0.author + }) { + SourceCatalogButtonView(availableSource: availableSource) } - ) { - SourceCatalogButtonView(availableSource: availableSource) } } } } + .conditionalId(UUID()) + .listStyle(.insetGrouped) } - .conditionalId(UUID()) - .listStyle(.insetGrouped) } - } - .sheet(isPresented: $navModel.showSourceSettings) { - SourceSettingsView() - .environmentObject(navModel) - } - .onAppear { - filteredUpdatedSources = updatedSources - viewTask = Task { - await sourceManager.fetchSourcesFromUrl() - filteredAvailableSources = sourceManager.availableSources - checkedForSources = true + .sheet(isPresented: $navModel.showSourceSettings) { + SourceSettingsView() + .environmentObject(navModel) } - } - .onDisappear { - viewTask?.cancel() - } - .navigationTitle("Sources") - .navigationSearchBar { - SearchBar("Search", text: $searchText, isEditing: $isEditing) - .showsCancelButton(isEditing) - .onCancel { - searchText = "" + .onAppear { + viewTask = Task { + await sourceManager.fetchSourcesFromUrl() + filteredAvailableSources = sourceManager.availableSources.filter { availableSource in + !installedSources.contains(where: { + availableSource.name == $0.name && + availableSource.listId == $0.listId && + availableSource.author == $0.author + }) + } + + filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources) + checkedForSources = true } - } - .onChange(of: searchText) { _ in - filteredAvailableSources = sourceManager.availableSources.filter { searchText.isEmpty ? true : $0.name.contains(searchText) } - filteredUpdatedSources = updatedSources.filter { searchText.isEmpty ? true : $0.name.contains(searchText) } - if #available(iOS 15.0, *) { - if searchText.isEmpty { - sources.nsPredicate = nil - } else { - sources.nsPredicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText) + } + .onDisappear { + viewTask?.cancel() + } + .onChange(of: searchText) { _ in + sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText) + } + .onReceive(installedSources.publisher.count()) { _ in + filteredAvailableSources = sourceManager.availableSources.filter { availableSource in + let sourceExists = installedSources.contains(where: { + availableSource.name == $0.name && + availableSource.listId == $0.listId && + availableSource.author == $0.author + }) + + if searchText.isEmpty { + return !sourceExists + } else { + return !sourceExists && availableSource.name.lowercased().contains(searchText.lowercased()) + } } + + filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources).filter { + searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased()) + } + } + .navigationTitle("Sources") + .navigationSearchBar { + SearchBar("Search", text: $searchText, isEditing: $isEditing) + .showsCancelButton(isEditing) + .onCancel { + searchText = "" + } } } }