diff --git a/App/Features/Entry/EntryView.swift b/App/Features/Entry/EntryView.swift index 65c8beb0..3b6ae3c3 100644 --- a/App/Features/Entry/EntryView.swift +++ b/App/Features/Entry/EntryView.swift @@ -80,8 +80,8 @@ struct EntryView: View { ) } .sheet(isPresented: $showTag) { - TagListFor(tagsForEntry: TagsForEntryPublisher(entry: entry)) - .environment(\.managedObjectContext, context) + TagListFor(entry: entry) + .presentationDetents([.medium, .large]) } #if os(iOS) .navigationBarTitleDisplayMode(.inline) diff --git a/App/Features/Sync/AppSync.swift b/App/Features/Sync/AppSync.swift index 97139651..19f61108 100644 --- a/App/Features/Sync/AppSync.swift +++ b/App/Features/Sync/AppSync.swift @@ -33,8 +33,8 @@ final class AppSync { progress = 0 entriesSynced = [] Task.detached(priority: .userInitiated) { [unowned self] in - await synchronizeEntries() await synchronizeTags() + await synchronizeEntries() purge() await MainActor.run { self.inProgress = false @@ -141,6 +141,7 @@ extension AppSync { tags[wallabagTag.id] = tag } } + try backgroundContext.save() } catch _ {} } } diff --git a/App/Features/Tag/TagListFor.swift b/App/Features/Tag/TagListFor.swift index 2d118822..f2487735 100644 --- a/App/Features/Tag/TagListFor.swift +++ b/App/Features/Tag/TagListFor.swift @@ -4,24 +4,57 @@ import SwiftUI struct TagListFor: View { @EnvironmentObject var appState: AppState @State private var tagLabel: String = "" + @ObservedObject var entry: Entry - @ObservedObject var tagsForEntry: TagsForEntryPublisher + @State var viewModel = TagsForEntryViewModel() var body: some View { - VStack(alignment: .leading) { - Text("Tag").font(.largeTitle).bold().padding() - HStack { - TextField("New tag", text: $tagLabel) - Button(action: { - Task { - await tagsForEntry.add(tag: tagLabel) - tagLabel = "" + NavigationStack { + Form { + Section("New tag") { + TextField("Tag name", text: $tagLabel) + Button(action: { + Task { + await viewModel.add(tag: tagLabel, for: entry) + tagLabel = "" + } + }, label: { + if viewModel.isLoading { + ProgressView() + } else { + Text("Add") + } + }) + .disabled(viewModel.isLoading) + } + Section("Tags list") { + List(viewModel.tags) { tag in + Button(action: { + Task { + await viewModel.toggle(tag: tag, for: entry) + } + }, label: { + HStack { + Text(tag.label) + Spacer() + if viewModel.isLoading { + ProgressView() + } else { + Image(systemName: tag.isChecked ? "checkmark.circle" : "circle") + } + } + .contentShape(Rectangle()) + }) + .buttonStyle(.plain) + .disabled(viewModel.isLoading) } - }, label: { Text("Add") }) - }.padding(.horizontal) - List(tagsForEntry.tags) { tag in - TagRow(tag: tag, tagsForEntry: tagsForEntry) + } } + .task { + await viewModel.load(for: entry) + } + .navigationTitle("Tag") + .navigationBarTitleDisplayMode(.large) } } } diff --git a/App/Features/Tag/TagRow.swift b/App/Features/Tag/TagRow.swift deleted file mode 100644 index 74369f6e..00000000 --- a/App/Features/Tag/TagRow.swift +++ /dev/null @@ -1,32 +0,0 @@ -import SwiftUI - -struct TagRow: View { - @ObservedObject var tag: Tag - @ObservedObject var tagsForEntry: TagsForEntryPublisher - - var body: some View { - HStack { - Text(tag.label) - if tag.isChecked { - Spacer() - Image(systemName: "checkmark") - } - }.onTapGesture { - Task { - if tag.isChecked { - await tagsForEntry.delete(tag: tag) - } else { - await tagsForEntry.add(tag: tag) - } - } - } - } -} - -/* - struct TagRow_Previews: PreviewProvider { - static var previews: some View { - TagRow(tag: Tag(), entry: Entry()) - } - } - */ diff --git a/App/Features/Tag/TagsForEntryPublisher.swift b/App/Features/Tag/TagsForEntryPublisher.swift deleted file mode 100644 index 5077044c..00000000 --- a/App/Features/Tag/TagsForEntryPublisher.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Combine -import CoreData -import Factory -import Foundation - -// swiftlint:disable:next all -class TagsForEntryPublisher: ObservableObject { - @Injected(\.wallabagSession) private var session - - var objectWillChange = PassthroughSubject() - - var tags: [Tag] - var entry: Entry - - @CoreDataViewContext var coreDataContext: NSManagedObjectContext - - init(entry: Entry) { - self.entry = entry - tags = (try? Container.shared.coreData().viewContext.fetch(Tag.fetchRequestSorted())) ?? [] - - tags.filter { tag in - entry.tags.contains(tag) - }.forEach { $0.isChecked = true } - } - - func add(tag: Tag) async { - await add(tag: tag.label) - tag.isChecked = true - } - - func add(tag: String) async { - try? await session.add(tag: tag, for: entry) - objectWillChange.send() - } - - func delete(tag: Tag) async { - tag.isChecked = false - tag.objectWillChange.send() - try? await session.delete(tag: tag, for: entry) - } -} diff --git a/App/Features/Tag/TagsForEntryViewModel.swift b/App/Features/Tag/TagsForEntryViewModel.swift new file mode 100644 index 00000000..a979063b --- /dev/null +++ b/App/Features/Tag/TagsForEntryViewModel.swift @@ -0,0 +1,55 @@ +import CoreData +import Factory +import Foundation + +@Observable +final class TagsForEntryViewModel { + @ObservationIgnored + @Injected(\.wallabagSession) private var session + + var tags: [Tag] = [] + var entry: Entry? + var isLoading = false + + @ObservationIgnored + @CoreDataViewContext var coreDataContext: NSManagedObjectContext + + @MainActor + func load(for entry: Entry) async { + tags = (try? coreDataContext.fetch(Tag.fetchRequestSorted())) ?? [] + tags.filter { tag in + entry.tags.contains(tag) + }.forEach { + $0.isChecked = true + } + } + + func toggle(tag: Tag, for entry: Entry) async { + defer { + isLoading = false + } + isLoading = true + if tag.isChecked { + await delete(tag: tag, for: entry) + } else { + await add(tag: tag, for: entry) + } + + await load(for: entry) + } + + func add(tag: String, for entry: Entry) async { + try? await session.add(tag: tag, for: entry) + await load(for: entry) + } + + private func add(tag: Tag, for entry: Entry) async { + await add(tag: tag.label, for: entry) + tag.isChecked = true + } + + private func delete(tag: Tag, for entry: Entry) async { + tag.isChecked = false + try? await session.delete(tag: tag, for: entry) + } +} diff --git a/wallabag.xcodeproj/project.pbxproj b/wallabag.xcodeproj/project.pbxproj index 929d0229..2ea50be3 100644 --- a/wallabag.xcodeproj/project.pbxproj +++ b/wallabag.xcodeproj/project.pbxproj @@ -74,8 +74,7 @@ 09644CF225C986EE000FFDA1 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644CF025C986EE000FFDA1 /* SearchViewModel.swift */; }; 09644CF325C986EE000FFDA1 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644CF125C986EE000FFDA1 /* SearchView.swift */; }; 09644CFD25C9870C000FFDA1 /* TagListFor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644CFA25C9870C000FFDA1 /* TagListFor.swift */; }; - 09644CFE25C9870C000FFDA1 /* TagRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644CFB25C9870C000FFDA1 /* TagRow.swift */; }; - 09644CFF25C9870C000FFDA1 /* TagsForEntryPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644CFC25C9870C000FFDA1 /* TagsForEntryPublisher.swift */; }; + 09644CFF25C9870C000FFDA1 /* TagsForEntryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644CFC25C9870C000FFDA1 /* TagsForEntryViewModel.swift */; }; 09644D0825C9871A000FFDA1 /* TipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644D0625C9871A000FFDA1 /* TipView.swift */; }; 09644D0925C9871A000FFDA1 /* TipViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644D0725C9871A000FFDA1 /* TipViewModel.swift */; }; 09644D1025C9872F000FFDA1 /* BundleKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644D0F25C9872F000FFDA1 /* BundleKey.swift */; }; @@ -220,8 +219,7 @@ 09644CF025C986EE000FFDA1 /* SearchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; 09644CF125C986EE000FFDA1 /* SearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; 09644CFA25C9870C000FFDA1 /* TagListFor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagListFor.swift; sourceTree = ""; }; - 09644CFB25C9870C000FFDA1 /* TagRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagRow.swift; sourceTree = ""; }; - 09644CFC25C9870C000FFDA1 /* TagsForEntryPublisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagsForEntryPublisher.swift; sourceTree = ""; }; + 09644CFC25C9870C000FFDA1 /* TagsForEntryViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TagsForEntryViewModel.swift; sourceTree = ""; }; 09644D0625C9871A000FFDA1 /* TipView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TipView.swift; sourceTree = ""; }; 09644D0725C9871A000FFDA1 /* TipViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TipViewModel.swift; sourceTree = ""; }; 09644D0F25C9872F000FFDA1 /* BundleKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BundleKey.swift; sourceTree = ""; }; @@ -611,8 +609,7 @@ isa = PBXGroup; children = ( 09644CFA25C9870C000FFDA1 /* TagListFor.swift */, - 09644CFB25C9870C000FFDA1 /* TagRow.swift */, - 09644CFC25C9870C000FFDA1 /* TagsForEntryPublisher.swift */, + 09644CFC25C9870C000FFDA1 /* TagsForEntryViewModel.swift */, ); path = Tag; sourceTree = ""; @@ -1034,7 +1031,6 @@ 099CD1312B501D950029E94A /* WallabagPlusStore.swift in Sources */, 09644CD725C986C0000FFDA1 /* ClientIdClientSecretViewModel.swift in Sources */, 09644CCB25C9869D000FFDA1 /* RegistrationView.swift in Sources */, - 09644CFE25C9870C000FFDA1 /* TagRow.swift in Sources */, 09644BFC25C983F8000FFDA1 /* Entry.swift in Sources */, 097F81EB25CB18BA006C85F6 /* Router.swift in Sources */, 81505C5D2B5DC23C003B5CDE /* AddEntryIntent.swift in Sources */, @@ -1062,7 +1058,7 @@ 09644CDF25C986C6000FFDA1 /* LoginViewModel.swift in Sources */, 09644C5D25C98596000FFDA1 /* AddEntryView.swift in Sources */, 09644C6025C98596000FFDA1 /* EntryView.swift in Sources */, - 09644CFF25C9870C000FFDA1 /* TagsForEntryPublisher.swift in Sources */, + 09644CFF25C9870C000FFDA1 /* TagsForEntryViewModel.swift in Sources */, 09644D1025C9872F000FFDA1 /* BundleKey.swift in Sources */, 81505C5B2B5DC21F003B5CDE /* WallabagIntent.swift in Sources */, 09644BAF25C98213000FFDA1 /* RefreshButton.swift in Sources */, @@ -1281,7 +1277,7 @@ ONLY_ACTIVE_ARCH = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_STRICT_CONCURRENCY = targeted; }; name = Debug; }; @@ -1339,7 +1335,7 @@ MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_STRICT_CONCURRENCY = targeted; }; name = Release; };