[Framework] Show top words for filtered items in the Feed List (#12)
This PR contains the work done to show the top 3 words belonging to the filtered reviews in the `FeedListViewController` view controller. Reviewed-on: rock-n-code/app-reviews#12 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #12.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// TopWord+DTOs.swift
|
||||
// ReviewsFeed
|
||||
//
|
||||
// Created by Javier Cicchelli on 21/03/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ReviewsFilterKit
|
||||
|
||||
extension TopWord {
|
||||
|
||||
// MARK: Initialisers
|
||||
init(_ dto: WordCount) {
|
||||
self = .init(
|
||||
id: UUID().uuidString,
|
||||
term: dto.word.term,
|
||||
count: dto.count
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// TopWord.swift
|
||||
// ReviewsFeed
|
||||
//
|
||||
// Created by Javier Cicchelli on 21/03/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
struct TopWord: Identifiable {
|
||||
|
||||
// MARK: Constants
|
||||
let id: String
|
||||
let term: String
|
||||
let count: Int
|
||||
|
||||
}
|
||||
@@ -18,6 +18,9 @@ extension FeedListViewController {
|
||||
|
||||
// MARK: Constants
|
||||
private let configuration: Configuration
|
||||
|
||||
private let filterWords: FilterWordsUseCase = .init()
|
||||
private let topWords: TopWordsUseCase = .init()
|
||||
|
||||
// MARK: Properties
|
||||
@Published var filter: FilterOption = .all
|
||||
@@ -26,9 +29,11 @@ extension FeedListViewController {
|
||||
@Published var isLoading: Bool = false
|
||||
|
||||
var items: [Review] = []
|
||||
var words: [TopWord] = []
|
||||
|
||||
private var reviewsAll: [Review] = []
|
||||
private var reviewsFiltered: FilteredReviews = [:]
|
||||
private var reviewsTopWords: TopWordsReviews = [:]
|
||||
|
||||
lazy private var iTunesService: iTunesService = {
|
||||
.init(configuration: .init(session: configuration.session))
|
||||
@@ -38,6 +43,18 @@ extension FeedListViewController {
|
||||
init(configuration: Configuration = .init()) {
|
||||
self.configuration = configuration
|
||||
}
|
||||
|
||||
// MARK: Computed
|
||||
var itemsCount: Int {
|
||||
isWordsShowing
|
||||
? items.count
|
||||
: items.count + 1
|
||||
}
|
||||
|
||||
var isWordsShowing: Bool {
|
||||
filter != .all
|
||||
&& !words.isEmpty
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
func fetch() {
|
||||
@@ -56,6 +73,14 @@ extension FeedListViewController {
|
||||
.reduce(into: FilteredReviews()) { partialResult, option in
|
||||
partialResult[option] = reviewsAll.filter { $0.rating.stars == option.rawValue }
|
||||
}
|
||||
reviewsTopWords = reviewsFiltered
|
||||
.mapValues { reviews in
|
||||
reviews.map(\.comment)
|
||||
.compactMap { try? filterWords($0) }
|
||||
}
|
||||
.mapValues {
|
||||
topWords($0).map(TopWord.init)
|
||||
}
|
||||
|
||||
items = reviewsAll
|
||||
isFilterEnabled = !items.isEmpty
|
||||
@@ -74,9 +99,18 @@ extension FeedListViewController {
|
||||
items = option == .all
|
||||
? reviewsAll
|
||||
: reviewsFiltered[option] ?? []
|
||||
|
||||
words = reviewsTopWords[option] ?? []
|
||||
|
||||
filter = option
|
||||
}
|
||||
|
||||
func item(for index: Int) -> Review? {
|
||||
guard index < items.count else { return nil }
|
||||
|
||||
return isWordsShowing
|
||||
? items[index - 1]
|
||||
: items[index]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -86,5 +120,6 @@ private extension FeedListViewController.ViewModel {
|
||||
|
||||
// MARK: Type aliases
|
||||
typealias FilteredReviews = [FilterOption: [Review]]
|
||||
typealias TopWordsReviews = [FilterOption: [TopWord]]
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FeedItemCell.swift
|
||||
// ReviewsFeed
|
||||
//
|
||||
// Created by Javier Cicchelli on 21/03/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import ReviewsUIKit
|
||||
import UIKit
|
||||
|
||||
final class FeedItemCell: UITableViewCell, TableCell {
|
||||
|
||||
// MARK: Constants
|
||||
static let cellID = "FeedItemCell"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// TopWordsCell.swift
|
||||
// ReviewsFeed
|
||||
//
|
||||
// Created by Javier Cicchelli on 21/03/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import ReviewsUIKit
|
||||
import UIKit
|
||||
|
||||
final class TopWordsCell: UITableViewCell, TableCell {
|
||||
|
||||
// MARK: Constants
|
||||
static let cellID = "TopWordsCell"
|
||||
|
||||
}
|
||||
@@ -40,11 +40,12 @@ public class FeedListViewController: UITableViewController {
|
||||
image: UIImage.Icon.star,
|
||||
children: {
|
||||
FilterOption.allCases.map { option -> UIAction in
|
||||
.init(title: option.text,
|
||||
image: .init(systemName: option.icon)
|
||||
) { [weak self] _ in
|
||||
self?.viewModel.filter(by: option)
|
||||
}
|
||||
.init(
|
||||
title: option.text,
|
||||
image: .init(systemName: option.icon)
|
||||
) { [weak self] _ in
|
||||
self?.viewModel.filter(by: option)
|
||||
}
|
||||
}
|
||||
}()
|
||||
)
|
||||
@@ -83,30 +84,18 @@ public class FeedListViewController: UITableViewController {
|
||||
_ tableView: UITableView,
|
||||
numberOfRowsInSection section: Int
|
||||
) -> Int {
|
||||
viewModel.items.count
|
||||
viewModel.itemsCount
|
||||
}
|
||||
|
||||
public override func tableView(
|
||||
_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath
|
||||
) -> UITableViewCell {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: .Cell.feedItem) else {
|
||||
return .init()
|
||||
if viewModel.isWordsShowing && indexPath.row == 0 {
|
||||
makeTopWordsCell(tableView)
|
||||
} else {
|
||||
makeFeedItemCell(tableView, at: indexPath.row)
|
||||
}
|
||||
|
||||
cell.contentConfiguration = {
|
||||
if #available(iOS 16.0, *) {
|
||||
UIHostingConfiguration {
|
||||
FeedItemCell(items[indexPath.row])
|
||||
}
|
||||
} else {
|
||||
HostingConfiguration {
|
||||
FeedItemCell(items[indexPath.row])
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: UITableViewDelegate
|
||||
@@ -158,8 +147,57 @@ private extension FeedListViewController {
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func makeFeedItemCell(
|
||||
_ tableView: UITableView,
|
||||
at row: Int
|
||||
) -> UITableViewCell {
|
||||
guard
|
||||
let cell = FeedItemCell.dequeue(from: tableView),
|
||||
let item = viewModel.item(for: row)
|
||||
else {
|
||||
return .init()
|
||||
}
|
||||
|
||||
cell.separatorInset = .init(width: tableView.bounds.size.width)
|
||||
cell.contentConfiguration = {
|
||||
if #available(iOS 16.0, *) {
|
||||
UIHostingConfiguration {
|
||||
FeedItemView(item)
|
||||
}
|
||||
} else {
|
||||
HostingConfiguration {
|
||||
FeedItemView(item)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func makeTopWordsCell(_ tableView: UITableView) -> UITableViewCell {
|
||||
guard let cell = TopWordsCell.dequeue(from: tableView) else {
|
||||
return .init()
|
||||
}
|
||||
|
||||
cell.separatorInset = .init(width: tableView.bounds.size.width)
|
||||
cell.contentConfiguration = {
|
||||
if #available(iOS 16.0, *) {
|
||||
UIHostingConfiguration {
|
||||
TopWordsView(viewModel.words)
|
||||
}
|
||||
} else {
|
||||
HostingConfiguration {
|
||||
TopWordsView(viewModel.words)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func registerTableCells() {
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: .Cell.feedItem)
|
||||
FeedItemCell.register(in: tableView)
|
||||
TopWordsCell.register(in: tableView)
|
||||
}
|
||||
|
||||
func setNavigationBar() {
|
||||
@@ -232,6 +270,21 @@ private extension String {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIEdgeInsets+Inits
|
||||
private extension UIEdgeInsets {
|
||||
|
||||
// MARK: Initialisers
|
||||
init(width: CGFloat) {
|
||||
self = .init(
|
||||
top: 0,
|
||||
left: width,
|
||||
bottom: 0,
|
||||
right: 0
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
#if DEBUG
|
||||
import ReviewsiTunesKit
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// FeedItemCell.swift
|
||||
// FeedItemView.swift
|
||||
// ReviewsFeed
|
||||
//
|
||||
// Created by Javier Cicchelli on 19/03/2024.
|
||||
@@ -9,7 +9,7 @@
|
||||
import ReviewsUIKit
|
||||
import SwiftUI
|
||||
|
||||
struct FeedItemCell: View {
|
||||
struct FeedItemView: View {
|
||||
|
||||
// MARK: Constants
|
||||
private let item: Review
|
||||
@@ -66,8 +66,8 @@ struct FeedItemCell: View {
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
#Preview("Feed Item Cell") {
|
||||
FeedItemCell(.init(
|
||||
#Preview("Feed Item") {
|
||||
FeedItemView(.init(
|
||||
author: "Some author name here...",
|
||||
comment: "Some review comment here...",
|
||||
id: 0,
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// TopWordsView.swift
|
||||
// ReviewsFeed
|
||||
//
|
||||
// Created by Javier Cicchelli on 21/03/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TopWordsView: View {
|
||||
|
||||
// MARK: Constants
|
||||
private let topWords: [TopWord]
|
||||
|
||||
// MARK: Initialisers
|
||||
init(_ topWords: [TopWord]) {
|
||||
self.topWords = topWords
|
||||
}
|
||||
|
||||
// MARK: Body
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 12) {
|
||||
ForEach(topWords) { topWord in
|
||||
Item(
|
||||
term: topWord.term,
|
||||
count: String(topWord.count)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Structs
|
||||
private extension TopWordsView {
|
||||
struct Item: View {
|
||||
|
||||
// MARK: Constants
|
||||
let term: String
|
||||
let count: String
|
||||
|
||||
// MARK: Body
|
||||
var body: some View {
|
||||
HStack(
|
||||
alignment: .center,
|
||||
spacing: 6
|
||||
) {
|
||||
Text(count)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.primary.opacity(0.75))
|
||||
|
||||
Text(term)
|
||||
.lineLimit(1)
|
||||
.font(.body.bold())
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.secondary.opacity(0.75))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
#Preview {
|
||||
TopWordsView([
|
||||
.init(id: "1", term: "Something", count: 3),
|
||||
.init(id: "2", term: "Something", count: 2),
|
||||
.init(id: "3", term: "Something", count: 1),
|
||||
])
|
||||
.padding(.horizontal)
|
||||
}
|
||||
Reference in New Issue
Block a user