From 647f87744b018da283c73b065f43cce653f324d0 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 15 Jul 2021 20:28:36 +0800 Subject: [PATCH] feat: add search history back --- .../CoreData.xcdatamodel/contents | 16 +- CoreDataStack/Entity/MastodonUser.swift | 1 + CoreDataStack/Entity/SearchHistory.swift | 14 +- CoreDataStack/Entity/Status.swift | 7 +- CoreDataStack/Entity/Tag.swift | 3 + Mastodon.xcodeproj/project.pbxproj | 94 ++++++-- ...SearchHistoryFetchedResultController.swift | 53 +++++ .../Diffiable/Item/SearchHistoryItem.swift | 41 ++++ .../Diffiable/Item/SearchResultItem.swift | 14 -- .../{ => Compose}/AutoCompleteSection.swift | 0 .../ComposeStatusAttachmentSection.swift | 0 .../ComposeStatusPollSection.swift | 0 .../{ => Compose}/ComposeStatusSection.swift | 0 .../CustomEmojiPickerSection.swift | 0 .../CategoryPickerSection.swift | 0 .../{ => Onboarding}/PickServerSection.swift | 0 .../RecommendAccountSection.swift | 0 .../RecommendHashTagSection.swift | 0 .../Section/Search/SearchHistorySection.swift | 56 +++++ .../{ => Search}/SearchResultSection.swift | 0 .../{ => Status}/NotificationSection.swift | 0 .../Section/{ => Status}/PollSection.swift | 0 .../Section/{ => Status}/ReportSection.swift | 0 .../Section/{ => Status}/StatusSection.swift | 0 .../Scene/Search/Search/SearchViewModel.swift | 203 +----------------- .../SearchDetailViewController.swift | 55 ++++- .../SearchHistoryViewController.swift | 128 +++++++++++ .../SearchHistoryViewModel.swift | 132 ++++++++++++ .../View/SearchHistoryTableHeaderView.swift | 96 +++++++++ ...hResultViewController+StatusProvider.swift | 0 .../SearchResultViewController.swift | 7 +- .../SearchResultViewModel+State.swift | 0 .../SearchResult/SearchResultViewModel.swift | 57 +++++ .../SearchResultTableViewCell.swift | 6 +- .../SearchHistoryTableHeaderView.swift | 29 --- .../SearchHistoryViewController.swift | 17 -- ...lViewControllerAnimatedTransitioning.swift | 6 +- .../Search/SearchTransitionController.swift | 9 + .../StatusFilterService.swift | 0 39 files changed, 738 insertions(+), 306 deletions(-) create mode 100644 Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift create mode 100644 Mastodon/Diffiable/Item/SearchHistoryItem.swift rename Mastodon/Diffiable/Section/{ => Compose}/AutoCompleteSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Compose}/ComposeStatusAttachmentSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Compose}/ComposeStatusPollSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Compose}/ComposeStatusSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Compose}/CustomEmojiPickerSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Onboarding}/CategoryPickerSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Onboarding}/PickServerSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Search}/RecommendAccountSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Search}/RecommendHashTagSection.swift (100%) create mode 100644 Mastodon/Diffiable/Section/Search/SearchHistorySection.swift rename Mastodon/Diffiable/Section/{ => Search}/SearchResultSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Status}/NotificationSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Status}/PollSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Status}/ReportSection.swift (100%) rename Mastodon/Diffiable/Section/{ => Status}/StatusSection.swift (100%) create mode 100644 Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift create mode 100644 Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift create mode 100644 Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift rename Mastodon/Scene/Search/{ => SearchDetail}/SearchResult/SearchResultViewController+StatusProvider.swift (100%) rename Mastodon/Scene/Search/{ => SearchDetail}/SearchResult/SearchResultViewController.swift (98%) rename Mastodon/Scene/Search/{ => SearchDetail}/SearchResult/SearchResultViewModel+State.swift (100%) rename Mastodon/Scene/Search/{ => SearchDetail}/SearchResult/SearchResultViewModel.swift (72%) rename Mastodon/Scene/Search/{SearchResult => SearchDetail}/TableViewCell/SearchResultTableViewCell.swift (98%) delete mode 100644 Mastodon/Scene/Search/SearchHistory/SearchHistoryTableHeaderView.swift delete mode 100644 Mastodon/Scene/Search/SearchHistory/SearchHistoryViewController.swift rename Mastodon/{Diffiable/Section => Service}/StatusFilterService.swift (100%) diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index 98ed7f356..5bc61b648 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -136,6 +136,7 @@ + @@ -182,8 +183,9 @@ - - + + + @@ -234,6 +236,7 @@ + @@ -266,6 +269,7 @@ + @@ -277,16 +281,16 @@ - + - + - + - + \ No newline at end of file diff --git a/CoreDataStack/Entity/MastodonUser.swift b/CoreDataStack/Entity/MastodonUser.swift index d442ec753..6b27b4cd8 100644 --- a/CoreDataStack/Entity/MastodonUser.swift +++ b/CoreDataStack/Entity/MastodonUser.swift @@ -43,6 +43,7 @@ final public class MastodonUser: NSManagedObject { // one-to-one relationship @NSManaged public private(set) var pinnedStatus: Status? @NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication? + @NSManaged public private(set) var searchHistory: SearchHistory? // one-to-many relationship @NSManaged public private(set) var statuses: Set? diff --git a/CoreDataStack/Entity/SearchHistory.swift b/CoreDataStack/Entity/SearchHistory.swift index d924917ee..da6d98bc2 100644 --- a/CoreDataStack/Entity/SearchHistory.swift +++ b/CoreDataStack/Entity/SearchHistory.swift @@ -13,9 +13,11 @@ public final class SearchHistory: NSManagedObject { @NSManaged public private(set) var identifier: ID @NSManaged public private(set) var createAt: Date @NSManaged public private(set) var updatedAt: Date - + + // one-to-one relationship @NSManaged public private(set) var account: MastodonUser? @NSManaged public private(set) var hashtag: Tag? + @NSManaged public private(set) var status: Status? } @@ -51,6 +53,16 @@ extension SearchHistory { searchHistory.hashtag = hashtag return searchHistory } + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + status: Status + ) -> SearchHistory { + let searchHistory: SearchHistory = context.insertObject() + searchHistory.status = status + return searchHistory + } } public extension SearchHistory { diff --git a/CoreDataStack/Entity/Status.swift b/CoreDataStack/Entity/Status.swift index 14f687241..717e54ab7 100644 --- a/CoreDataStack/Entity/Status.swift +++ b/CoreDataStack/Entity/Status.swift @@ -38,20 +38,21 @@ public final class Status: NSManagedObject { @NSManaged public private(set) var language: String? // (ISO 639 Part 1 two-letter language code) @NSManaged public private(set) var text: String? - // many-to-one relastionship + // many-to-one relationship @NSManaged public private(set) var author: MastodonUser @NSManaged public private(set) var reblog: Status? @NSManaged public private(set) var replyTo: Status? - // many-to-many relastionship + // many-to-many relationship @NSManaged public private(set) var favouritedBy: Set? @NSManaged public private(set) var rebloggedBy: Set? @NSManaged public private(set) var mutedBy: Set? @NSManaged public private(set) var bookmarkedBy: Set? - // one-to-one relastionship + // one-to-one relationship @NSManaged public private(set) var pinnedBy: MastodonUser? @NSManaged public private(set) var poll: Poll? + @NSManaged public private(set) var searchHistory: SearchHistory? // one-to-many relationship @NSManaged public private(set) var reblogFrom: Set? diff --git a/CoreDataStack/Entity/Tag.swift b/CoreDataStack/Entity/Tag.swift index 3044cacc0..6aeee520e 100644 --- a/CoreDataStack/Entity/Tag.swift +++ b/CoreDataStack/Entity/Tag.swift @@ -17,6 +17,9 @@ public final class Tag: NSManagedObject { @NSManaged public private(set) var name: String @NSManaged public private(set) var url: String + // one-to-one relationship + @NSManaged public private(set) var searchHistory: SearchHistory? + // many-to-many relationship @NSManaged public private(set) var statuses: Set? diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index a389287d7..aded53b0d 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -274,6 +274,10 @@ DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */; }; DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */; }; DB4F096C269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */; }; + DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */; }; + DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */; }; + DB4F097D26A03A5B00D62E92 /* SearchHistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */; }; + DB4F097F26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */; }; DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */; }; DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; @@ -921,6 +925,10 @@ DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryTableHeaderView.swift; sourceTree = ""; }; DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewModel+State.swift"; sourceTree = ""; }; DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewController+StatusProvider.swift"; sourceTree = ""; }; + DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryViewModel.swift; sourceTree = ""; }; + DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySection.swift; sourceTree = ""; }; + DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryItem.swift; sourceTree = ""; }; + DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryFetchedResultController.swift; sourceTree = ""; }; DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchToSearchDetailViewControllerAnimatedTransitioning.swift; sourceTree = ""; }; DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTransitionController.swift; sourceTree = ""; }; DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; @@ -1509,6 +1517,7 @@ DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */, DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */, DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */, + DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */, ); path = Service; sourceTree = ""; @@ -1570,24 +1579,13 @@ 2D76319D25C151F600929FB9 /* Section */ = { isa = PBXGroup; children = ( - 2D76319E25C1521200929FB9 /* StatusSection.swift */, - DB4481C525EE2ADA00BEFB67 /* PollSection.swift */, - DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */, - DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */, - 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */, - 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */, + DB4F097926A039C400D62E92 /* Status */, + DB4F097826A039B400D62E92 /* Onboarding */, + DB4F097726A039A200D62E92 /* Search */, + DB4F097626A0398000D62E92 /* Compose */, 2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */, - 2D35237926256D920031AF25 /* NotificationSection.swift */, - 2D198648261C0B8500F0B013 /* SearchResultSection.swift */, - DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, - DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */, - DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */, - DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */, DB6D9F7C26358ED4008423CD /* SettingsSection.swift */, - 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */, - DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */, DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */, - DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */, ); path = Section; sourceTree = ""; @@ -1641,6 +1639,7 @@ children = ( 2D7631B225C159F700929FB9 /* Item.swift */, 2D198642261BF09500F0B013 /* SearchResultItem.swift */, + DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */, 2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */, 2D7867182625B77500211898 /* NotificationItem.swift */, DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */, @@ -2010,7 +2009,6 @@ DB4F0964269ED06700D62E92 /* SearchResult */ = { isa = PBXGroup; children = ( - 2DFAD5212616F8E300F9EE7C /* TableViewCell */, DB4F0962269ED06300D62E92 /* SearchResultViewController.swift */, DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */, DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */, @@ -2019,6 +2017,57 @@ path = SearchResult; sourceTree = ""; }; + DB4F097626A0398000D62E92 /* Compose */ = { + isa = PBXGroup; + children = ( + DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, + DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */, + DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */, + DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */, + DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */, + ); + path = Compose; + sourceTree = ""; + }; + DB4F097726A039A200D62E92 /* Search */ = { + isa = PBXGroup; + children = ( + 2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */, + 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */, + 2D198648261C0B8500F0B013 /* SearchResultSection.swift */, + DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */, + ); + path = Search; + sourceTree = ""; + }; + DB4F097826A039B400D62E92 /* Onboarding */ = { + isa = PBXGroup; + children = ( + DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */, + DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; + DB4F097926A039C400D62E92 /* Status */ = { + isa = PBXGroup; + children = ( + 2D76319E25C1521200929FB9 /* StatusSection.swift */, + DB4481C525EE2ADA00BEFB67 /* PollSection.swift */, + 2D35237926256D920031AF25 /* NotificationSection.swift */, + 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */, + ); + path = Status; + sourceTree = ""; + }; + DB4F098026A0475500D62E92 /* View */ = { + isa = PBXGroup; + children = ( + DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */, + ); + path = View; + sourceTree = ""; + }; DB4FFC2D269EC39C00D62E92 /* Search */ = { isa = PBXGroup; children = ( @@ -2428,8 +2477,6 @@ children = ( DBF1D253269DB02C00C1C08A /* Search */, DBF1D24F269DAF6100C1C08A /* SearchDetail */, - DB4F0964269ED06700D62E92 /* SearchResult */, - DBF1D252269DB01700C1C08A /* SearchHistory */, ); path = Search; sourceTree = ""; @@ -2656,6 +2703,7 @@ DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */, DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */, DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */, + DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */, ); path = FetchedResultsController; sourceTree = ""; @@ -2696,6 +2744,9 @@ DBF1D24F269DAF6100C1C08A /* SearchDetail */ = { isa = PBXGroup; children = ( + 2DFAD5212616F8E300F9EE7C /* TableViewCell */, + DB4F0964269ED06700D62E92 /* SearchResult */, + DBF1D252269DB01700C1C08A /* SearchHistory */, DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */, DBF1D256269DBAC600C1C08A /* SearchDetailViewModel.swift */, ); @@ -2705,8 +2756,9 @@ DBF1D252269DB01700C1C08A /* SearchHistory */ = { isa = PBXGroup; children = ( + DB4F098026A0475500D62E92 /* View */, DBF1D250269DB01200C1C08A /* SearchHistoryViewController.swift */, - DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */, + DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */, ); path = SearchHistory; sourceTree = ""; @@ -3388,6 +3440,7 @@ 2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */, 0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */, DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */, + DB4F097F26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift in Sources */, DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */, DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */, DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */, @@ -3528,6 +3581,7 @@ DBA94438265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift in Sources */, 0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */, DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */, + DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */, DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */, DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, @@ -3555,6 +3609,7 @@ DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */, DBD376AC2692ECDB007FEC24 /* ThemePreference.swift in Sources */, DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */, + DB4F097D26A03A5B00D62E92 /* SearchHistoryItem.swift in Sources */, DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */, DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, @@ -3624,6 +3679,7 @@ DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */, DB9A489026035963008B817C /* APIService+Media.swift in Sources */, 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */, + DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */, DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */, DBCBCC0B2680B03F000F5B51 /* AsyncHomeTimelineViewModel+LoadOldestState.swift in Sources */, 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, diff --git a/Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift b/Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift new file mode 100644 index 000000000..b83bfe662 --- /dev/null +++ b/Mastodon/Diffiable/FetchedResultsController/SearchHistoryFetchedResultController.swift @@ -0,0 +1,53 @@ +// +// SearchHistoryFetchedResultController.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-15. +// + +import os.log +import UIKit +import Combine +import CoreData +import CoreDataStack +import MastodonSDK + +final class SearchHistoryFetchedResultController: NSObject { + + var disposeBag = Set() + + let fetchedResultsController: NSFetchedResultsController + + // output + let objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) + + init(managedObjectContext: NSManagedObjectContext) { + self.fetchedResultsController = { + let fetchRequest = SearchHistory.sortedFetchRequest + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.fetchBatchSize = 20 + let controller = NSFetchedResultsController( + fetchRequest: fetchRequest, + managedObjectContext: managedObjectContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + + return controller + }() + super.init() + + fetchedResultsController.delegate = self + } + +} + +// MARK: - NSFetchedResultsControllerDelegate +extension SearchHistoryFetchedResultController: NSFetchedResultsControllerDelegate { + func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + + let objects = fetchedResultsController.fetchedObjects ?? [] + self.objectIDs.value = objects.map { $0.objectID } + } +} diff --git a/Mastodon/Diffiable/Item/SearchHistoryItem.swift b/Mastodon/Diffiable/Item/SearchHistoryItem.swift new file mode 100644 index 000000000..de97eae34 --- /dev/null +++ b/Mastodon/Diffiable/Item/SearchHistoryItem.swift @@ -0,0 +1,41 @@ +// +// SearchHistoryItem.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-15. +// + +import Foundation +import CoreData + +enum SearchHistoryItem { + case account(objectID: NSManagedObjectID) + case hashtag(objectID: NSManagedObjectID) + case status(objectID: NSManagedObjectID, attribute: Item.StatusAttribute) +} + +extension SearchHistoryItem: Hashable { + static func == (lhs: SearchHistoryItem, rhs: SearchHistoryItem) -> Bool { + switch (lhs, rhs) { + case (.account(let objectIDLeft), account(let objectIDRight)): + return objectIDLeft == objectIDRight + case (.hashtag(let objectIDLeft), hashtag(let objectIDRight)): + return objectIDLeft == objectIDRight + case (.status(let objectIDLeft, _), status(let objectIDRight, _)): + return objectIDLeft == objectIDRight + default: + return false + } + } + + func hash(into hasher: inout Hasher) { + switch self { + case .account(let objectID): + hasher.combine(objectID) + case .hashtag(let objectID): + hasher.combine(objectID) + case .status(let objectID, _): + hasher.combine(objectID) + } + } +} diff --git a/Mastodon/Diffiable/Item/SearchResultItem.swift b/Mastodon/Diffiable/Item/SearchResultItem.swift index b3d70be49..7f57c4355 100644 --- a/Mastodon/Diffiable/Item/SearchResultItem.swift +++ b/Mastodon/Diffiable/Item/SearchResultItem.swift @@ -12,11 +12,7 @@ import MastodonSDK enum SearchResultItem { case hashtag(tag: Mastodon.Entity.Tag) case account(account: Mastodon.Entity.Account) - - case accountObjectID(accountObjectID: NSManagedObjectID) - case hashtagObjectID(hashtagObjectID: NSManagedObjectID) case status(statusObjectID: NSManagedObjectID, attribute: Item.StatusAttribute) - case bottomLoader(attribute: BottomLoaderAttribute) } @@ -47,10 +43,6 @@ extension SearchResultItem: Equatable { return tagLeft == tagRight case (.account(let accountLeft), .account(let accountRight)): return accountLeft == accountRight - case (.accountObjectID(let idLeft), .accountObjectID(let idRight)): - return idLeft == idRight - case (.hashtagObjectID(let idLeft), .hashtagObjectID(let idRight)): - return idLeft == idRight case (.status(let idLeft, _), .status(let idRight, _)): return idLeft == idRight case (.bottomLoader(let attributeLeft), .bottomLoader(let attributeRight)): @@ -70,10 +62,6 @@ extension SearchResultItem: Hashable { case .hashtag(let tag): hasher.combine(String(describing: SearchResultItem.hashtag.self)) hasher.combine(tag.name) - case .accountObjectID(let id): - hasher.combine(id) - case .hashtagObjectID(let id): - hasher.combine(id) case .status(let id, _): hasher.combine(id) case .bottomLoader(let attribute): @@ -99,8 +87,6 @@ extension SearchResultItem { return .status(objectID: objectID) case .hashtag, .account, - .accountObjectID, - .hashtagObjectID, .bottomLoader: return nil } diff --git a/Mastodon/Diffiable/Section/AutoCompleteSection.swift b/Mastodon/Diffiable/Section/Compose/AutoCompleteSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/AutoCompleteSection.swift rename to Mastodon/Diffiable/Section/Compose/AutoCompleteSection.swift diff --git a/Mastodon/Diffiable/Section/ComposeStatusAttachmentSection.swift b/Mastodon/Diffiable/Section/Compose/ComposeStatusAttachmentSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/ComposeStatusAttachmentSection.swift rename to Mastodon/Diffiable/Section/Compose/ComposeStatusAttachmentSection.swift diff --git a/Mastodon/Diffiable/Section/ComposeStatusPollSection.swift b/Mastodon/Diffiable/Section/Compose/ComposeStatusPollSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/ComposeStatusPollSection.swift rename to Mastodon/Diffiable/Section/Compose/ComposeStatusPollSection.swift diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/Compose/ComposeStatusSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/ComposeStatusSection.swift rename to Mastodon/Diffiable/Section/Compose/ComposeStatusSection.swift diff --git a/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift rename to Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift diff --git a/Mastodon/Diffiable/Section/CategoryPickerSection.swift b/Mastodon/Diffiable/Section/Onboarding/CategoryPickerSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/CategoryPickerSection.swift rename to Mastodon/Diffiable/Section/Onboarding/CategoryPickerSection.swift diff --git a/Mastodon/Diffiable/Section/PickServerSection.swift b/Mastodon/Diffiable/Section/Onboarding/PickServerSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/PickServerSection.swift rename to Mastodon/Diffiable/Section/Onboarding/PickServerSection.swift diff --git a/Mastodon/Diffiable/Section/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/RecommendAccountSection.swift rename to Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift diff --git a/Mastodon/Diffiable/Section/RecommendHashTagSection.swift b/Mastodon/Diffiable/Section/Search/RecommendHashTagSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/RecommendHashTagSection.swift rename to Mastodon/Diffiable/Section/Search/RecommendHashTagSection.swift diff --git a/Mastodon/Diffiable/Section/Search/SearchHistorySection.swift b/Mastodon/Diffiable/Section/Search/SearchHistorySection.swift new file mode 100644 index 000000000..8f39eb6bd --- /dev/null +++ b/Mastodon/Diffiable/Section/Search/SearchHistorySection.swift @@ -0,0 +1,56 @@ +// +// SearchHistorySection.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-15. +// + +import UIKit +import CoreDataStack + +enum SearchHistorySection: Hashable { + case main +} + +extension SearchHistorySection { + static func tableViewDiffableDataSource( + for tableView: UITableView, + dependency: NeedsDependency + ) -> UITableViewDiffableDataSource { + UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in + switch item { + case .account(let objectID): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell + if let user = try? dependency.context.managedObjectContext.existingObject(with: objectID) as? MastodonUser { + cell.config(with: user) + } + return cell + case .hashtag(let objectID): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SearchResultTableViewCell.self), for: indexPath) as! SearchResultTableViewCell + if let hashtag = try? dependency.context.managedObjectContext.existingObject(with: objectID) as? Tag { + cell.config(with: hashtag) + } + return cell + case .status: + return UITableViewCell() +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell +// if let status = try? dependency.context.managedObjectContext.existingObject(with: statusObjectID) as? Status { +// let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value +// let requestUserID = activeMastodonAuthenticationBox?.userID ?? "" +// StatusSection.configure( +// cell: cell, +// tableView: tableView, +// timelineContext: .search, +// dependency: dependency, +// readableLayoutFrame: tableView.readableContentGuide.layoutFrame, +// status: status, +// requestUserID: requestUserID, +// statusItemAttribute: attribute +// ) +// } +// cell.delegate = statusTableViewCellDelegate +// return cell + } // end switch + } // end UITableViewDiffableDataSource + } // end func +} diff --git a/Mastodon/Diffiable/Section/SearchResultSection.swift b/Mastodon/Diffiable/Section/Search/SearchResultSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/SearchResultSection.swift rename to Mastodon/Diffiable/Section/Search/SearchResultSection.swift diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/Status/NotificationSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/NotificationSection.swift rename to Mastodon/Diffiable/Section/Status/NotificationSection.swift diff --git a/Mastodon/Diffiable/Section/PollSection.swift b/Mastodon/Diffiable/Section/Status/PollSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/PollSection.swift rename to Mastodon/Diffiable/Section/Status/PollSection.swift diff --git a/Mastodon/Diffiable/Section/ReportSection.swift b/Mastodon/Diffiable/Section/Status/ReportSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/ReportSection.swift rename to Mastodon/Diffiable/Section/Status/ReportSection.swift diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/Status/StatusSection.swift similarity index 100% rename from Mastodon/Diffiable/Section/StatusSection.swift rename to Mastodon/Diffiable/Section/Status/StatusSection.swift diff --git a/Mastodon/Scene/Search/Search/SearchViewModel.swift b/Mastodon/Scene/Search/Search/SearchViewModel.swift index e3b551628..681fa0f54 100644 --- a/Mastodon/Scene/Search/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/Search/SearchViewModel.swift @@ -25,13 +25,7 @@ final class SearchViewModel: NSObject { let viewDidAppeared = PassthroughSubject() // output - let searchText = CurrentValueSubject("") - let searchScope = CurrentValueSubject(Mastodon.API.V2.Search.SearchType.default) - - let isSearching = CurrentValueSubject(false) - - let searchResult = CurrentValueSubject(nil) - + // var recommendHashTags = [Mastodon.Entity.Tag]() var recommendAccounts = [NSManagedObjectID]() var recommendAccountsFallback = PassthroughSubject() @@ -39,85 +33,11 @@ final class SearchViewModel: NSObject { var hashtagDiffableDataSource: UICollectionViewDiffableDataSource? var accountDiffableDataSource: UICollectionViewDiffableDataSource? - let statusFetchedResultsController: StatusFetchedResultsController - init(context: AppContext, coordinator: SceneCoordinator) { self.coordinator = coordinator self.context = context - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: nil, - additionalTweetPredicate: nil - ) super.init() - // bind active authentication - context.authenticationService.activeMastodonAuthentication - .sink { [weak self] activeMastodonAuthentication in - guard let self = self else { return } - guard let activeMastodonAuthentication = activeMastodonAuthentication else { - self.currentMastodonUser.value = nil - return - } - self.currentMastodonUser.value = activeMastodonAuthentication.user - self.statusFetchedResultsController.domain.value = activeMastodonAuthentication.domain - } - .store(in: &disposeBag) - - Publishers.CombineLatest( - searchText - .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates(), - searchScope - ) - .filter { text, _ in - !text.isEmpty - } - .compactMap { (text, scope) -> AnyPublisher, Error>, Never>? in - guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return nil } - let query = Mastodon.API.V2.Search.Query( - q: text, - type: scope, - accountID: nil, - maxID: nil, - minID: nil, - excludeUnreviewed: nil, - resolve: nil, - limit: nil, - offset: nil, - following: nil - ) - return context.apiService.search( - domain: activeMastodonAuthenticationBox.domain, - query: query, - mastodonAuthenticationBox: activeMastodonAuthenticationBox - ) - // .retry(3) // iOS 14.0 SDK may not works here. needs testing before add this - .map { response in Result, Error> { response } } - .catch { error in Just(Result, Error> { throw error }) } - .eraseToAnyPublisher() - } - .switchToLatest() - .sink { [weak self] result in - guard let self = self else { return } - switch result { - case .success(let response): - guard self.isSearching.value else { return } - self.searchResult.value = response.value - case .failure(let error): - break - } - } - .store(in: &disposeBag) - - isSearching - .sink { [weak self] isSearching in - if !isSearching { - self?.searchResult.value = nil - self?.searchText.value = "" - } - } - .store(in: &disposeBag) - Publishers.CombineLatest( context.authenticationService.activeMastodonAuthenticationBox, viewDidAppeared @@ -220,126 +140,5 @@ final class SearchViewModel: NSObject { snapshot.appendItems(self.recommendAccounts, toSection: .main) dataSource.apply(snapshot, animatingDifferences: false, completion: nil) } - - func searchResultItemDidSelected(item: SearchResultItem, from: UIViewController) { - let searchHistories = fetchSearchHistory() - _ = context.managedObjectContext.performChanges { [weak self] in - guard let self = self else { return } - switch item { - case .account(let account): - guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - // load request mastodon user - let requestMastodonUser: MastodonUser? = { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: activeMastodonAuthenticationBox.domain, id: activeMastodonAuthenticationBox.userID) - request.fetchLimit = 1 - request.returnsObjectsAsFaults = false - do { - return try self.context.managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil - } - }() - let (mastodonUser, _) = APIService.CoreData.createOrMergeMastodonUser(into: self.context.managedObjectContext, for: requestMastodonUser, in: activeMastodonAuthenticationBox.domain, entity: account, userCache: nil, networkDate: Date(), log: OSLog.api) - if let searchHistories = searchHistories { - let history = searchHistories.first { history -> Bool in - guard let account = history.account else { return false } - return account.objectID == mastodonUser.objectID - } - if let history = history { - history.update(updatedAt: Date()) - } else { - SearchHistory.insert(into: self.context.managedObjectContext, account: mastodonUser) - } - } else { - SearchHistory.insert(into: self.context.managedObjectContext, account: mastodonUser) - } - let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: mastodonUser) - DispatchQueue.main.async { - self.coordinator.present(scene: .profile(viewModel: viewModel), from: from, transition: .show) - } - case .hashtag(let tag): - let (tagInCoreData, _) = APIService.CoreData.createOrMergeTag(into: self.context.managedObjectContext, entity: tag) - if let searchHistories = searchHistories { - let history = searchHistories.first { history -> Bool in - guard let hashtag = history.hashtag else { return false } - return hashtag.objectID == tagInCoreData.objectID - } - if let history = history { - history.update(updatedAt: Date()) - } else { - SearchHistory.insert(into: self.context.managedObjectContext, hashtag: tagInCoreData) - } - } else { - SearchHistory.insert(into: self.context.managedObjectContext, hashtag: tagInCoreData) - } - let viewModel = HashtagTimelineViewModel(context: self.context, hashtag: tagInCoreData.name) - DispatchQueue.main.async { - self.coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: from, transition: .show) - } - case .accountObjectID(let accountObjectID): - if let searchHistories = searchHistories { - let history = searchHistories.first { history -> Bool in - guard let account = history.account else { return false } - return account.objectID == accountObjectID - } - if let history = history { - history.update(updatedAt: Date()) - } - } - let mastodonUser = self.context.managedObjectContext.object(with: accountObjectID) as! MastodonUser - let viewModel = ProfileViewModel(context: self.context, optionalMastodonUser: mastodonUser) - DispatchQueue.main.async { - self.coordinator.present(scene: .profile(viewModel: viewModel), from: from, transition: .show) - } - case .hashtagObjectID(let hashtagObjectID): - if let searchHistories = searchHistories { - let history = searchHistories.first { history -> Bool in - guard let hashtag = history.hashtag else { return false } - return hashtag.objectID == hashtagObjectID - } - if let history = history { - history.update(updatedAt: Date()) - } - } - let tagInCoreData = self.context.managedObjectContext.object(with: hashtagObjectID) as! Tag - let viewModel = HashtagTimelineViewModel(context: self.context, hashtag: tagInCoreData.name) - DispatchQueue.main.async { - self.coordinator.present(scene: .hashtagTimeline(viewModel: viewModel), from: from, transition: .show) - } - default: - break - } - } - } - - func fetchSearchHistory() -> [SearchHistory]? { - let searchHistory: [SearchHistory]? = { - let request = SearchHistory.sortedFetchRequest - request.predicate = nil - request.returnsObjectsAsFaults = false - do { - return try context.managedObjectContext.fetch(request) - } catch { - assertionFailure(error.localizedDescription) - return nil - } - - }() - return searchHistory - } - - func deleteSearchHistory() { - let result = fetchSearchHistory() - _ = context.managedObjectContext.performChanges { [weak self] in - result?.forEach { history in - self?.context.managedObjectContext.delete(history) - } - self?.isSearching.value = true - } - } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift index 2505eb42a..02880b796 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift @@ -23,6 +23,7 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency { var viewModel: SearchDetailViewModel! var viewControllers: [SearchResultViewController]! + let navigationBarVisualEffectBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) let navigationBarBackgroundView = UIView() let navigationBar: UINavigationBar = { let navigationItem = UINavigationItem() @@ -32,7 +33,9 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency { navigationItem.compactAppearance = barAppearance navigationItem.scrollEdgeAppearance = barAppearance - let navigationBar = UINavigationBar() + let navigationBar = UINavigationBar( + frame: CGRect(x: 0, y: 0, width: 300, height: 100) + ) navigationBar.setItems([navigationItem], animated: false) return navigationBar }() @@ -40,9 +43,18 @@ final class SearchDetailViewController: PageboyViewController, NeedsDependency { let searchBar = UISearchBar() searchBar.placeholder = L10n.Scene.Search.SearchBar.placeholder searchBar.scopeButtonTitles = SearchDetailViewModel.SearchScope.allCases.map { $0.segmentedControlTitle } + searchBar.sizeToFit() searchBar.scopeBarBackgroundImage = UIImage() return searchBar }() + + private(set) lazy var searchHistoryViewController: SearchHistoryViewController = { + let searchHistoryViewController = SearchHistoryViewController() + searchHistoryViewController.context = context + searchHistoryViewController.coordinator = coordinator + searchHistoryViewController.viewModel = SearchHistoryViewModel(context: context) + return searchHistoryViewController + }() } extension SearchDetailViewController { @@ -82,6 +94,26 @@ extension SearchDetailViewController { navigationBarBackgroundView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor), ]) + navigationBarVisualEffectBackgroundView.translatesAutoresizingMaskIntoConstraints = false + view.insertSubview(navigationBarVisualEffectBackgroundView, belowSubview: navigationBarBackgroundView) + NSLayoutConstraint.activate([ + navigationBarVisualEffectBackgroundView.topAnchor.constraint(equalTo: navigationBarBackgroundView.topAnchor), + navigationBarVisualEffectBackgroundView.leadingAnchor.constraint(equalTo: navigationBarBackgroundView.leadingAnchor), + navigationBarVisualEffectBackgroundView.trailingAnchor.constraint(equalTo: navigationBarBackgroundView.trailingAnchor), + navigationBarVisualEffectBackgroundView.bottomAnchor.constraint(equalTo: navigationBarBackgroundView.bottomAnchor), + ]) + + addChild(searchHistoryViewController) + searchHistoryViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(searchHistoryViewController.view) + searchHistoryViewController.didMove(toParent: self) + NSLayoutConstraint.activate([ + searchHistoryViewController.view.topAnchor.constraint(equalTo: navigationBarBackgroundView.bottomAnchor), + searchHistoryViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + searchHistoryViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + searchHistoryViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + transition = Transition(style: .fade, duration: 0.1) isScrollEnabled = false @@ -168,12 +200,25 @@ extension SearchDetailViewController { searchResultViewController.viewModel.stateMachine.enter(SearchResultViewModel.State.Loading.self) } .store(in: &disposeBag) + + // bind search history display + viewModel.searchText + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] searchText in + guard let self = self else { return } + self.searchHistoryViewController.view.isHidden = !searchText.isEmpty + } + .store(in: &disposeBag) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(true, animated: animated) + searchBar.setShowsScope(true, animated: false) + searchBar.setNeedsLayout() + searchBar.layoutIfNeeded() } override func viewWillDisappear(_ animated: Bool) { @@ -193,11 +238,7 @@ extension SearchDetailViewController { extension SearchDetailViewController { private func setupSearchBar() { - searchBar.setShowsScope(true, animated: false) - searchBar.sizeToFit() - navigationBar.topItem?.titleView = searchBar - navigationBar.sizeToFit() searchBar.delegate = self } @@ -222,7 +263,7 @@ extension SearchDetailViewController: UISearchBarDelegate { func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - navigationController?.popViewController(animated: true) + navigationController?.popViewController(animated: false) } } @@ -231,7 +272,7 @@ extension SearchDetailViewController: UISearchBarDelegate { extension SearchDetailViewController: PageboyViewControllerDataSource { func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { - return 4 + return viewControllers.count } func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? { diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift new file mode 100644 index 000000000..f60b2029d --- /dev/null +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -0,0 +1,128 @@ +// +// SearchHistoryViewController.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-13. +// + +import UIKit +import Combine +import CoreDataStack + +final class SearchHistoryViewController: UIViewController, NeedsDependency { + + var disposeBag = Set() + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var viewModel: SearchHistoryViewModel! + + let searchHistoryTableHeaderView = SearchHistoryTableHeaderView() + let tableView: UITableView = { + let tableView = UITableView() + tableView.register(SearchResultTableViewCell.self, forCellReuseIdentifier: String(describing: SearchResultTableViewCell.self)) +// tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) + tableView.separatorStyle = .none + tableView.tableFooterView = UIView() + tableView.backgroundColor = .clear + return tableView + }() + +} + +extension SearchHistoryViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) + ThemeService.shared.currentTheme + .receive(on: RunLoop.main) + .sink { [weak self] theme in + guard let self = self else { return } + self.setupBackgroundColor(theme: theme) + } + .store(in: &disposeBag) + + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + tableView.delegate = self + viewModel.setupDiffableDataSource( + tableView: tableView, + dependency: self + ) + + searchHistoryTableHeaderView.delegate = self + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + tableView.deselectRow(with: transitionCoordinator, animated: animated) + } + +} + +extension SearchHistoryViewController { + private func setupBackgroundColor(theme: Theme) { + view.backgroundColor = theme.systemGroupedBackgroundColor + } +} + +// MARK: - UITableViewDelegate +extension SearchHistoryViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + switch section { + case 0: + return searchHistoryTableHeaderView + default: + return UIView() + } + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + switch section { + case 0: + return UITableView.automaticDimension + default: + return .leastNonzeroMagnitude + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + + viewModel.persistSearchHistory(for: item) + + switch item { + case .account(let objectID): + guard let user = try? viewModel.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? MastodonUser else { return } + let profileViewModel = CachedProfileViewModel(context: context, mastodonUser: user) + coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show) + case .hashtag(let objectID): + guard let hashtag = try? viewModel.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? Tag else { return } + let hashtagViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag.name) + coordinator.present(scene: .hashtagTimeline(viewModel: hashtagViewModel), from: self, transition: .show) + case .status(let objectID, _): + guard let status = try? viewModel.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? Status else { return } + let threadViewModel = CachedThreadViewModel(context: context, status: status) + coordinator.present(scene: .thread(viewModel: threadViewModel), from: self, transition: .show) + } + } +} + +// MARK: - SearchHistoryTableHeaderViewDelegate +extension SearchHistoryViewController: SearchHistoryTableHeaderViewDelegate { + func searchHistoryTableHeaderView(_ searchHistoryTableHeaderView: SearchHistoryTableHeaderView, clearSearchHistoryButtonDidPressed button: UIButton) { + viewModel.clearSearchHistory() + } +} diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift new file mode 100644 index 000000000..19bb875c9 --- /dev/null +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift @@ -0,0 +1,132 @@ +// +// SearchHistoryViewModel.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-15. +// + +import UIKit +import Combine +import CoreDataStack +import CommonOSLog + +final class SearchHistoryViewModel { + + var disposeBag = Set() + + // input + let context: AppContext + let searchHistoryFetchedResultController: SearchHistoryFetchedResultController + + // output + var diffableDataSource: UITableViewDiffableDataSource! + + init(context: AppContext) { + self.context = context + self.searchHistoryFetchedResultController = SearchHistoryFetchedResultController(managedObjectContext: context.managedObjectContext) + + // may block main queue by large dataset + searchHistoryFetchedResultController.objectIDs + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] objectIDs in + guard let self = self else { return } + guard let diffableDataSource = self.diffableDataSource else { return } + let managedObjectContext = self.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext + + var items: [SearchHistoryItem] = [] + for objectID in objectIDs { + guard let searchHistory = try? managedObjectContext.existingObject(with: objectID) as? SearchHistory else { continue } + if let account = searchHistory.account { + items.append(.account(objectID: account.objectID)) + } else if let hashtag = searchHistory.hashtag { + items.append(.hashtag(objectID: hashtag.objectID)) + } else { + // TODO: status + } + } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(items, toSection: .main) + + diffableDataSource.apply(snapshot, animatingDifferences: false) + } + .store(in: &disposeBag) + + try? searchHistoryFetchedResultController.fetchedResultsController.performFetch() + } + +} + +extension SearchHistoryViewModel { + func setupDiffableDataSource( + tableView: UITableView, + dependency: NeedsDependency + ) { + diffableDataSource = SearchHistorySection.tableViewDiffableDataSource( + for: tableView, + dependency: dependency + ) + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + diffableDataSource.apply(snapshot, animatingDifferences: false) + } +} + +extension SearchHistoryViewModel { + func persistSearchHistory(for item: SearchHistoryItem) { + switch item { + case .account(let objectID): + let managedObjectContext = context.backgroundManagedObjectContext + managedObjectContext.performChanges { + guard let user = try? managedObjectContext.existingObject(with: objectID) as? MastodonUser else { return } + if let searchHistory = user.searchHistory { + searchHistory.update(updatedAt: Date()) + } else { + SearchHistory.insert(into: managedObjectContext, account: user) + } + } + .sink { result in + // do nothing + } + .store(in: &context.disposeBag) + + case .hashtag(let objectID): + let managedObjectContext = context.backgroundManagedObjectContext + managedObjectContext.performChanges { + guard let hashtag = try? managedObjectContext.existingObject(with: objectID) as? Tag else { return } + if let searchHistory = hashtag.searchHistory { + searchHistory.update(updatedAt: Date()) + } else { + SearchHistory.insert(into: managedObjectContext, hashtag: hashtag) + } + } + .sink { result in + // do nothing + } + .store(in: &context.disposeBag) + + case .status: + // FIXME: + break + } + } + + func clearSearchHistory() { + let managedObjectContext = context.backgroundManagedObjectContext + managedObjectContext.performChanges { + let request = SearchHistory.sortedFetchRequest + let searchHistories = managedObjectContext.safeFetch(request) + for searchHistory in searchHistories { + managedObjectContext.delete(searchHistory) + } + } + .sink { result in + // do nothing + } + .store(in: &context.disposeBag) + + } +} diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift new file mode 100644 index 000000000..6a360e78b --- /dev/null +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/View/SearchHistoryTableHeaderView.swift @@ -0,0 +1,96 @@ +// +// SearchHistoryTableHeaderView.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-14. +// + +import os.log +import UIKit +import Combine + +protocol SearchHistoryTableHeaderViewDelegate: AnyObject { + func searchHistoryTableHeaderView(_ searchHistoryTableHeaderView: SearchHistoryTableHeaderView, clearSearchHistoryButtonDidPressed button: UIButton) +} + +final class SearchHistoryTableHeaderView: UIView { + + let logger = Logger(subsystem: "SearchHistory", category: "UI") + + weak var delegate: SearchHistoryTableHeaderViewDelegate? + var disposeBag = Set() + + let recentSearchesLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) + label.textColor = Asset.Colors.Label.primary.color + label.text = L10n.Scene.Search.Searching.recentSearch + return label + }() + + let clearSearchHistoryButton: HighlightDimmableButton = { + let button = HighlightDimmableButton(type: .custom) + button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) + button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) + button.setTitle(L10n.Scene.Search.Searching.clear, for: .normal) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension SearchHistoryTableHeaderView { + private func _init() { + preservesSuperviewLayoutMargins = true + + recentSearchesLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(recentSearchesLabel) + NSLayoutConstraint.activate([ + recentSearchesLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16), + recentSearchesLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + bottomAnchor.constraint(equalTo: recentSearchesLabel.bottomAnchor, constant: 16), + ]) + + clearSearchHistoryButton.translatesAutoresizingMaskIntoConstraints = false + addSubview(clearSearchHistoryButton) + NSLayoutConstraint.activate([ + clearSearchHistoryButton.centerYAnchor.constraint(equalTo: recentSearchesLabel.centerYAnchor), + clearSearchHistoryButton.leadingAnchor.constraint(equalTo: recentSearchesLabel.trailingAnchor), + clearSearchHistoryButton.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + ]) + clearSearchHistoryButton.setContentHuggingPriority(.defaultHigh + 10, for: .horizontal) + + clearSearchHistoryButton.addTarget(self, action: #selector(SearchHistoryTableHeaderView.clearSearchHistoryButtonDidPressed(_:)), for: .touchUpInside) + + setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) + ThemeService.shared.currentTheme + .receive(on: RunLoop.main) + .sink { [weak self] theme in + guard let self = self else { return } + self.setupBackgroundColor(theme: theme) + } + .store(in: &disposeBag) + } +} + +extension SearchHistoryTableHeaderView { + @objc private func clearSearchHistoryButtonDidPressed(_ sender: UIButton) { + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + delegate?.searchHistoryTableHeaderView(self, clearSearchHistoryButtonDidPressed: sender) + } +} + +extension SearchHistoryTableHeaderView { + private func setupBackgroundColor(theme: Theme) { + backgroundColor = theme.systemGroupedBackgroundColor + } +} diff --git a/Mastodon/Scene/Search/SearchResult/SearchResultViewController+StatusProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+StatusProvider.swift similarity index 100% rename from Mastodon/Scene/Search/SearchResult/SearchResultViewController+StatusProvider.swift rename to Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+StatusProvider.swift diff --git a/Mastodon/Scene/Search/SearchResult/SearchResultViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift similarity index 98% rename from Mastodon/Scene/Search/SearchResult/SearchResultViewController.swift rename to Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift index 988c2945b..6c320af51 100644 --- a/Mastodon/Scene/Search/SearchResult/SearchResultViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController.swift @@ -195,7 +195,10 @@ extension SearchResultViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let diffableDataSource = viewModel.diffableDataSource else { return } - let item = diffableDataSource.itemIdentifier(for: indexPath) + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + + viewModel.persistSearchHistory(for: item) + switch item { case .account(let account): let profileViewModel = RemoteProfileViewModel(context: context, userID: account.id) @@ -207,8 +210,6 @@ extension SearchResultViewController: UITableViewDelegate { aspectTableView(tableView, didSelectRowAt: indexPath) case .bottomLoader: break - default: - assertionFailure() } } diff --git a/Mastodon/Scene/Search/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift similarity index 100% rename from Mastodon/Scene/Search/SearchResult/SearchResultViewModel+State.swift rename to Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift diff --git a/Mastodon/Scene/Search/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift similarity index 72% rename from Mastodon/Scene/Search/SearchResult/SearchResultViewModel.swift rename to Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift index 1b5cb504a..0ace96226 100644 --- a/Mastodon/Scene/Search/SearchResult/SearchResultViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift @@ -10,6 +10,7 @@ import Combine import CoreData import CoreDataStack import GameplayKit +import CommonOSLog final class SearchResultViewModel { @@ -137,3 +138,59 @@ extension SearchResultViewModel { diffableDataSource.apply(snapshot, animatingDifferences: false) } } + +extension SearchResultViewModel { + func persistSearchHistory(for item: SearchResultItem) { + guard let box = context.authenticationService.activeMastodonAuthenticationBox.value else { return } + let domain = box.domain + + switch item { + case .account(let account): + let managedObjectContext = context.backgroundManagedObjectContext + managedObjectContext.performChanges { + let (user, _) = APIService.CoreData.createOrMergeMastodonUser( + into: managedObjectContext, + for: nil, + in: domain, + entity: account, + userCache: nil, + networkDate: Date(), + log: OSLog.api + ) + if let searchHistory = user.searchHistory { + searchHistory.update(updatedAt: Date()) + } else { + SearchHistory.insert(into: managedObjectContext, account: user) + } + } + .sink { result in + // do nothing + } + .store(in: &context.disposeBag) + + case .hashtag(let hashtag): + let managedObjectContext = context.backgroundManagedObjectContext + managedObjectContext.performChanges { + let (hashtag, _) = APIService.CoreData.createOrMergeTag( + into: managedObjectContext, + entity: hashtag + ) + if let searchHistory = hashtag.searchHistory { + searchHistory.update(updatedAt: Date()) + } else { + SearchHistory.insert(into: managedObjectContext, hashtag: hashtag) + } + } + .sink { result in + // do nothing + } + .store(in: &context.disposeBag) + + case .status: + // FIXME: + break + case .bottomLoader: + break + } + } +} diff --git a/Mastodon/Scene/Search/SearchResult/TableViewCell/SearchResultTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift similarity index 98% rename from Mastodon/Scene/Search/SearchResult/TableViewCell/SearchResultTableViewCell.swift rename to Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift index ca563845a..8223c6ddf 100644 --- a/Mastodon/Scene/Search/SearchResult/TableViewCell/SearchResultTableViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift @@ -68,14 +68,14 @@ extension SearchResultTableViewCell { containerStackView.axis = .horizontal containerStackView.distribution = .fill containerStackView.spacing = 12 - containerStackView.layoutMargins = UIEdgeInsets(top: 12, left: 21, bottom: 12, right: 12) + containerStackView.layoutMargins = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0) containerStackView.isLayoutMarginsRelativeArrangement = true containerStackView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(containerStackView) NSLayoutConstraint.activate([ containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) ]) diff --git a/Mastodon/Scene/Search/SearchHistory/SearchHistoryTableHeaderView.swift b/Mastodon/Scene/Search/SearchHistory/SearchHistoryTableHeaderView.swift deleted file mode 100644 index 64a1b0c5d..000000000 --- a/Mastodon/Scene/Search/SearchHistory/SearchHistoryTableHeaderView.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// SearchHistoryTableHeaderView.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-7-14. -// - -import Foundation - -// lazy var searchHeader: UIView = { -// let view = UIView() -// view.frame = CGRect(origin: .zero, size: CGSize(width: searchingTableView.frame.width, height: 56)) -// return view -// }() -// -// let recentSearchesLabel: UILabel = { -// let label = UILabel() -// label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) -// label.textColor = Asset.Colors.Label.primary.color -// label.text = L10n.Scene.Search.Searching.recentSearch -// return label -// }() -// -// let clearSearchHistoryButton: HighlightDimmableButton = { -// let button = HighlightDimmableButton(type: .custom) -// button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) -// button.setTitle(L10n.Scene.Search.Searching.clear, for: .normal) -// return button -// }() diff --git a/Mastodon/Scene/Search/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchHistory/SearchHistoryViewController.swift deleted file mode 100644 index 2472b454d..000000000 --- a/Mastodon/Scene/Search/SearchHistory/SearchHistoryViewController.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// SearchHistoryViewController.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-7-13. -// - -import UIKit - -final class SearchHistoryViewController: UIViewController, NeedsDependency { - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } -} - -extension SearchHistoryViewController { - -} diff --git a/Mastodon/Scene/Transition/Search/SearchToSearchDetailViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/Search/SearchToSearchDetailViewControllerAnimatedTransitioning.swift index c47eec472..f06d04d9a 100644 --- a/Mastodon/Scene/Transition/Search/SearchToSearchDetailViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/Search/SearchToSearchDetailViewControllerAnimatedTransitioning.swift @@ -15,7 +15,7 @@ final class SearchToSearchDetailViewControllerAnimatedTransitioning: ViewControl override init(operation: UINavigationController.Operation) { super.init(operation: operation) - self.transitionDuration = 0.01 + self.transitionDuration = 0.2 } deinit { @@ -37,7 +37,7 @@ extension SearchToSearchDetailViewControllerAnimatedTransitioning { } } - private func pushTransition(using transitionContext: UIViewControllerContextTransitioning, curve: UIView.AnimationCurve = .easeInOut) -> UIViewPropertyAnimator { + private func pushTransition(using transitionContext: UIViewControllerContextTransitioning, curve: UIView.AnimationCurve = .easeOut) -> UIViewPropertyAnimator { guard let toVC = transitionContext.viewController(forKey: .to) as? SearchDetailViewController, let toView = transitionContext.view(forKey: .to) else { fatalError() @@ -46,12 +46,14 @@ extension SearchToSearchDetailViewControllerAnimatedTransitioning { let toViewEndFrame = transitionContext.finalFrame(for: toVC) transitionContext.containerView.addSubview(toView) toView.frame = toViewEndFrame + toView.alpha = 0 let animator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), curve: curve) animator.addAnimations { } animator.addCompletion { position in + toView.alpha = 1 transitionContext.completeTransition(true) } return animator diff --git a/Mastodon/Scene/Transition/Search/SearchTransitionController.swift b/Mastodon/Scene/Transition/Search/SearchTransitionController.swift index ce97742f7..871fd7baf 100644 --- a/Mastodon/Scene/Transition/Search/SearchTransitionController.swift +++ b/Mastodon/Scene/Transition/Search/SearchTransitionController.swift @@ -26,4 +26,13 @@ extension SearchTransitionController: UINavigationControllerDelegate { return nil } } + + func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { + switch viewController { + case is SearchDetailViewController: + navigationController.interactivePopGestureRecognizer?.isEnabled = false + default: + navigationController.interactivePopGestureRecognizer?.isEnabled = true + } + } } diff --git a/Mastodon/Diffiable/Section/StatusFilterService.swift b/Mastodon/Service/StatusFilterService.swift similarity index 100% rename from Mastodon/Diffiable/Section/StatusFilterService.swift rename to Mastodon/Service/StatusFilterService.swift