diff --git a/App/Sources/AppDelegate.swift b/App/Sources/AppDelegate.swift index 5975da1..fb1338f 100644 --- a/App/Sources/AppDelegate.swift +++ b/App/Sources/AppDelegate.swift @@ -1,6 +1,6 @@ // // AppDelegate.swift -// AppStoreReviews +// ReviewsApp // // Created by Dmitrii Ivanov on 21/07/2020. // Copyright © 2020 ING. All rights reserved. @@ -10,16 +10,27 @@ import ReviewsFeed import UIKit @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { +class AppDelegate: UIResponder { + // MARK: Properties 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 + } + +} diff --git a/Frameworks/Feed/Bundle/Resources/Catalogs/Localizable.xcstrings b/Frameworks/Feed/Bundle/Resources/Catalogs/Localizable.xcstrings new file mode 100644 index 0000000..d39c508 --- /dev/null +++ b/Frameworks/Feed/Bundle/Resources/Catalogs/Localizable.xcstrings @@ -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" +} \ No newline at end of file diff --git a/Frameworks/Feed/Bundle/Sources/Logic/Enumerations/FilterOption.swift b/Frameworks/Feed/Bundle/Sources/Logic/Enumerations/FilterOption.swift new file mode 100644 index 0000000..8accaa1 --- /dev/null +++ b/Frameworks/Feed/Bundle/Sources/Logic/Enumerations/FilterOption.swift @@ -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" + } + } + } +} diff --git a/Frameworks/Feed/Bundle/Sources/Logic/Extensions/Bundle+Constants.swift b/Frameworks/Feed/Bundle/Sources/Logic/Extensions/Bundle+Constants.swift new file mode 100644 index 0000000..051c3b7 --- /dev/null +++ b/Frameworks/Feed/Bundle/Sources/Logic/Extensions/Bundle+Constants.swift @@ -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) + +} diff --git a/Frameworks/Feed/Bundle/Sources/Logic/Extensions/Review+DTOs.swift b/Frameworks/Feed/Bundle/Sources/Logic/Extensions/Review+DTOs.swift new file mode 100644 index 0000000..9e189cd --- /dev/null +++ b/Frameworks/Feed/Bundle/Sources/Logic/Extensions/Review+DTOs.swift @@ -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 + ) + } + +} diff --git a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift new file mode 100644 index 0000000..a36237a --- /dev/null +++ b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift @@ -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]] + +} diff --git a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift deleted file mode 100644 index 6a0cf7d..0000000 --- a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift +++ /dev/null @@ -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 - } - } - - } -} diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift index 09ee7bd..ed019a8 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift @@ -7,6 +7,8 @@ // import Combine +import Foundation +import ReviewsFoundationKit import ReviewsUIKit import SwiftUI import UIKit @@ -19,6 +21,36 @@ public class FeedListViewController: UITableViewController { // MARK: Properties private var cancellables: Set = [] + // 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 public init(configuration: Configuration = .init()) { self.viewModel = .init(configuration: configuration) @@ -30,6 +62,11 @@ public class FeedListViewController: UITableViewController { fatalError("init(coder:) has not been implemented") } + // MARK: Computed + var items: [Review] { + viewModel.items + } + // MARK: UIViewController public override func viewDidLoad() { super.viewDidLoad() @@ -60,11 +97,11 @@ public class FeedListViewController: UITableViewController { cell.contentConfiguration = { if #available(iOS 16.0, *) { UIHostingConfiguration { - FeedItemCell(viewModel.items[indexPath.row]) + FeedItemCell(items[indexPath.row]) } } else { HostingConfiguration { - FeedItemCell(viewModel.items[indexPath.row]) + FeedItemCell(items[indexPath.row]) } } }() @@ -77,7 +114,7 @@ public class FeedListViewController: UITableViewController { _ tableView: UITableView, didSelectRowAt indexPath: IndexPath ) { - let details = FeedItemViewController(viewModel.items[indexPath.row]) + let details = FeedItemViewController(items[indexPath.row]) tableView.deselectRow( at: indexPath, @@ -94,15 +131,26 @@ private extension FeedListViewController { // MARK: Functions func bindViewModel() { - viewModel.$loading - .sink { loading in - print("LOADING: \(loading)") + viewModel.$filter + .receive(on: RunLoop.main) + .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) - viewModel.$loading + viewModel.$isLoading .dropFirst() .filter { $0 == false } + .removeDuplicates() .receive(on: RunLoop.main) .sink { [weak self] _ in self?.tableView.reloadData() @@ -118,7 +166,24 @@ private extension FeedListViewController { navigationController?.navigationBar.prefersLargeTitles = 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 { 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 #if DEBUG -import ReviewsFoundationKit import ReviewsiTunesKit @available(iOS 17.0, *) diff --git a/Libraries/UI/Kit/Sources/Extensions/String+Icons.swift b/Libraries/UI/Kit/Sources/Extensions/String+Icons.swift index bfe3ffc..5ecef45 100644 --- a/Libraries/UI/Kit/Sources/Extensions/String+Icons.swift +++ b/Libraries/UI/Kit/Sources/Extensions/String+Icons.swift @@ -10,9 +10,18 @@ public extension String { enum Icon { // MARK: Constants - static let questionMark = "questionmark.circle.fill" - public static let info = "info.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" } } diff --git a/Libraries/UI/Kit/Sources/Extensions/UIImage+ICons.swift b/Libraries/UI/Kit/Sources/Extensions/UIImage+ICons.swift new file mode 100644 index 0000000..ee2789d --- /dev/null +++ b/Libraries/UI/Kit/Sources/Extensions/UIImage+ICons.swift @@ -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) + + } +} diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index 7bcda2a..9402628 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -9,7 +9,11 @@ /* Begin PBXBuildFile section */ 0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0220ADA22BA90646001E6A9F /* FeedItemCell.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, ); }; }; 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, ); }; }; @@ -50,7 +54,11 @@ /* Begin PBXFileReference section */ 0220ADA22BA90646001E6A9F /* FeedItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemCell.swift; sourceTree = ""; }; 023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Constants.swift"; sourceTree = ""; }; - 02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = ""; }; + 02620B8B2BA89C9A00DE7137 /* FeedListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListViewModel.swift; sourceTree = ""; }; + 02909E782BAB6B0200710E14 /* FilterOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterOption.swift; sourceTree = ""; }; + 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Constants.swift"; sourceTree = ""; }; + 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review+DTOs.swift"; sourceTree = ""; }; + 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 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 = ""; }; 02DC7FB12BA52084000EEEBE /* Libraries */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Libraries; sourceTree = ""; }; @@ -96,7 +104,9 @@ 023AC7FA2BAA3EB60027D064 /* Extensions */ = { isa = PBXGroup; children = ( + 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */, 023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */, + 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */, ); path = Extensions; sourceTree = ""; @@ -113,6 +123,7 @@ 02620B862BA89C0000DE7137 /* Logic */ = { isa = PBXGroup; children = ( + 02909E772BAB6AD500710E14 /* Enumerations */, 023AC7FA2BAA3EB60027D064 /* Extensions */, 02620B8A2BA89C3300DE7137 /* Models */, 02620B872BA89C0700DE7137 /* View Models */, @@ -123,7 +134,7 @@ 02620B872BA89C0700DE7137 /* View Models */ = { isa = PBXGroup; children = ( - 02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */, + 02620B8B2BA89C9A00DE7137 /* FeedListViewModel.swift */, ); path = "View Models"; sourceTree = ""; @@ -153,10 +164,19 @@ path = Models; sourceTree = ""; }; + 02909E772BAB6AD500710E14 /* Enumerations */ = { + isa = PBXGroup; + children = ( + 02909E782BAB6B0200710E14 /* FilterOption.swift */, + ); + path = Enumerations; + sourceTree = ""; + }; 02A6DA2F2BA591C000B943E2 /* Bundle */ = { isa = PBXGroup; children = ( 02DC7F912BA51793000EEEBE /* ReviewsFeed.h */, + 02DA924B2BAAE3E500C47985 /* Resources */, 02DC7FB02BA51B4F000EEEBE /* Sources */, ); path = Bundle; @@ -169,6 +189,22 @@ path = Test; sourceTree = ""; }; + 02DA924B2BAAE3E500C47985 /* Resources */ = { + isa = PBXGroup; + children = ( + 02DA924C2BAAE3ED00C47985 /* Catalogs */, + ); + path = Resources; + sourceTree = ""; + }; + 02DA924C2BAAE3ED00C47985 /* Catalogs */ = { + isa = PBXGroup; + children = ( + 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */, + ); + path = Catalogs; + sourceTree = ""; + }; 02DC7F722BA4F8F0000EEEBE /* Resources */ = { isa = PBXGroup; children = ( @@ -365,6 +401,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -384,12 +421,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */, + 02620B8C2BA89C9A00DE7137 /* FeedListViewModel.swift in Sources */, 023AC7FC2BAA3EC10027D064 /* Int+Constants.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 */, 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */, 02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */, + 02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };