feat: add search history back
This commit is contained in:
parent
acc24b7ef5
commit
647f87744b
|
@ -136,6 +136,7 @@
|
|||
<relationship name="privateNotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="to" inverseEntity="PrivateNote"/>
|
||||
<relationship name="privateNotesTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="from" inverseEntity="PrivateNote"/>
|
||||
<relationship name="reblogged" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="rebloggedBy" inverseEntity="Status"/>
|
||||
<relationship name="searchHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="account" inverseEntity="SearchHistory"/>
|
||||
<relationship name="statuses" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="author" inverseEntity="Status"/>
|
||||
<relationship name="votePollOptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
|
||||
<relationship name="votePolls" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
|
||||
|
@ -182,8 +183,9 @@
|
|||
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
|
||||
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="searchHistory" inverseEntity="MastodonUser"/>
|
||||
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag" inverseName="searchHistory" inverseEntity="Tag"/>
|
||||
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="searchHistory" inverseEntity="Status"/>
|
||||
</entity>
|
||||
<entity name="Setting" representedClassName=".Setting" syncable="YES">
|
||||
<attribute name="appearanceRaw" attributeType="String"/>
|
||||
|
@ -234,6 +236,7 @@
|
|||
<relationship name="rebloggedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
|
||||
<relationship name="replyFrom" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="replyTo" inverseEntity="Status"/>
|
||||
<relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/>
|
||||
<relationship name="searchHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="status" inverseEntity="SearchHistory"/>
|
||||
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="statuses" inverseEntity="Tag"/>
|
||||
</entity>
|
||||
<entity name="Subscription" representedClassName=".Subscription" syncable="YES">
|
||||
|
@ -266,6 +269,7 @@
|
|||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="url" attributeType="String"/>
|
||||
<relationship name="histories" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="History" inverseName="tag" inverseEntity="History"/>
|
||||
<relationship name="searchHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="hashtag" inverseEntity="SearchHistory"/>
|
||||
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="tags" inverseEntity="Status"/>
|
||||
</entity>
|
||||
<elements>
|
||||
|
@ -277,16 +281,16 @@
|
|||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="704"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="719"/>
|
||||
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
|
||||
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="PrivateNote" positionX="0" positionY="0" width="128" height="89"/>
|
||||
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="104"/>
|
||||
<element name="SearchHistory" positionX="0" positionY="0" width="128" height="119"/>
|
||||
<element name="Setting" positionX="72" positionY="162" width="128" height="164"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="599"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="614"/>
|
||||
<element name="Subscription" positionX="81" positionY="171" width="128" height="179"/>
|
||||
<element name="SubscriptionAlerts" positionX="72" positionY="162" width="128" height="14"/>
|
||||
<element name="Tag" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="Tag" positionX="0" positionY="0" width="128" height="149"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -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<Status>?
|
||||
|
|
|
@ -14,8 +14,10 @@ public final class SearchHistory: NSManagedObject {
|
|||
@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 {
|
||||
|
|
|
@ -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<MastodonUser>?
|
||||
@NSManaged public private(set) var rebloggedBy: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var mutedBy: Set<MastodonUser>?
|
||||
@NSManaged public private(set) var bookmarkedBy: Set<MastodonUser>?
|
||||
|
||||
// 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<Status>?
|
||||
|
|
|
@ -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<Status>?
|
||||
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
DB4F0969269EDAD200D62E92 /* SearchResultViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewModel+State.swift"; sourceTree = "<group>"; };
|
||||
DB4F096B269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchResultViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
||||
DB4F097426A037F500D62E92 /* SearchHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryViewModel.swift; sourceTree = "<group>"; };
|
||||
DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySection.swift; sourceTree = "<group>"; };
|
||||
DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryItem.swift; sourceTree = "<group>"; };
|
||||
DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryFetchedResultController.swift; sourceTree = "<group>"; };
|
||||
DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchToSearchDetailViewControllerAnimatedTransitioning.swift; sourceTree = "<group>"; };
|
||||
DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTransitionController.swift; sourceTree = "<group>"; };
|
||||
DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
|
@ -1509,6 +1517,7 @@
|
|||
DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */,
|
||||
DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */,
|
||||
DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */,
|
||||
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -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 = "<group>";
|
||||
};
|
||||
DB4F097626A0398000D62E92 /* Compose */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||
DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */,
|
||||
DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */,
|
||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
|
||||
);
|
||||
path = Compose;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB4F097726A039A200D62E92 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2DE0FAC02615F04D00CDF649 /* RecommendHashTagSection.swift */,
|
||||
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */,
|
||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */,
|
||||
DB4F097A26A039FF00D62E92 /* SearchHistorySection.swift */,
|
||||
);
|
||||
path = Search;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB4F097826A039B400D62E92 /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
|
||||
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
|
||||
);
|
||||
path = Onboarding;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB4F097926A039C400D62E92 /* Status */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2D76319E25C1521200929FB9 /* StatusSection.swift */,
|
||||
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
|
||||
2D35237926256D920031AF25 /* NotificationSection.swift */,
|
||||
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
||||
);
|
||||
path = Status;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB4F098026A0475500D62E92 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB4FFC2D269EC39C00D62E92 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2428,8 +2477,6 @@
|
|||
children = (
|
||||
DBF1D253269DB02C00C1C08A /* Search */,
|
||||
DBF1D24F269DAF6100C1C08A /* SearchDetail */,
|
||||
DB4F0964269ED06700D62E92 /* SearchResult */,
|
||||
DBF1D252269DB01700C1C08A /* SearchHistory */,
|
||||
);
|
||||
path = Search;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2656,6 +2703,7 @@
|
|||
DBCBED1C26132E1A00B49291 /* StatusFetchedResultsController.swift */,
|
||||
DBA088DE26958164003EB4B2 /* UserFetchedResultsController.swift */,
|
||||
DB6D9F75263587C7008423CD /* SettingFetchedResultController.swift */,
|
||||
DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */,
|
||||
);
|
||||
path = FetchedResultsController;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
let fetchedResultsController: NSFetchedResultsController<SearchHistory>
|
||||
|
||||
// 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<NSFetchRequestResult>, 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 }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<SearchHistorySection, SearchHistoryItem> {
|
||||
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
|
||||
}
|
|
@ -25,12 +25,6 @@ final class SearchViewModel: NSObject {
|
|||
let viewDidAppeared = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
let searchText = CurrentValueSubject<String, Never>("")
|
||||
let searchScope = CurrentValueSubject<Mastodon.API.V2.Search.SearchType, Never>(Mastodon.API.V2.Search.SearchType.default)
|
||||
|
||||
let isSearching = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
let searchResult = CurrentValueSubject<Mastodon.Entity.SearchResult?, Never>(nil)
|
||||
|
||||
// var recommendHashTags = [Mastodon.Entity.Tag]()
|
||||
var recommendAccounts = [NSManagedObjectID]()
|
||||
|
@ -39,85 +33,11 @@ final class SearchViewModel: NSObject {
|
|||
var hashtagDiffableDataSource: UICollectionViewDiffableDataSource<RecommendHashTagSection, Mastodon.Entity.Tag>?
|
||||
var accountDiffableDataSource: UICollectionViewDiffableDataSource<RecommendAccountSection, NSManagedObjectID>?
|
||||
|
||||
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<Result<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, 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<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, Error> { response } }
|
||||
.catch { error in Just(Result<Mastodon.Response.Content<Mastodon.Entity.SearchResult>, 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
|
||||
|
@ -221,125 +141,4 @@ final class SearchViewModel: NSObject {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let searchHistoryFetchedResultController: SearchHistoryFetchedResultController
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<SearchHistorySection, SearchHistoryItem>!
|
||||
|
||||
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<SearchHistorySection, SearchHistoryItem>()
|
||||
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<SearchHistorySection, SearchHistoryItem>()
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
])
|
||||
|
|
@ -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
|
||||
// }()
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue