[Framework] Feed item filtering in the Feed List view (#11)
This PR contains the work done to implement the filtering of the items shown in the `FeedListViewController` view controller by star rating. Reviewed-on: #11 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
parent
c9f4b9a677
commit
eac34c61c1
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// AppDelegate.swift
|
// AppDelegate.swift
|
||||||
// AppStoreReviews
|
// ReviewsApp
|
||||||
//
|
//
|
||||||
// Created by Dmitrii Ivanov on 21/07/2020.
|
// Created by Dmitrii Ivanov on 21/07/2020.
|
||||||
// Copyright © 2020 ING. All rights reserved.
|
// Copyright © 2020 ING. All rights reserved.
|
||||||
@ -10,16 +10,27 @@ import ReviewsFeed
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
||||||
window = UIWindow(frame: UIScreen.main.bounds)
|
|
||||||
let viewController = FeedListViewController()
|
|
||||||
window?.rootViewController = UINavigationController(rootViewController: viewController)
|
|
||||||
window?.makeKeyAndVisible()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - UIApplicationDelegate
|
||||||
|
extension AppDelegate: UIApplicationDelegate {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
|
|
||||||
|
window?.rootViewController = UINavigationController(rootViewController: FeedListViewController())
|
||||||
|
window?.makeKeyAndVisible()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
114
Frameworks/Feed/Bundle/Resources/Catalogs/Localizable.xcstrings
Normal file
114
Frameworks/Feed/Bundle/Resources/Catalogs/Localizable.xcstrings
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"sourceLanguage" : "en",
|
||||||
|
"strings" : {
|
||||||
|
"common.filter.menu.all-reviews.action.title.text" : {
|
||||||
|
"comment" : "The title for the All Reviews action inside the Filter menu in the Feed List view",
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "All reviews"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common.filter.menu.only-1-star-reviews.action.title.text" : {
|
||||||
|
"comment" : "The title for the 1-Star Only Reviews action inside the Filter menu in the Feed List view",
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "1-star reviews only "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common.filter.menu.only-2-star-reviews.action.title.text" : {
|
||||||
|
"comment" : "The title for the 2-Star Only Reviews action inside the Filter menu in the Feed List view",
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "2-star reviews only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common.filter.menu.only-3-star-reviews.action.title.text" : {
|
||||||
|
"comment" : "The title for the 3-Star Only Reviews action inside the Filter menu in the Feed List view",
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "3-star reviews only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common.filter.menu.only-4-star-reviews.action.title.text" : {
|
||||||
|
"comment" : "The title for the 4-Star Only Reviews action inside the Filter menu in the Feed List view",
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "4-star reviews only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common.filter.menu.only-5-star-reviews.action.title.text" : {
|
||||||
|
"comment" : "The title for the 5-Star Only Reviews action inside the Filter menu in the Feed List view",
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "5-star reviews only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common.filter.menu.title.text" : {
|
||||||
|
"comment" : "The title for the Filter menu option in the Feed List view.",
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Filter review by star rating"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"view.feed-list.navigation-bar.button.filter-list.text" : {
|
||||||
|
"comment" : "The text for the Filter button at the navigation bar in the Feed List view.",
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Filter items"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"view.feed-list.navigation-bar.title.text" : {
|
||||||
|
"comment" : "The title for the navigation bar in the Feed List view.",
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Latest reviews"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version" : "1.0"
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
//
|
||||||
|
// FilterOption.swift
|
||||||
|
// ReviewsFeed
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 20/03/2024.
|
||||||
|
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import ReviewsFoundationKit
|
||||||
|
import ReviewsUIKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum FilterOption: Int, CaseIterable {
|
||||||
|
case all = 0
|
||||||
|
case only1Star
|
||||||
|
case only2Star
|
||||||
|
case only3Star
|
||||||
|
case only4Star
|
||||||
|
case only5Star
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Computed
|
||||||
|
extension FilterOption {
|
||||||
|
|
||||||
|
var icon: String {
|
||||||
|
switch self {
|
||||||
|
case .all: .Icon.starAll
|
||||||
|
case .only1Star: .Icon.star1
|
||||||
|
case .only2Star: .Icon.star2
|
||||||
|
case .only3Star: .Icon.star3
|
||||||
|
case .only4Star: .Icon.star4
|
||||||
|
case .only5Star: .Icon.star5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var text: String {
|
||||||
|
switch self {
|
||||||
|
case .all: NSLocalizedString(
|
||||||
|
.Key.Menu.Action.allReviews,
|
||||||
|
bundle: .module,
|
||||||
|
comment: .empty
|
||||||
|
)
|
||||||
|
case .only1Star: NSLocalizedString(
|
||||||
|
.Key.Menu.Action.only1StarReviews,
|
||||||
|
bundle: .module,
|
||||||
|
comment: .empty
|
||||||
|
)
|
||||||
|
case .only2Star: NSLocalizedString(
|
||||||
|
.Key.Menu.Action.only2StarReviews,
|
||||||
|
bundle: .module,
|
||||||
|
comment: .empty
|
||||||
|
)
|
||||||
|
case .only3Star: NSLocalizedString(
|
||||||
|
.Key.Menu.Action.only3StarReviews,
|
||||||
|
bundle: .module,
|
||||||
|
comment: .empty
|
||||||
|
)
|
||||||
|
case .only4Star: NSLocalizedString(
|
||||||
|
.Key.Menu.Action.only4StarReviews,
|
||||||
|
bundle: .module,
|
||||||
|
comment: .empty
|
||||||
|
)
|
||||||
|
case .only5Star: NSLocalizedString(
|
||||||
|
.Key.Menu.Action.only5StarReviews,
|
||||||
|
bundle: .module,
|
||||||
|
comment: .empty
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String+Constants
|
||||||
|
private extension String {
|
||||||
|
enum Key {
|
||||||
|
enum Menu {
|
||||||
|
enum Action {
|
||||||
|
static let allReviews = "common.filter.menu.all-reviews.action.title.text"
|
||||||
|
static let only1StarReviews = "common.filter.menu.only-1-star-reviews.action.title.text"
|
||||||
|
static let only2StarReviews = "common.filter.menu.only-2-star-reviews.action.title.text"
|
||||||
|
static let only3StarReviews = "common.filter.menu.only-3-star-reviews.action.title.text"
|
||||||
|
static let only4StarReviews = "common.filter.menu.only-4-star-reviews.action.title.text"
|
||||||
|
static let only5StarReviews = "common.filter.menu.only-5-star-reviews.action.title.text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// Bundle+Constants.swift
|
||||||
|
// ReviewsFeed
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 20/03/2024.
|
||||||
|
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Bundle {
|
||||||
|
|
||||||
|
// MARK: Constants
|
||||||
|
static let module = Bundle(for: FeedItemViewController.self)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// Review+DTOs.swift
|
||||||
|
// ReviewsFeed
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 20/03/2024.
|
||||||
|
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import ReviewsFeedKit
|
||||||
|
|
||||||
|
extension Review {
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
init(_ dto: ReviewsFeedKit.Review) {
|
||||||
|
self = .init(
|
||||||
|
author: dto.author,
|
||||||
|
comment: dto.content,
|
||||||
|
id: dto.id,
|
||||||
|
rating: .init(
|
||||||
|
stars: dto.rating,
|
||||||
|
appVersion: dto.version
|
||||||
|
),
|
||||||
|
title: dto.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
//
|
||||||
|
// FeedListViewModel.swift
|
||||||
|
// ReviewsFeed
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 18/03/2024.
|
||||||
|
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import ReviewsFilterKit
|
||||||
|
import ReviewsiTunesKit
|
||||||
|
|
||||||
|
extension FeedListViewController {
|
||||||
|
final class ViewModel: ObservableObject {
|
||||||
|
|
||||||
|
// MARK: Type aliases
|
||||||
|
typealias Configuration = FeedListViewController.Configuration
|
||||||
|
|
||||||
|
// MARK: Constants
|
||||||
|
private let configuration: Configuration
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
@Published var filter: FilterOption = .all
|
||||||
|
@Published var isFilterEnabled: Bool = false
|
||||||
|
@Published var isFiltering: Bool = false
|
||||||
|
@Published var isLoading: Bool = false
|
||||||
|
|
||||||
|
var items: [Review] = []
|
||||||
|
|
||||||
|
private var reviewsAll: [Review] = []
|
||||||
|
private var reviewsFiltered: FilteredReviews = [:]
|
||||||
|
|
||||||
|
lazy private var iTunesService: iTunesService = {
|
||||||
|
.init(configuration: .init(session: configuration.session))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
init(configuration: Configuration = .init()) {
|
||||||
|
self.configuration = configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
func fetch() {
|
||||||
|
Task {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
items = reviewsAll
|
||||||
|
isFilterEnabled = !items.isEmpty
|
||||||
|
} catch {
|
||||||
|
// TODO: handle this error gracefully.
|
||||||
|
print("ERROR: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter(by option: FilterOption) {
|
||||||
|
guard option != filter else { return }
|
||||||
|
|
||||||
|
items = option == .all
|
||||||
|
? reviewsAll
|
||||||
|
: reviewsFiltered[option] ?? []
|
||||||
|
|
||||||
|
filter = option
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
private extension FeedListViewController.ViewModel {
|
||||||
|
|
||||||
|
// MARK: Type aliases
|
||||||
|
typealias FilteredReviews = [FilterOption: [Review]]
|
||||||
|
|
||||||
|
}
|
@ -1,69 +0,0 @@
|
|||||||
//
|
|
||||||
// FeedViewModel.swift
|
|
||||||
// ReviewsFeed
|
|
||||||
//
|
|
||||||
// Created by Javier Cicchelli on 18/03/2024.
|
|
||||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import ReviewsiTunesKit
|
|
||||||
|
|
||||||
extension FeedListViewController {
|
|
||||||
final class ViewModel: ObservableObject {
|
|
||||||
|
|
||||||
// MARK: Type aliases
|
|
||||||
typealias Configuration = FeedListViewController.Configuration
|
|
||||||
|
|
||||||
// MARK: Constants
|
|
||||||
private let configuration: Configuration
|
|
||||||
|
|
||||||
// MARK: Properties
|
|
||||||
@Published var loading: Bool = false
|
|
||||||
|
|
||||||
var items: [Review] = []
|
|
||||||
|
|
||||||
// MARK: Initialisers
|
|
||||||
init(configuration: Configuration = .init()) {
|
|
||||||
self.configuration = configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Computed
|
|
||||||
lazy private var iTunesService: iTunesService = {
|
|
||||||
.init(configuration: .init(session: configuration.session))
|
|
||||||
}()
|
|
||||||
|
|
||||||
// MARK: Functions
|
|
||||||
func fetch() {
|
|
||||||
Task {
|
|
||||||
loading = true
|
|
||||||
|
|
||||||
do {
|
|
||||||
let output = try await iTunesService.getReviews(.init(
|
|
||||||
appID: configuration.appID,
|
|
||||||
countryCode: configuration.countryCode
|
|
||||||
))
|
|
||||||
|
|
||||||
items = output.reviews
|
|
||||||
.map { review -> Review in
|
|
||||||
.init(
|
|
||||||
author: review.author,
|
|
||||||
comment: review.content,
|
|
||||||
id: review.id,
|
|
||||||
rating: .init(
|
|
||||||
stars: review.rating,
|
|
||||||
appVersion: review.version
|
|
||||||
),
|
|
||||||
title: review.title
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// TODO: handle this error gracefully.
|
|
||||||
}
|
|
||||||
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,6 +7,8 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import ReviewsFoundationKit
|
||||||
import ReviewsUIKit
|
import ReviewsUIKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
@ -19,6 +21,36 @@ public class FeedListViewController: UITableViewController {
|
|||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
// MARK: Outlets
|
||||||
|
private lazy var filterButton = {
|
||||||
|
UIBarButtonItem(
|
||||||
|
title: NSLocalizedString(
|
||||||
|
.Key.Navigation.Button.filter,
|
||||||
|
bundle: .module,
|
||||||
|
comment: .empty
|
||||||
|
),
|
||||||
|
image: .Icon.filter,
|
||||||
|
primaryAction: nil,
|
||||||
|
menu: .init(
|
||||||
|
title: NSLocalizedString(
|
||||||
|
.Key.Menu.filter,
|
||||||
|
bundle: .module,
|
||||||
|
comment: .empty
|
||||||
|
),
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
|
||||||
// MARK: Initialisers
|
// MARK: Initialisers
|
||||||
public init(configuration: Configuration = .init()) {
|
public init(configuration: Configuration = .init()) {
|
||||||
self.viewModel = .init(configuration: configuration)
|
self.viewModel = .init(configuration: configuration)
|
||||||
@ -30,6 +62,11 @@ public class FeedListViewController: UITableViewController {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Computed
|
||||||
|
var items: [Review] {
|
||||||
|
viewModel.items
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: UIViewController
|
// MARK: UIViewController
|
||||||
public override func viewDidLoad() {
|
public override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
@ -60,11 +97,11 @@ public class FeedListViewController: UITableViewController {
|
|||||||
cell.contentConfiguration = {
|
cell.contentConfiguration = {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
UIHostingConfiguration {
|
UIHostingConfiguration {
|
||||||
FeedItemCell(viewModel.items[indexPath.row])
|
FeedItemCell(items[indexPath.row])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HostingConfiguration {
|
HostingConfiguration {
|
||||||
FeedItemCell(viewModel.items[indexPath.row])
|
FeedItemCell(items[indexPath.row])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -77,7 +114,7 @@ public class FeedListViewController: UITableViewController {
|
|||||||
_ tableView: UITableView,
|
_ tableView: UITableView,
|
||||||
didSelectRowAt indexPath: IndexPath
|
didSelectRowAt indexPath: IndexPath
|
||||||
) {
|
) {
|
||||||
let details = FeedItemViewController(viewModel.items[indexPath.row])
|
let details = FeedItemViewController(items[indexPath.row])
|
||||||
|
|
||||||
tableView.deselectRow(
|
tableView.deselectRow(
|
||||||
at: indexPath,
|
at: indexPath,
|
||||||
@ -94,15 +131,26 @@ private extension FeedListViewController {
|
|||||||
|
|
||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
func bindViewModel() {
|
func bindViewModel() {
|
||||||
viewModel.$loading
|
viewModel.$filter
|
||||||
.sink { loading in
|
.receive(on: RunLoop.main)
|
||||||
print("LOADING: \(loading)")
|
.sink { [weak self] option in
|
||||||
|
self?.updateFilterMenu(option)
|
||||||
|
self?.tableView.reloadData()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
viewModel.$isFilterEnabled
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.sink { [weak self] enabled in
|
||||||
|
self?.filterButton.isEnabled = enabled
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$loading
|
viewModel.$isLoading
|
||||||
.dropFirst()
|
.dropFirst()
|
||||||
.filter { $0 == false }
|
.filter { $0 == false }
|
||||||
|
.removeDuplicates()
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.tableView.reloadData()
|
self?.tableView.reloadData()
|
||||||
@ -118,7 +166,24 @@ private extension FeedListViewController {
|
|||||||
navigationController?.navigationBar.prefersLargeTitles = true
|
navigationController?.navigationBar.prefersLargeTitles = true
|
||||||
navigationController?.navigationBar.isTranslucent = true
|
navigationController?.navigationBar.isTranslucent = true
|
||||||
|
|
||||||
navigationItem.title = "Latest reviews"
|
navigationItem.rightBarButtonItem = filterButton
|
||||||
|
navigationItem.title = NSLocalizedString(
|
||||||
|
.Key.Navigation.title,
|
||||||
|
bundle: .module,
|
||||||
|
comment: .empty
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFilterMenu(_ option: FilterOption) {
|
||||||
|
filterButton
|
||||||
|
.menu?
|
||||||
|
.children
|
||||||
|
.compactMap { $0 as? UIAction }
|
||||||
|
.forEach { action in
|
||||||
|
action.state = action.title == option.text
|
||||||
|
? .on
|
||||||
|
: .off
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -151,11 +216,24 @@ private extension String {
|
|||||||
enum Cell {
|
enum Cell {
|
||||||
static let feedItem = "FeedItemCell"
|
static let feedItem = "FeedItemCell"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Key {
|
||||||
|
enum Menu {
|
||||||
|
static let filter = "common.filter.menu.title.text"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Navigation {
|
||||||
|
static let title = "view.feed-list.navigation-bar.title.text"
|
||||||
|
|
||||||
|
enum Button {
|
||||||
|
static let filter = "view.feed-list.navigation-bar.button.filter-list.text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
import ReviewsFoundationKit
|
|
||||||
import ReviewsiTunesKit
|
import ReviewsiTunesKit
|
||||||
|
|
||||||
@available(iOS 17.0, *)
|
@available(iOS 17.0, *)
|
||||||
|
@ -10,9 +10,18 @@ public extension String {
|
|||||||
enum Icon {
|
enum Icon {
|
||||||
|
|
||||||
// MARK: Constants
|
// MARK: Constants
|
||||||
static let questionMark = "questionmark.circle.fill"
|
|
||||||
|
|
||||||
public static let info = "info.circle.fill"
|
public static let info = "info.circle.fill"
|
||||||
public static let person = "person.crop.circle.fill"
|
public static let person = "person.crop.circle.fill"
|
||||||
|
public static let starAll = "a.circle"
|
||||||
|
public static let star1 = "1.circle"
|
||||||
|
public static let star2 = "2.circle"
|
||||||
|
public static let star3 = "3.circle"
|
||||||
|
public static let star4 = "4.circle"
|
||||||
|
public static let star5 = "5.circle"
|
||||||
|
|
||||||
|
static let filter = "camera.filters"
|
||||||
|
static let questionMark = "questionmark.circle.fill"
|
||||||
|
static let star = "star"
|
||||||
|
static let starFill = "star.fill"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
Libraries/UI/Kit/Sources/Extensions/UIImage+ICons.swift
Normal file
19
Libraries/UI/Kit/Sources/Extensions/UIImage+ICons.swift
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// UIImage+ICons.swift
|
||||||
|
// ReviewsUIKit
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 20/03/2024.
|
||||||
|
// Copyright © 2024 Röck+Cöde VoF. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public extension UIImage {
|
||||||
|
enum Icon {
|
||||||
|
|
||||||
|
// MARK: Constants
|
||||||
|
public static let filter = UIImage(systemName: .Icon.filter)
|
||||||
|
public static let star = UIImage(systemName: .Icon.star)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,11 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0220ADA22BA90646001E6A9F /* FeedItemCell.swift */; };
|
0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0220ADA22BA90646001E6A9F /* FeedItemCell.swift */; };
|
||||||
023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */; };
|
023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */; };
|
||||||
02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */; };
|
02620B8C2BA89C9A00DE7137 /* FeedListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02620B8B2BA89C9A00DE7137 /* FeedListViewModel.swift */; };
|
||||||
|
02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E782BAB6B0200710E14 /* FilterOption.swift */; };
|
||||||
|
02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */; };
|
||||||
|
02909E7D2BAB7FFE00710E14 /* Review+DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */; };
|
||||||
|
02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */; };
|
||||||
02DC7F9F2BA51793000EEEBE /* ReviewsFeed.h in Headers */ = {isa = PBXBuildFile; fileRef = 02DC7F912BA51793000EEEBE /* ReviewsFeed.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
02DC7F9F2BA51793000EEEBE /* ReviewsFeed.h in Headers */ = {isa = PBXBuildFile; fileRef = 02DC7F912BA51793000EEEBE /* ReviewsFeed.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
02DC7FA22BA51793000EEEBE /* ReviewsFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; };
|
02DC7FA22BA51793000EEEBE /* ReviewsFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; };
|
||||||
02DC7FA32BA51793000EEEBE /* ReviewsFeed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
02DC7FA32BA51793000EEEBE /* ReviewsFeed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
@ -50,7 +54,11 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
0220ADA22BA90646001E6A9F /* FeedItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemCell.swift; sourceTree = "<group>"; };
|
0220ADA22BA90646001E6A9F /* FeedItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemCell.swift; sourceTree = "<group>"; };
|
||||||
023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Constants.swift"; sourceTree = "<group>"; };
|
023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Constants.swift"; sourceTree = "<group>"; };
|
||||||
02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = "<group>"; };
|
02620B8B2BA89C9A00DE7137 /* FeedListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
02909E782BAB6B0200710E14 /* FilterOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterOption.swift; sourceTree = "<group>"; };
|
||||||
|
02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Constants.swift"; sourceTree = "<group>"; };
|
||||||
|
02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review+DTOs.swift"; sourceTree = "<group>"; };
|
||||||
|
02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReviewsFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReviewsFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
02DC7F912BA51793000EEEBE /* ReviewsFeed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReviewsFeed.h; sourceTree = "<group>"; };
|
02DC7F912BA51793000EEEBE /* ReviewsFeed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReviewsFeed.h; sourceTree = "<group>"; };
|
||||||
02DC7FB12BA52084000EEEBE /* Libraries */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Libraries; sourceTree = "<group>"; };
|
02DC7FB12BA52084000EEEBE /* Libraries */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Libraries; sourceTree = "<group>"; };
|
||||||
@ -96,7 +104,9 @@
|
|||||||
023AC7FA2BAA3EB60027D064 /* Extensions */ = {
|
023AC7FA2BAA3EB60027D064 /* Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */,
|
||||||
023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */,
|
023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */,
|
||||||
|
02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -113,6 +123,7 @@
|
|||||||
02620B862BA89C0000DE7137 /* Logic */ = {
|
02620B862BA89C0000DE7137 /* Logic */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
02909E772BAB6AD500710E14 /* Enumerations */,
|
||||||
023AC7FA2BAA3EB60027D064 /* Extensions */,
|
023AC7FA2BAA3EB60027D064 /* Extensions */,
|
||||||
02620B8A2BA89C3300DE7137 /* Models */,
|
02620B8A2BA89C3300DE7137 /* Models */,
|
||||||
02620B872BA89C0700DE7137 /* View Models */,
|
02620B872BA89C0700DE7137 /* View Models */,
|
||||||
@ -123,7 +134,7 @@
|
|||||||
02620B872BA89C0700DE7137 /* View Models */ = {
|
02620B872BA89C0700DE7137 /* View Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */,
|
02620B8B2BA89C9A00DE7137 /* FeedListViewModel.swift */,
|
||||||
);
|
);
|
||||||
path = "View Models";
|
path = "View Models";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -153,10 +164,19 @@
|
|||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
02909E772BAB6AD500710E14 /* Enumerations */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
02909E782BAB6B0200710E14 /* FilterOption.swift */,
|
||||||
|
);
|
||||||
|
path = Enumerations;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
02A6DA2F2BA591C000B943E2 /* Bundle */ = {
|
02A6DA2F2BA591C000B943E2 /* Bundle */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
02DC7F912BA51793000EEEBE /* ReviewsFeed.h */,
|
02DC7F912BA51793000EEEBE /* ReviewsFeed.h */,
|
||||||
|
02DA924B2BAAE3E500C47985 /* Resources */,
|
||||||
02DC7FB02BA51B4F000EEEBE /* Sources */,
|
02DC7FB02BA51B4F000EEEBE /* Sources */,
|
||||||
);
|
);
|
||||||
path = Bundle;
|
path = Bundle;
|
||||||
@ -169,6 +189,22 @@
|
|||||||
path = Test;
|
path = Test;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
02DA924B2BAAE3E500C47985 /* Resources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
02DA924C2BAAE3ED00C47985 /* Catalogs */,
|
||||||
|
);
|
||||||
|
path = Resources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
02DA924C2BAAE3ED00C47985 /* Catalogs */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */,
|
||||||
|
);
|
||||||
|
path = Catalogs;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
02DC7F722BA4F8F0000EEEBE /* Resources */ = {
|
02DC7F722BA4F8F0000EEEBE /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -365,6 +401,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -384,12 +421,15 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */,
|
02620B8C2BA89C9A00DE7137 /* FeedListViewModel.swift in Sources */,
|
||||||
023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */,
|
023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */,
|
||||||
02DC7FAC2BA51B4C000EEEBE /* FeedItemViewController.swift in Sources */,
|
02DC7FAC2BA51B4C000EEEBE /* FeedItemViewController.swift in Sources */,
|
||||||
|
02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */,
|
||||||
|
02909E7D2BAB7FFE00710E14 /* Review+DTOs.swift in Sources */,
|
||||||
0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */,
|
0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */,
|
||||||
02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */,
|
02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */,
|
||||||
02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */,
|
02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */,
|
||||||
|
02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user