Skip to content

Commit

Permalink
Merge pull request #21828 from wordpress-mobile/task/search-new-cells
Browse files Browse the repository at this point in the history
Posts & Pages: Integrate new cells in search
  • Loading branch information
kean authored Oct 20, 2023
2 parents aa537e3 + 2756f7c commit 9ca04a8
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 167 deletions.
90 changes: 90 additions & 0 deletions WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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"
}
}
]
},
Expand Down
50 changes: 18 additions & 32 deletions WordPress/Classes/ViewRelated/Post/PostListCell.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import UIKit
import Combine

final class PostListCell: UITableViewCell, Reusable {

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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() {
Expand Down
41 changes: 34 additions & 7 deletions WordPress/Classes/ViewRelated/Post/PostListItemViewModel.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
97 changes: 3 additions & 94 deletions WordPress/Classes/ViewRelated/Post/Search/PostSearchService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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<NSManagedObjectID> = []
Expand Down Expand Up @@ -77,108 +77,17 @@ 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 {
let searchTerm: String
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<String.Index>], in string: String) -> [Range<String.Index>] {
var output: [Range<String.Index>] = []
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..<rhs.upperBound
ranges.removeLast()
ranges.append(range)
} else {
output.append(rhs)
}
}
return output
}
}

private extension String {
func ranges(of string: String, options: String.CompareOptions) -> [Range<String.Index>] {
var ranges: [Range<String.Index>] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let range = range(of: string, options: options, range: startIndex..<endIndex) {
ranges.append(range)
startIndex = range.upperBound
}
return ranges
}
}
Loading

0 comments on commit 9ca04a8

Please sign in to comment.