[Framework] Feed List improvements (#19)

This PR contains the work done to improve the overall implementation of the `FeedListViewModel` view model in the `Feed` framework by utilising unit tests.

Reviewed-on: rock-n-code/app-reviews#19
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 #19.
This commit is contained in:
2024-03-22 14:44:52 +00:00
committed by javier
parent 050e7e9620
commit c150b88ddc
6 changed files with 919 additions and 102 deletions
@@ -0,0 +1,65 @@
//
// Array+ReviewDTOs.swift
// ReviewsFeed
//
// Created by Javier Cicchelli on 22/03/2024.
// Copyright © 2024 Röck+Cöde. All rights reserved.
//
import Foundation
import ReviewsFeedKit
extension Array where Element == ReviewsFeedKit.Review {
// MARK: Constants
static let none: [ReviewsFeedKit.Review] = []
static let sample: [ReviewsFeedKit.Review] = [
.init(
id: 1,
author: "Some author name #1 here",
title: "Some review title #1 goes here...",
content: "Some long, long, explanatory review comment #1 goes here...",
rating: 3,
version: "v1.0.0",
updated: .init()
),
.init(
id: 2,
author: "Some author name #2 here",
title: "Some review title #2 goes here...",
content: "Some long, long, explanatory review comment #2 goes here...",
rating: 5,
version: "v1.0.0",
updated: .init()
),
.init(
id: 3,
author: "Some author name #3 here",
title: "Some review title #3 goes here...",
content: "Some long, long, explanatory review comment #3 goes here...",
rating: 1,
version: "v1.0.0",
updated: .init()
),
.init(
id: 4,
author: "Some author name #4 here",
title: "Some review title #4 goes here...",
content: "Some long, long, explanatory review comment #4 goes here...",
rating: 4,
version: "v1.0.0",
updated: .init()
),
.init(
id: 5,
author: "Some author name #5 here",
title: "Some review title #5 goes here...",
content: "Some long, long, explanatory review comment #5 goes here...",
rating: 2,
version: "v1.0.0",
updated: .init()
),
]
}
@@ -20,14 +20,14 @@ extension FeedListViewController {
private let topWords: TopWordsUseCase = .init()
// MARK: Properties
@Published var filter: FilterOption = .all
@Published var isFilterEnabled: Bool = false
@Published var isFiltering: Bool = false
@Published var isLoading: Bool = false
@Published var state: FeedListState = .initial
@Published private(set) var filter: FilterOption = .all
@Published private(set) var isFilterEnabled: Bool = false
@Published private(set) var isFiltering: Bool = false
@Published private(set) var isLoading: Bool = false
@Published private(set) var state: FeedListState = .initial
var items: [Review] = []
var words: [TopWord] = []
private(set) var items: [Review] = []
private(set) var words: [TopWord] = []
private var reviewsAll: [Review] = []
private var reviewsFiltered: FilteredReviews = [:]
@@ -57,50 +57,51 @@ extension FeedListViewController {
var isWordsShowing: Bool {
filter != .all
&& !words.isEmpty
&& !words.isEmpty
}
// MARK: Functions
func fetch() {
Task {
isFilterEnabled = false
isLoading = items.isEmpty
func fetch() async {
isFilterEnabled = false
isLoading = items.isEmpty
do {
let output = try await iTunesService.getReviews(.init(
appID: configuration.appID,
countryCode: configuration.countryCode
))
reviewsAll = output.reviews.map(Review.init)
reviewsFiltered = FilterOption.allCases
.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)
}
do {
let output = try await iTunesService.getReviews(.init(
appID: configuration.appID,
countryCode: configuration.countryCode
))
reviewsAll = output.reviews.map(Review.init)
reviewsFiltered = FilterOption.allCases
.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 = filter == .all
? reviewsAll
: reviewsFiltered[filter] ?? []
items = filter == .all
? reviewsAll
: reviewsFiltered[filter] ?? []
words = filter == .all
? []
: reviewsTopWords[filter] ?? []
isFilterEnabled = !items.isEmpty
state = items.isEmpty
? .empty
: .populated
} catch {
items = []
state = .error
}
isLoading = false
isFilterEnabled = !items.isEmpty
state = items.isEmpty
? .empty
: .populated
} catch {
items = []
state = .error
}
isLoading = false
}
func filter(by option: FilterOption) {
@@ -115,16 +116,19 @@ extension FeedListViewController {
}
func item(for index: Int) -> Review? {
guard
!items.isEmpty,
index < items.count
else {
guard !items.isEmpty else {
return nil
}
return isWordsShowing
? items[index - 1]
: items[index]
let indexToUse = isWordsShowing
? index - 1
: index
guard indexToUse < items.count else {
return nil
}
return items[indexToUse]
}
func openItem(at index: Int) {
@@ -118,7 +118,7 @@ final class FeedListViewController: UIViewController {
registerTableCells()
bindViewModel()
viewModel.fetch()
Task { await viewModel.fetch() }
}
}
@@ -170,7 +170,7 @@ private extension FeedListViewController {
// MARK: Actions
@objc func refresh(_ sender: AnyObject) {
self.viewModel.fetch()
Task { await self.viewModel.fetch() }
}
// MARK: Functions
@@ -264,7 +264,7 @@ private extension FeedListViewController {
)
: nil,
action: isErrorState
? { self.viewModel.fetch() }
? { Task { await self.viewModel.fetch() } }
: nil
)
@@ -455,53 +455,7 @@ import ReviewsiTunesKit
#Preview("Feed List with few reviews") {
MockURLProtocol.response = .init(
statusCode: 200,
object: Feed(entries: [
.init(
id: 1,
author: "Some author name #1 here",
title: "Some review title #1 goes here...",
content: "Some long, explanatory review comment #1 goes here...",
rating: 3,
version: "v1.0.0",
updated: .init()
),
.init(
id: 2,
author: "Some author name #2 here",
title: "Some review title #2 goes here...",
content: "Some long, explanatory review comment #2 goes here...",
rating: 5,
version: "v1.0.0",
updated: .init()
),
.init(
id: 3,
author: "Some author name #3 here",
title: "Some review title #3 goes here...",
content: "Some long, explanatory review comment #3 goes here...",
rating: 1,
version: "v1.0.0",
updated: .init()
),
.init(
id: 4,
author: "Some author name #4 here",
title: "Some review title #4 goes here...",
content: "Some long, explanatory review comment #4 goes here...",
rating: 4,
version: "v1.0.0",
updated: .init()
),
.init(
id: 5,
author: "Some author name #5 here",
title: "Some review title #5 goes here...",
content: "Some long, explanatory review comment #5 goes here...",
rating: 2,
version: "v1.0.0",
updated: .init()
),
])
object: Feed(entries: .sample)
)
return UINavigationController(rootViewController: FeedListViewController(.init(