diff --git a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved index c564eb2e5be0..bd99391cc337 100644 --- a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,87 @@ { "object": { "pins": [ + { + "package": "AutomatticAbout", + "repositoryURL": "https://github.com/automattic/AutomatticAbout-swift", + "state": { + "branch": null, + "revision": "0f784591b324e5d3ddc5771808ef8eca923e3de2", + "version": "1.1.2" + } + }, + { + "package": "Charts", + "repositoryURL": "https://github.com/danielgindi/Charts", + "state": { + "branch": null, + "revision": "07b23476ad52b926be772f317d8f1d4511ee8d02", + "version": "4.1.0" + } + }, + { + "package": "CwlCatchException", + "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", + "state": { + "branch": null, + "revision": "35f9e770f54ce62dd8526470f14c6e137cef3eea", + "version": "2.1.1" + } + }, + { + "package": "CwlPreconditionTesting", + "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", + "state": { + "branch": null, + "revision": "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", + "version": "2.1.0" + } + }, + { + "package": "Lottie", + "repositoryURL": "https://github.com/airbnb/lottie-ios.git", + "state": { + "branch": null, + "revision": "4ca8023b820b7d5d5ae1e2637c046e3dab0f45d0", + "version": "3.4.2" + } + }, + { + "package": "Nimble", + "repositoryURL": "https://github.com/Quick/Nimble", + "state": { + "branch": null, + "revision": "1f3bde57bde12f5e7b07909848c071e9b73d6edc", + "version": "10.0.0" + } + }, + { + "package": "ScreenObject", + "repositoryURL": "https://github.com/Automattic/ScreenObject", + "state": { + "branch": null, + "revision": "328db56c62aab91440ec5e07cc9f7eef6e26a26e", + "version": "0.2.3" + } + }, + { + "package": "swift-algorithms", + "repositoryURL": "https://github.com/apple/swift-algorithms", + "state": { + "branch": null, + "revision": "b14b7f4c528c942f121c8b860b9410b2bf57825e", + "version": "1.0.0" + } + }, + { + "package": "swift-numerics", + "repositoryURL": "https://github.com/apple/swift-numerics", + "state": { + "branch": null, + "revision": "0a5bc04095a675662cf24757cc0640aa2204253b", + "version": "1.0.2" + } + }, { "package": "BuildkiteTestCollector", "repositoryURL": "https://github.com/buildkite/test-collector-swift", @@ -9,6 +90,15 @@ "revision": "77c7f492f5c1c9ca159f73d18f56bbd1186390b0", "version": "0.3.0" } + }, + { + "package": "XCUITestHelpers", + "repositoryURL": "https://github.com/Automattic/XCUITestHelpers", + "state": { + "branch": null, + "revision": "5179cb69d58b90761cc713bdee7740c4889d3295", + "version": "0.4.0" + } } ] }, diff --git a/WordPress/Classes/ViewRelated/Post/PostListCell.swift b/WordPress/Classes/ViewRelated/Post/PostListCell.swift index a6abb6f545ac..03ac2d5e9011 100644 --- a/WordPress/Classes/ViewRelated/Post/PostListCell.swift +++ b/WordPress/Classes/ViewRelated/Post/PostListCell.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import Combine final class PostListCell: UITableViewCell, Reusable { @@ -18,9 +19,10 @@ final class PostListCell: UITableViewCell, Reusable { }() private let headerView = PostListHeaderView() - private let titleAndSnippetLabel = UILabel() + private let contentLabel = UILabel() private let featuredImageView = CachedAnimatedImageView() private let statusLabel = UILabel() + private var cancellables: [AnyCancellable] = [] // MARK: - Properties @@ -39,10 +41,18 @@ final class PostListCell: UITableViewCell, Reusable { // MARK: - Public + override func prepareForReuse() { + super.prepareForReuse() + + cancellables = [] + } + func configure(with viewModel: PostListItemViewModel) { headerView.configure(with: viewModel) - configureTitleAndSnippet(with: viewModel) + viewModel.$content.sink { [contentLabel] in + contentLabel.attributedText = $0 + }.store(in: &cancellables) imageLoader.prepareForReuse() featuredImageView.isHidden = viewModel.imageURL == nil @@ -58,40 +68,16 @@ final class PostListCell: UITableViewCell, Reusable { statusLabel.isHidden = viewModel.status.isEmpty } - private func configureTitleAndSnippet(with viewModel: PostListItemViewModel) { - var titleAndSnippetString = NSMutableAttributedString() - - if let title = viewModel.title, !title.isEmpty { - let attributes: [NSAttributedString.Key: Any] = [ - .font: WPStyleGuide.fontForTextStyle(.callout, fontWeight: .semibold), - .foregroundColor: UIColor.text - ] - let titleAttributedString = NSAttributedString(string: "\(title)\n", attributes: attributes) - titleAndSnippetString.append(titleAttributedString) - } - - if let snippet = viewModel.snippet, !snippet.isEmpty { - let attributes: [NSAttributedString.Key: Any] = [ - .font: WPStyleGuide.fontForTextStyle(.footnote, fontWeight: .regular), - .foregroundColor: UIColor.textSubtle - ] - let snippetAttributedString = NSAttributedString(string: snippet, attributes: attributes) - titleAndSnippetString.append(snippetAttributedString) - } - - titleAndSnippetLabel.attributedText = titleAndSnippetString - } - // MARK: - Setup private func setupViews() { - setupTitleAndSnippetLabel() + setupcontentLabel() setupFeaturedImageView() setupStatusLabel() contentStackView.translatesAutoresizingMaskIntoConstraints = false contentStackView.addArrangedSubviews([ - titleAndSnippetLabel, + contentLabel, featuredImageView ]) contentStackView.spacing = 16 @@ -112,10 +98,10 @@ final class PostListCell: UITableViewCell, Reusable { contentView.backgroundColor = .systemBackground } - private func setupTitleAndSnippetLabel() { - titleAndSnippetLabel.translatesAutoresizingMaskIntoConstraints = false - titleAndSnippetLabel.adjustsFontForContentSizeCategory = true - titleAndSnippetLabel.numberOfLines = 3 + private func setupcontentLabel() { + contentLabel.translatesAutoresizingMaskIntoConstraints = false + contentLabel.adjustsFontForContentSizeCategory = true + contentLabel.numberOfLines = 3 } private func setupFeaturedImageView() { diff --git a/WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift index 37857a23cd5b..ac1b70bca99e 100644 --- a/WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift @@ -1,29 +1,56 @@ import Foundation -struct PostListItemViewModel { +final class PostListItemViewModel { let post: Post - let title: String? - let snippet: String? + @Published var content: NSAttributedString let imageURL: URL? let date: String? let accessibilityIdentifier: String? - private var statusViewModel: PostCardStatusViewModel { .init(post: post) } - var status: String { statusViewModel.statusAndBadges(separatedBy: " · ")} var statusColor: UIColor { statusViewModel.statusColor } var author: String { statusViewModel.author } + private let statusViewModel: PostCardStatusViewModel + init(post: Post) { self.post = post - self.title = post.titleForDisplay() - self.snippet = post.contentPreviewForDisplay() + self.content = makeContentAttributedString(for: post) self.imageURL = post.featuredImageURL self.date = post.displayDate()?.capitalizeFirstWord + self.statusViewModel = PostCardStatusViewModel(post: post) self.accessibilityIdentifier = post.slugForDisplay() } } +private func makeContentAttributedString(for post: Post) -> NSAttributedString { + let title = post.titleForDisplay() + let snippet = post.contentPreviewForDisplay() + + let string = NSMutableAttributedString() + if !title.isEmpty { + let attributes: [NSAttributedString.Key: Any] = [ + .font: WPStyleGuide.fontForTextStyle(.callout, fontWeight: .semibold), + .foregroundColor: UIColor.text + ] + let titleAttributedString = NSAttributedString(string: title, attributes: attributes) + string.append(titleAttributedString) + } + if !snippet.isEmpty { + if string.length > 0 { + string.append(NSAttributedString(string: "\n")) + } + let attributes: [NSAttributedString.Key: Any] = [ + .font: WPStyleGuide.fontForTextStyle(.footnote, fontWeight: .regular), + .foregroundColor: UIColor.textSubtle + ] + let snippetAttributedString = NSAttributedString(string: snippet, attributes: attributes) + string.append(snippetAttributedString) + } + + return string +} + private extension String { var capitalizeFirstWord: String { let firstLetter = self.prefix(1).capitalized diff --git a/WordPress/Classes/ViewRelated/Post/Search/PostSearchService.swift b/WordPress/Classes/ViewRelated/Post/Search/PostSearchService.swift index 666259ee54ec..037f5dab8dd6 100644 --- a/WordPress/Classes/ViewRelated/Post/Search/PostSearchService.swift +++ b/WordPress/Classes/ViewRelated/Post/Search/PostSearchService.swift @@ -2,7 +2,7 @@ import Foundation import CoreData protocol PostSearchServiceDelegate: AnyObject { - func service(_ service: PostSearchService, didAppendPosts page: [PostSearchResult]) + func service(_ service: PostSearchService, didAppendPosts page: [AbstractPost]) func serviceDidUpdateState(_ service: PostSearchService) } @@ -13,9 +13,9 @@ final class PostSearchService { weak var delegate: PostSearchServiceDelegate? + let criteria: PostSearchCriteria private let blog: Blog private let settings: PostListFilterSettings - private let criteria: PostSearchCriteria private let coreDataStack: CoreDataStack private var postIDs: Set = [] @@ -77,49 +77,13 @@ final class PostSearchService { let newPosts = posts.filter { !postIDs.contains($0.objectID) } postIDs.formUnion(newPosts.map(\.objectID)) - - preprocess(newPosts) { [weak self] in - guard let self else { return } - self.delegate?.service(self, didAppendPosts: $0) - } + self.delegate?.service(self, didAppendPosts: newPosts) case .failure(let error): self.error = error } isLoading = false delegate?.serviceDidUpdateState(self) } - - private func preprocess(_ posts: [AbstractPost], _ completion: @escaping ([PostSearchResult]) -> Void) { - let rawTitles = posts.map(\.postTitle) - let searchTerm = criteria.searchTerm - DispatchQueue.global().async { - let terms = searchTerm - .components(separatedBy: .whitespaces) - .filter { !$0.isEmpty } - let titles = rawTitles.map { PostSearchService.makeTitle(for: $0 ?? "", terms: terms) } - let results = zip(posts, titles).map { - PostSearchResult(post: $0, title: $1, searchTerm: searchTerm) - } - DispatchQueue.main.async { - completion(results) - } - } - } -} - -struct PostSearchResult { - let post: AbstractPost - /// Preprocessed titles with highlighted search ranges. - let title: NSAttributedString - let searchTerm: String - - var id: ID { ID(objectID: post.objectID, searchTerm: searchTerm) } - - struct ID: Hashable { - let objectID: NSManagedObjectID - /// Adding search term because the cell updates as the term changes. - let searchTerm: String - } } struct PostSearchCriteria: Hashable { @@ -127,58 +91,3 @@ struct PostSearchCriteria: Hashable { let authorID: NSNumber? let tag: String? } - -extension PostSearchService { - // Both decoding & searching are expensive, so the service performs these - // operations in the background. - static func makeTitle(for title: String, terms: [String]) -> NSAttributedString { - let title = title - .trimmingCharacters(in: .whitespaces) - .stringByDecodingXMLCharacters() - - let ranges = terms.flatMap { - title.ranges(of: $0, options: [.caseInsensitive, .diacriticInsensitive]) - }.sorted { $0.lowerBound < $1.lowerBound } - - let string = NSMutableAttributedString(string: title, attributes: [ - .font: WPStyleGuide.fontForTextStyle(.body) - ]) - for range in collapseAdjacentRanges(ranges, in: title) { - string.setAttributes([ - .backgroundColor: UIColor.systemYellow.withAlphaComponent(0.25) - ], range: NSRange(range, in: title)) - } - return string - } - - private static func collapseAdjacentRanges(_ ranges: [Range], in string: String) -> [Range] { - var output: [Range] = [] - var ranges = ranges - while let rhs = ranges.popLast() { - if let lhs = ranges.last, - rhs.lowerBound > string.startIndex, - lhs.upperBound == string.index(before: rhs.lowerBound), - string[string.index(before: rhs.lowerBound)].isWhitespace { - let range = lhs.lowerBound.. [Range] { - var ranges: [Range] = [] - var startIndex = self.startIndex - while startIndex < endIndex, - let range = range(of: string, options: options, range: startIndex.. NSAttributedString { + let title = title + .trimmingCharacters(in: .whitespaces) + .stringByDecodingXMLCharacters() + + let ranges = terms.flatMap { + title.ranges(of: $0, options: [.caseInsensitive, .diacriticInsensitive]) + }.sorted { $0.lowerBound < $1.lowerBound } + + let string = NSMutableAttributedString(string: title, attributes: [ + .font: WPStyleGuide.fontForTextStyle(.body) + ]) + for range in collapseAdjacentRanges(ranges, in: title) { + string.setAttributes([ + .backgroundColor: UIColor.systemYellow.withAlphaComponent(0.25) + ], range: NSRange(range, in: title)) + } + return string + } + + private static func collapseAdjacentRanges(_ ranges: [Range], in string: String) -> [Range] { + var output: [Range] = [] + var ranges = ranges + while let rhs = ranges.popLast() { + if let lhs = ranges.last, + rhs.lowerBound > string.startIndex, + lhs.upperBound == string.index(before: rhs.lowerBound), + string[string.index(before: rhs.lowerBound)].isWhitespace { + ranges.removeLast() + ranges.append(lhs.lowerBound.. [Range] { + var ranges: [Range] = [] + var startIndex = self.startIndex + while startIndex < endIndex, + let range = range(of: string, options: options, range: startIndex..? private let suggestionsService: PostSearchSuggestionsService @@ -48,7 +49,7 @@ final class PostSearchViewModel: NSObject, PostSearchServiceDelegate { $searchTerm .dropFirst() .removeDuplicates() - .sink { [weak self] in self?.updateSuggestedTokens(for: $0) } + .sink { [weak self] in self?.didUpdateSearchTerm($0) } .store(in: &cancellables) $searchTerm.map { $0.trimmingCharacters(in: .whitespaces) } @@ -62,6 +63,11 @@ final class PostSearchViewModel: NSObject, PostSearchServiceDelegate { // MARK: - Events + private func didUpdateSearchTerm(_ searchTerm: String) { + updateHighlightForSearchResults(for: searchTerm) + updateSuggestedTokens(for: searchTerm) + } + func didReachBottom() { guard let searchService, searchService.error == nil else { return } searchService.loadMore() @@ -75,7 +81,7 @@ final class PostSearchViewModel: NSObject, PostSearchServiceDelegate { let token = suggestedTokens[index] cancelCurrentRemoteSearch() suggestedTokens = [] - posts = [] + results = [] selectedTokens.append(token) searchTerm = "" } @@ -86,8 +92,8 @@ final class PostSearchViewModel: NSObject, PostSearchServiceDelegate { cancelCurrentRemoteSearch() guard searchTerm.count > 1 || !selectedTokens.isEmpty else { - if !posts.isEmpty { - posts = [] + if !results.isEmpty { + results = [] } return } @@ -122,24 +128,28 @@ final class PostSearchViewModel: NSObject, PostSearchServiceDelegate { // MARK: - PostSearchServiceDelegate - func service(_ service: PostSearchService, didAppendPosts posts: [PostSearchResult]) { + func service(_ service: PostSearchService, didAppendPosts posts: [AbstractPost]) { assert(Thread.isMainThread) + let items = posts.map(getSearchResultItem) if isRefreshing { - self.posts = posts + self.results = items isRefreshing = false } else { - self.posts += posts + self.results += items } + + // Updating for current searchTerm, not the one that the service searched for + updateHighlightForSearchResults(for: searchTerm) } func serviceDidUpdateState(_ service: PostSearchService) { assert(Thread.isMainThread) if isRefreshing && service.error != nil { - posts = [] + results = [] } - if service.isLoading && (!isRefreshing || posts.isEmpty) { + if service.isLoading && (!isRefreshing || results.isEmpty) { footerState = .loading } else if service.error != nil { footerState = .error @@ -148,6 +158,47 @@ final class PostSearchViewModel: NSObject, PostSearchServiceDelegate { } } + // MARK: - Results + + private func getSearchResultItem(for item: AbstractPost) -> PostSearchResultItem { + switch item { + case let post as Post: + return .post(getViewModel(for: post)) + case let page as Page: + return .page(page) + default: + fatalError("Unsupported item: \(type(of: item))") + } + } + + private func getViewModel(for post: Post) -> PostListItemViewModel { + if let viewModel = postViewModels[post.objectID] { + return viewModel + } + let viewModel = PostListItemViewModel(post: post) + postViewModels[post.objectID] = viewModel + return viewModel + } + + // MARK: - Highlighter + + private func updateHighlightForSearchResults(for searchTerm: String) { + let terms = searchTerm + .trimmingCharacters(in: .whitespaces) + .components(separatedBy: .whitespaces) + .filter { !$0.isEmpty } + for item in results { + switch item { + case .post(let viewModel): + let string = NSMutableAttributedString(attributedString: viewModel.content) + PostSearchViewModel.highlight(terms: terms, in: string) + viewModel.content = string + case .page: + break // TODO: Implement highlighting + } + } + } + // MARK: - Search Tokens private func updateSuggestedTokens(for searchTerm: String) { @@ -160,3 +211,17 @@ final class PostSearchViewModel: NSObject, PostSearchServiceDelegate { } } } + +enum PostSearchResultItem { + case post(PostListItemViewModel) + case page(Page) + + var objectID: NSManagedObjectID { + switch self { + case .post(let viewModel): + return viewModel.post.objectID + case .page(let page): + return page.objectID + } + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index e2b9b34f3f23..248080cc2312 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -397,6 +397,8 @@ 0C0AE75A2A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */; }; 0C0D3B0D2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; 0C0D3B0E2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; + 0C1531FE2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1531FD2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift */; }; + 0C1531FF2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1531FD2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift */; }; 0C23F3362AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */; }; 0C23F3372AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */; }; 0C23F33E2AC4AEF600EE6117 /* SiteMediaPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C23F33D2AC4AEF600EE6117 /* SiteMediaPickerViewController.swift */; }; @@ -479,13 +481,13 @@ 0CB4057A29C8DDEE008EED0A /* BlogDashboardPersonalizationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4057029C8DCF4008EED0A /* BlogDashboardPersonalizationViewModel.swift */; }; 0CB4057D29C8DF83008EED0A /* BlogDashboardPersonalizeCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4057B29C8DEE1008EED0A /* BlogDashboardPersonalizeCardCell.swift */; }; 0CB4057E29C8DF84008EED0A /* BlogDashboardPersonalizeCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB4057B29C8DEE1008EED0A /* BlogDashboardPersonalizeCardCell.swift */; }; - 0CB424F62AE0416D0080B807 /* SolidColorActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F52AE0416D0080B807 /* SolidColorActivityIndicator.swift */; }; - 0CB424F72AE0416D0080B807 /* SolidColorActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F52AE0416D0080B807 /* SolidColorActivityIndicator.swift */; }; 0CB424EE2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424ED2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift */; }; 0CB424EF2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424ED2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift */; }; 0CB424F12ADEE52A0080B807 /* PostSearchToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F02ADEE52A0080B807 /* PostSearchToken.swift */; }; 0CB424F22ADEE52A0080B807 /* PostSearchToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F02ADEE52A0080B807 /* PostSearchToken.swift */; }; - 0CB424F42ADF3CBE0080B807 /* PostSearchServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F32ADF3CBE0080B807 /* PostSearchServiceTests.swift */; }; + 0CB424F42ADF3CBE0080B807 /* PostSearchViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F32ADF3CBE0080B807 /* PostSearchViewModelTests.swift */; }; + 0CB424F62AE0416D0080B807 /* SolidColorActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F52AE0416D0080B807 /* SolidColorActivityIndicator.swift */; }; + 0CB424F72AE0416D0080B807 /* SolidColorActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB424F52AE0416D0080B807 /* SolidColorActivityIndicator.swift */; }; 0CD223DF2AA8ADFD002BD761 /* DashboardQuickActionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD223DE2AA8ADFD002BD761 /* DashboardQuickActionsViewModel.swift */; }; 0CD223E02AA8ADFD002BD761 /* DashboardQuickActionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD223DE2AA8ADFD002BD761 /* DashboardQuickActionsViewModel.swift */; }; 0CD382832A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD382822A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift */; }; @@ -6116,6 +6118,7 @@ 0C04532A2AC77245003079C8 /* SiteMediaDocumentInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaDocumentInfoView.swift; sourceTree = ""; }; 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerMenu.swift; sourceTree = ""; }; 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsStream.swift; sourceTree = ""; }; + 0C1531FD2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostSearchViewModel+Highlighter.swift"; sourceTree = ""; }; 0C23F3352AC4AD3400EE6117 /* SiteMediaSelectionTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaSelectionTitleView.swift; sourceTree = ""; }; 0C23F33D2AC4AEF600EE6117 /* SiteMediaPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaPickerViewController.swift; sourceTree = ""; }; 0C2518AD2ABE1EA000381D31 /* iphone-photo.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = "iphone-photo.heic"; sourceTree = ""; }; @@ -6165,10 +6168,10 @@ 0CB4057029C8DCF4008EED0A /* BlogDashboardPersonalizationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationViewModel.swift; sourceTree = ""; }; 0CB4057229C8DD01008EED0A /* BlogDashboardPersonalizationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizationView.swift; sourceTree = ""; }; 0CB4057B29C8DEE1008EED0A /* BlogDashboardPersonalizeCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersonalizeCardCell.swift; sourceTree = ""; }; - 0CB424F52AE0416D0080B807 /* SolidColorActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidColorActivityIndicator.swift; sourceTree = ""; }; 0CB424ED2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchTokenTableCell.swift; sourceTree = ""; }; 0CB424F02ADEE52A0080B807 /* PostSearchToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchToken.swift; sourceTree = ""; }; - 0CB424F32ADF3CBE0080B807 /* PostSearchServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchServiceTests.swift; sourceTree = ""; }; + 0CB424F32ADF3CBE0080B807 /* PostSearchViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSearchViewModelTests.swift; sourceTree = ""; }; + 0CB424F52AE0416D0080B807 /* SolidColorActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidColorActivityIndicator.swift; sourceTree = ""; }; 0CD223DE2AA8ADFD002BD761 /* DashboardQuickActionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardQuickActionsViewModel.swift; sourceTree = ""; }; 0CD382822A4B699E00612173 /* DashboardBlazeCardCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCardCellViewModel.swift; sourceTree = ""; }; 0CD382852A4B6FCE00612173 /* DashboardBlazeCardCellViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardBlazeCardCellViewModelTest.swift; sourceTree = ""; }; @@ -10276,6 +10279,7 @@ children = ( 0CD9CC9E2AD73A560044A33C /* PostSearchViewController.swift */, 0CD9CCA22AD831590044A33C /* PostSearchViewModel.swift */, + 0C1531FD2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift */, 0CB424ED2ADEE3CD0080B807 /* PostSearchTokenTableCell.swift */, 0CB424F02ADEE52A0080B807 /* PostSearchToken.swift */, 0CA10F6C2ADAE86D00CE75AC /* PostSearchSuggestionsService.swift */, @@ -12314,7 +12318,7 @@ 59ECF87A1CB7061D00E68F25 /* PostSharingControllerTests.swift */, F18B43771F849F580089B817 /* PostAttachmentTests.swift */, 8B6BD54F24293FBE00DB8F28 /* PrepublishingNudgesViewControllerTests.swift */, - 0CB424F32ADF3CBE0080B807 /* PostSearchServiceTests.swift */, + 0CB424F32ADF3CBE0080B807 /* PostSearchViewModelTests.swift */, ); name = Posts; sourceTree = ""; @@ -21246,6 +21250,7 @@ F5B9151F244653C100179876 /* TabbedViewController.swift in Sources */, FF00889B204DF3ED007CCE66 /* Blog+Quota.swift in Sources */, C533CF350E6D3ADA000C3DE8 /* CommentsViewController.m in Sources */, + 0C1531FE2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift in Sources */, FAFF153D1C98962E007D1C90 /* SiteSettingsViewController+SiteManagement.swift in Sources */, D816C1EE20E0892200C4D82F /* Follow.swift in Sources */, 0C896DE22A3A767200D7D4E7 /* SiteVisibility+Extensions.swift in Sources */, @@ -23555,7 +23560,7 @@ D88A649C208D7D81008AE9BC /* StockPhotosDataSourceTests.swift in Sources */, F4DD58362A168229009A772D /* ReaderPostCellActionsTests.swift in Sources */, 3236F7A124B61B950088E8F3 /* ReaderInterestsDataSourceTests.swift in Sources */, - 0CB424F42ADF3CBE0080B807 /* PostSearchServiceTests.swift in Sources */, + 0CB424F42ADF3CBE0080B807 /* PostSearchViewModelTests.swift in Sources */, 8BDA5A74247C5EAA00AB124C /* ReaderDetailCoordinatorTests.swift in Sources */, 74585B991F0D58F300E7E667 /* DomainsServiceTests.swift in Sources */, 8B7623382384373E00AB3EE7 /* PageListViewControllerTests.swift in Sources */, @@ -24768,6 +24773,7 @@ FABB23A02602FC2C00C8785C /* PluginDirectoryViewController.swift in Sources */, FABB23A12602FC2C00C8785C /* RoleService.swift in Sources */, FABB23A22602FC2C00C8785C /* AccountHelper.swift in Sources */, + 0C1531FF2AE17140003CDE13 /* PostSearchViewModel+Highlighter.swift in Sources */, FABB23A32602FC2C00C8785C /* Sites.intentdefinition in Sources */, FABB23A42602FC2C00C8785C /* MenuItemSourceTextBar.m in Sources */, FABB23A52602FC2C00C8785C /* PluginDirectoryViewModel.swift in Sources */, diff --git a/WordPress/WordPressTest/PostSearchServiceTests.swift b/WordPress/WordPressTest/PostSearchViewModelTests.swift similarity index 78% rename from WordPress/WordPressTest/PostSearchServiceTests.swift rename to WordPress/WordPressTest/PostSearchViewModelTests.swift index 58f626e9946c..97cf8001e0cc 100644 --- a/WordPress/WordPressTest/PostSearchServiceTests.swift +++ b/WordPress/WordPressTest/PostSearchViewModelTests.swift @@ -2,13 +2,13 @@ import XCTest @testable import WordPress -class PostSearchServiceTests: XCTestCase { +class PostSearchViewModelTests: XCTestCase { func testThatAdjacentRangesAreCollapsed() throws { // GIVEN - let title = "one two xxxxx one" + let string = NSMutableAttributedString(string: "one two xxxxx one") // WHEN - let string = PostSearchService.makeTitle(for: title, terms: ["one", "two"]) + PostSearchViewModel.highlight(terms: ["one", "two"], in: string) // THEN XCTAssertTrue(string.hasAttribute(.backgroundColor, in: NSRange(location: 0, length: 7))) @@ -18,10 +18,10 @@ class PostSearchServiceTests: XCTestCase { func testThatCaseIsIgnored() { // GIVEN - let title = "One xxxxx óne" + let string = NSMutableAttributedString(string: "One xxxxx óne") // WHEN - let string = PostSearchService.makeTitle(for: title, terms: ["one"]) + PostSearchViewModel.highlight(terms: ["one"], in: string) // THEN XCTAssertTrue(string.hasAttribute(.backgroundColor, in: NSRange(location: 0, length: 3)))