From 548a77cd639d9ebb972013c0287f97b0cfa7bd31 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 21:15:48 +0100 Subject: [PATCH 01/10] Defined some constants for the String+Icons and UIImage+Icons extensions in the UI library. --- .../Kit/Sources/Extensions/String+Icons.swift | 13 +++++++++++-- .../Sources/Extensions/UIImage+ICons.swift | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 Libraries/UI/Kit/Sources/Extensions/UIImage+ICons.swift 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) + + } +} -- 2.47.1 From f39801b987a43f854b307b8ca6be4b42e74a6182 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 21:17:30 +0100 Subject: [PATCH 02/10] Implemented the "module" constant for the Bundle+Constants extension in the Feed framework. --- .../Logic/Extensions/Bundle+Constants.swift | 16 ++++++++++++++++ Reviews.xcodeproj/project.pbxproj | 4 ++++ 2 files changed, 20 insertions(+) create mode 100644 Frameworks/Feed/Bundle/Sources/Logic/Extensions/Bundle+Constants.swift 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/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index 7bcda2a..c60fad7 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 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 */; }; + 02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */; }; 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, ); }; }; @@ -51,6 +52,7 @@ 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 = ""; }; + 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Constants.swift"; 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,6 +98,7 @@ 023AC7FA2BAA3EB60027D064 /* Extensions */ = { isa = PBXGroup; children = ( + 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */, 023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */, ); path = Extensions; @@ -387,6 +390,7 @@ 02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */, 023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */, 02DC7FAC2BA51B4C000EEEBE /* FeedItemViewController.swift in Sources */, + 02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */, 0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */, 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */, 02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */, -- 2.47.1 From 249281424fd89bbc6e4c9863fe1188ad0b144f88 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 21:19:17 +0100 Subject: [PATCH 03/10] Defined some English strings for the Localizable strings catalog in the Feed framework. --- .../Resources/Catalogs/Localizable.xcstrings | 114 ++++++++++++++++++ Reviews.xcodeproj/project.pbxproj | 20 +++ 2 files changed, 134 insertions(+) create mode 100644 Frameworks/Feed/Bundle/Resources/Catalogs/Localizable.xcstrings 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/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index c60fad7..bede719 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */; }; 02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */; }; 02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.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, ); }; }; @@ -53,6 +54,7 @@ 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 = ""; }; 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Constants.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 = ""; }; @@ -160,6 +162,7 @@ isa = PBXGroup; children = ( 02DC7F912BA51793000EEEBE /* ReviewsFeed.h */, + 02DA924B2BAAE3E500C47985 /* Resources */, 02DC7FB02BA51B4F000EEEBE /* Sources */, ); path = Bundle; @@ -172,6 +175,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 = ( @@ -368,6 +387,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; -- 2.47.1 From 05c4ed08d90d30e09b94410d3e02e2b2c22c16fc Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 21:20:48 +0100 Subject: [PATCH 04/10] Implemented the FilterOption enumeration in the Feed framework. --- .../Logic/Enumerations/FilterOption.swift | 102 ++++++++++++++++++ Reviews.xcodeproj/project.pbxproj | 12 +++ 2 files changed, 114 insertions(+) create mode 100644 Frameworks/Feed/Bundle/Sources/Logic/Enumerations/FilterOption.swift 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..d00a222 --- /dev/null +++ b/Frameworks/Feed/Bundle/Sources/Logic/Enumerations/FilterOption.swift @@ -0,0 +1,102 @@ +// +// 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 { + 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: - CaseIterable +extension FilterOption: CaseIterable { + + // MARK: Computed + static var allCases: [FilterOption] = [ + .only1Star, + .only2Star, + .only3Star, + .only4Star, + .only5Star + ] + +} + +// 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/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index bede719..bed4db9 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 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 */; }; + 02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E782BAB6B0200710E14 /* FilterOption.swift */; }; 02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.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, ); }; }; @@ -53,6 +54,7 @@ 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 = ""; }; + 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 = ""; }; 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; }; @@ -118,6 +120,7 @@ 02620B862BA89C0000DE7137 /* Logic */ = { isa = PBXGroup; children = ( + 02909E772BAB6AD500710E14 /* Enumerations */, 023AC7FA2BAA3EB60027D064 /* Extensions */, 02620B8A2BA89C3300DE7137 /* Models */, 02620B872BA89C0700DE7137 /* View Models */, @@ -158,6 +161,14 @@ path = Models; sourceTree = ""; }; + 02909E772BAB6AD500710E14 /* Enumerations */ = { + isa = PBXGroup; + children = ( + 02909E782BAB6B0200710E14 /* FilterOption.swift */, + ); + path = Enumerations; + sourceTree = ""; + }; 02A6DA2F2BA591C000B943E2 /* Bundle */ = { isa = PBXGroup; children = ( @@ -414,6 +425,7 @@ 0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */, 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */, 02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */, + 02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; -- 2.47.1 From 656c0ee5ff2f89371623fd772179757a57f741d8 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 21:22:18 +0100 Subject: [PATCH 05/10] Renamed the FeedViewModel view model in the Feed framework as FeedListViewModel. --- ...iewModel.swift => FeedListViewModel.swift} | 28 +++++++++++-------- Reviews.xcodeproj/project.pbxproj | 8 +++--- 2 files changed, 20 insertions(+), 16 deletions(-) rename Frameworks/Feed/Bundle/Sources/Logic/View Models/{FeedViewModel.swift => FeedListViewModel.swift} (81%) diff --git a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift similarity index 81% rename from Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift rename to Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift index 6a0cf7d..e1cbef7 100644 --- a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift +++ b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift @@ -1,5 +1,5 @@ // -// FeedViewModel.swift +// FeedListViewModel.swift // ReviewsFeed // // Created by Javier Cicchelli on 18/03/2024. @@ -19,25 +19,27 @@ extension FeedListViewController { private let configuration: Configuration // MARK: Properties - @Published var loading: Bool = false - + @Published var filter: FilterOption = .all + @Published var isFilterEnabled: Bool = false + @Published var isLoading: Bool = false + var items: [Review] = [] + lazy private var iTunesService: iTunesService = { + .init(configuration: .init(session: configuration.session)) + }() + // 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 - + isFilterEnabled = false + isLoading = items.isEmpty + do { let output = try await iTunesService.getReviews(.init( appID: configuration.appID, @@ -57,11 +59,13 @@ extension FeedListViewController { title: review.title ) } + isFilterEnabled = !items.isEmpty } catch { // TODO: handle this error gracefully. + print("ERROR: \(error.localizedDescription)") } - loading = false + isLoading = false } } diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index bed4db9..77f44ca 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* 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 */; }; 02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */; }; @@ -53,7 +53,7 @@ /* 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 = ""; }; 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; @@ -131,7 +131,7 @@ 02620B872BA89C0700DE7137 /* View Models */ = { isa = PBXGroup; children = ( - 02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */, + 02620B8B2BA89C9A00DE7137 /* FeedListViewModel.swift */, ); path = "View Models"; sourceTree = ""; @@ -418,7 +418,7 @@ 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 */, -- 2.47.1 From 799007621249a9545fd1cc84e2b9998e3822fa36 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 21:24:02 +0100 Subject: [PATCH 06/10] Implemented the filter navigation button with its respective filter menu for the FeedListViewController view controller in the Feed framework. --- .../FeedListViewController.swift | 90 +++++++++++++++++-- 1 file changed, 81 insertions(+), 9 deletions(-) diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift index 09ee7bd..fb60287 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,50 @@ public class FeedListViewController: UITableViewController { // MARK: Properties private var cancellables: Set = [] + // MARK: Outlets + private lazy var filterButton = { + let allStars = UIAction( + title: FilterOption.all.text, + image: .init(systemName: FilterOption.all.icon) + ) { [weak self] _ in + self?.viewModel.filter = .all + } + + return 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: [allStars, filterStarMenu] + ) + ) + }() + + private lazy var filterStarMenu = { + UIMenu( + options: .displayInline, + children: { + FilterOption.allCases.map { option -> UIAction in + .init(title: option.text, + image: .init(systemName: option.icon) + ) { [weak self] _ in + self?.viewModel.filter = option + } + } + }() + ) + }() + // MARK: Initialisers public init(configuration: Configuration = .init()) { self.viewModel = .init(configuration: configuration) @@ -30,6 +76,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 +111,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 +128,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 +145,18 @@ private extension FeedListViewController { // MARK: Functions func bindViewModel() { - viewModel.$loading - .sink { loading in - print("LOADING: \(loading)") + 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 +172,12 @@ 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 + ) } } @@ -151,11 +210,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, *) -- 2.47.1 From 94509e4fa3b8315dcc0fa5abe4287ea0250bfc50 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 21:26:44 +0100 Subject: [PATCH 07/10] Improved the overall implementation of the AppDelegate delegate in the App target. --- App/Sources/AppDelegate.swift | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) 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 + } + +} -- 2.47.1 From 0ae7aa85cdffd1e96be13d32f029d353733c60ef Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 21:44:06 +0100 Subject: [PATCH 08/10] Implemented the "init(_: )" initialiser function for the Review+DTOs extension in the Feed framework. --- .../Logic/Extensions/Review+DTOs.swift | 27 +++++++++++++++++++ Reviews.xcodeproj/project.pbxproj | 4 +++ 2 files changed, 31 insertions(+) create mode 100644 Frameworks/Feed/Bundle/Sources/Logic/Extensions/Review+DTOs.swift 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/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index 77f44ca..9402628 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 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 */; }; @@ -56,6 +57,7 @@ 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 = ""; }; @@ -104,6 +106,7 @@ children = ( 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */, 023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */, + 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */, ); path = Extensions; sourceTree = ""; @@ -422,6 +425,7 @@ 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 */, -- 2.47.1 From 8d84f15b40225e2f5a9e5084d5a5fc54303ec23a Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 22:18:17 +0100 Subject: [PATCH 09/10] Improved the conformance to the CaseIterable protocol for the FilterOption enumeration in the Feed framework. --- .../Logic/Enumerations/FilterOption.swift | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/Frameworks/Feed/Bundle/Sources/Logic/Enumerations/FilterOption.swift b/Frameworks/Feed/Bundle/Sources/Logic/Enumerations/FilterOption.swift index d00a222..8accaa1 100644 --- a/Frameworks/Feed/Bundle/Sources/Logic/Enumerations/FilterOption.swift +++ b/Frameworks/Feed/Bundle/Sources/Logic/Enumerations/FilterOption.swift @@ -11,7 +11,7 @@ import ReviewsFoundationKit import ReviewsUIKit import UIKit -enum FilterOption: Int { +enum FilterOption: Int, CaseIterable { case all = 0 case only1Star case only2Star @@ -71,20 +71,6 @@ extension FilterOption { } -// MARK: - CaseIterable -extension FilterOption: CaseIterable { - - // MARK: Computed - static var allCases: [FilterOption] = [ - .only1Star, - .only2Star, - .only3Star, - .only4Star, - .only5Star - ] - -} - // MARK: - String+Constants private extension String { enum Key { -- 2.47.1 From e9590990eae43c830b2c6a91b5c59146fb29c9b3 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 22:22:14 +0100 Subject: [PATCH 10/10] Implemented the items filtering by star rating for the FeedListViewController view controller and its view model in the Feed framework. --- .../Logic/View Models/FeedListViewModel.swift | 41 ++++++++++---- .../FeedListViewController.swift | 56 ++++++++++--------- 2 files changed, 60 insertions(+), 37 deletions(-) diff --git a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift index e1cbef7..a36237a 100644 --- a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift +++ b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedListViewModel.swift @@ -7,6 +7,7 @@ // import Foundation +import ReviewsFilterKit import ReviewsiTunesKit extension FeedListViewController { @@ -21,10 +22,14 @@ extension FeedListViewController { // 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)) }() @@ -46,19 +51,13 @@ extension FeedListViewController { 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 - ) + 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. @@ -68,6 +67,24 @@ extension FeedListViewController { 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/UI/View Controllers/FeedListViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift index fb60287..ed019a8 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift @@ -23,14 +23,7 @@ public class FeedListViewController: UITableViewController { // MARK: Outlets private lazy var filterButton = { - let allStars = UIAction( - title: FilterOption.all.text, - image: .init(systemName: FilterOption.all.icon) - ) { [weak self] _ in - self?.viewModel.filter = .all - } - - return UIBarButtonItem( + UIBarButtonItem( title: NSLocalizedString( .Key.Navigation.Button.filter, bundle: .module, @@ -45,26 +38,19 @@ public class FeedListViewController: UITableViewController { comment: .empty ), image: UIImage.Icon.star, - children: [allStars, filterStarMenu] + children: { + FilterOption.allCases.map { option -> UIAction in + .init(title: option.text, + image: .init(systemName: option.icon) + ) { [weak self] _ in + self?.viewModel.filter(by: option) + } + } + }() ) ) }() - - private lazy var filterStarMenu = { - UIMenu( - options: .displayInline, - children: { - FilterOption.allCases.map { option -> UIAction in - .init(title: option.text, - image: .init(systemName: option.icon) - ) { [weak self] _ in - self?.viewModel.filter = option - } - } - }() - ) - }() - + // MARK: Initialisers public init(configuration: Configuration = .init()) { self.viewModel = .init(configuration: configuration) @@ -145,6 +131,14 @@ private extension FeedListViewController { // MARK: Functions func bindViewModel() { + 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) @@ -180,6 +174,18 @@ private extension FeedListViewController { ) } + func updateFilterMenu(_ option: FilterOption) { + filterButton + .menu? + .children + .compactMap { $0 as? UIAction } + .forEach { action in + action.state = action.title == option.text + ? .on + : .off + } + } + } // MARK: - Configuration -- 2.47.1