From 07ca1605e2178932827429e871308e81448f5143 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 19 Mar 2024 10:24:52 +0100 Subject: [PATCH 01/12] Renamed the FeedItem component in the Feed framework as FeedItemCell. --- .../{FeedItem.swift => Cells/FeedItemCell.swift} | 9 +++++---- .../UI/View Controllers/FeedViewController.swift | 4 ++-- Reviews.xcodeproj/project.pbxproj | 16 ++++++++++++---- 3 files changed, 19 insertions(+), 10 deletions(-) rename Frameworks/Feed/Bundle/Sources/UI/Components/{FeedItem.swift => Cells/FeedItemCell.swift} (94%) diff --git a/Frameworks/Feed/Bundle/Sources/UI/Components/FeedItem.swift b/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift similarity index 94% rename from Frameworks/Feed/Bundle/Sources/UI/Components/FeedItem.swift rename to Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift index a472ed0..7225169 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/Components/FeedItem.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift @@ -1,5 +1,5 @@ // -// FeedItem.swift +// FeedItemCell.swift // ReviewsFeed // // Created by Javier Cicchelli on 19/03/2024. @@ -8,7 +8,7 @@ import SwiftUI -struct FeedItem: View { +struct FeedItemCell: View { // MARK: Constants private let item: Review @@ -76,8 +76,8 @@ struct FeedItem: View { } // MARK: - Previews -#Preview("Feed Item") { - FeedItem(.init( +#Preview("Feed Item Cell") { + FeedItemCell(.init( author: "Some author name here...", comment: "Some review comment here...", id: 0, @@ -87,4 +87,5 @@ struct FeedItem: View { ), title: "Some review title here..." )) + .padding(.horizontal) } diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift index dd91f38..d1c86bd 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift @@ -63,11 +63,11 @@ public class FeedViewController: UITableViewController { cell.contentConfiguration = { if #available(iOS 16.0, *) { UIHostingConfiguration { - FeedItem(viewModel.items[indexPath.row]) + FeedItemCell(viewModel.items[indexPath.row]) } } else { HostingConfiguration { - FeedItem(viewModel.items[indexPath.row]) + FeedItemCell(viewModel.items[indexPath.row]) } } }() diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index 62a1186..b9c0791 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 0220ADA32BA90646001E6A9F /* FeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0220ADA22BA90646001E6A9F /* FeedItem.swift */; }; + 0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0220ADA22BA90646001E6A9F /* FeedItemCell.swift */; }; 02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02620B8B2BA89C9A00DE7137 /* FeedViewModel.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 */; }; @@ -47,7 +47,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0220ADA22BA90646001E6A9F /* FeedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItem.swift; sourceTree = ""; }; + 0220ADA22BA90646001E6A9F /* FeedItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemCell.swift; sourceTree = ""; }; 02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.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 = ""; }; @@ -83,6 +83,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0220ADA72BA98F8B001E6A9F /* Cells */ = { + isa = PBXGroup; + children = ( + 0220ADA22BA90646001E6A9F /* FeedItemCell.swift */, + ); + path = Cells; + sourceTree = ""; + }; 02620B852BA89BF900DE7137 /* UI */ = { isa = PBXGroup; children = ( @@ -121,7 +129,7 @@ 02620B892BA89C2400DE7137 /* Components */ = { isa = PBXGroup; children = ( - 0220ADA22BA90646001E6A9F /* FeedItem.swift */, + 0220ADA72BA98F8B001E6A9F /* Cells */, ); path = Components; sourceTree = ""; @@ -367,7 +375,7 @@ files = ( 02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */, 02DC7FAC2BA51B4C000EEEBE /* DetailsViewController.swift in Sources */, - 0220ADA32BA90646001E6A9F /* FeedItem.swift in Sources */, + 0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */, 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */, 02DC7FAE2BA51B4C000EEEBE /* FeedViewController.swift in Sources */, ); -- 2.47.1 From 05b5b8dc50558d4d99d719fffce54db98f8a82d0 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 19 Mar 2024 11:53:02 +0100 Subject: [PATCH 02/12] Improved the overall implementation of the DetailsViewController view controller in the Feed framework. --- .../DetailsViewController.swift | 150 ++++++++++++------ .../View Controllers/FeedViewController.swift | 2 +- 2 files changed, 99 insertions(+), 53 deletions(-) diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/DetailsViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/DetailsViewController.swift index b27f16d..f1f8708 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/DetailsViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/DetailsViewController.swift @@ -1,5 +1,5 @@ // -// AppDelegate.swift +// DetailsViewController.swift // ReviewsFeed // // Created by Dmitrii Ivanov on 21/07/2020. @@ -8,17 +8,60 @@ import UIKit -class DetailsViewController: UIViewController { - - private let review: Review - - private var titleLabel = UILabel() - private var authorLabel = UILabel() - private var contentLabel = UILabel() - private var ratingVersionLabel = UILabel() +final class DetailsViewController: UIViewController { - init(review: Review) { - self.review = review + // MARK: Constants + private let item: Review + + // MARK: Outlets + private lazy var titleLabel = { + let label = UILabel() + + label.font = UIFont.preferredFont(forTextStyle: .title3) + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + label.text = item.title + + return label + }() + + private lazy var authorLabel = { + let label = UILabel() + + label.font = UIFont.preferredFont(forTextStyle: .headline) + label.numberOfLines = 1 + label.translatesAutoresizingMaskIntoConstraints = false + label.text = item.author + + return label + }() + + private lazy var commentLabel = { + let label = UILabel() + + label.font = UIFont.preferredFont(forTextStyle: .body) + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + label.text = item.comment + + return label + }() + + private lazy var ratingVersionLabel = { + let label = UILabel() + + label.font = UIFont.preferredFont(forTextStyle: .subheadline) + label.numberOfLines = 1 + label.translatesAutoresizingMaskIntoConstraints = false + label.text = item.rating.appVersion + + return label + }() + + // MARK: Initialisers + init(_ item: Review) { + self.item = item + super.init(nibName: nil, bundle: nil) } @@ -26,54 +69,57 @@ class DetailsViewController: UIViewController { fatalError("init(coder:) has not been implemented") } + // MARK: UIViewController override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = UIColor.white - setupViews() - } - func setupViews() { - titleLabel.translatesAutoresizingMaskIntoConstraints = false - authorLabel.translatesAutoresizingMaskIntoConstraints = false - contentLabel.translatesAutoresizingMaskIntoConstraints = false - ratingVersionLabel.translatesAutoresizingMaskIntoConstraints = false + setupView() + } + +} + +// MARK: - Helpers +private extension DetailsViewController { + + // MARK: Functions + func setupView() { + view.backgroundColor = .white view.addSubview(ratingVersionLabel) view.addSubview(authorLabel) view.addSubview(titleLabel) - view.addSubview(contentLabel) + view.addSubview(commentLabel) - ratingVersionLabel.text = review.rating.appVersion - ratingVersionLabel.font = UIFont.italicSystemFont(ofSize: 18) - - authorLabel.text = review.author - authorLabel.font = UIFont.systemFont(ofSize: 18) - - titleLabel.text = review.title - titleLabel.numberOfLines = 0 - titleLabel.font = UIFont.boldSystemFont(ofSize: 22) - - contentLabel.text = review.comment - contentLabel.numberOfLines = 0 - - ratingVersionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8).isActive = true - ratingVersionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8).isActive = true - ratingVersionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true - ratingVersionLabel.heightAnchor.constraint(equalToConstant: 24).isActive = true - - authorLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8).isActive = true - authorLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8).isActive = true - authorLabel.topAnchor.constraint(equalTo: ratingVersionLabel.bottomAnchor, constant: 8).isActive = true - authorLabel.heightAnchor.constraint(equalToConstant: 24).isActive = true - - titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8).isActive = true - titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8).isActive = true - titleLabel.topAnchor.constraint(equalTo: authorLabel.bottomAnchor, constant: 8).isActive = true - titleLabel.heightAnchor.constraint(lessThanOrEqualToConstant: 72).isActive = true - - contentLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8).isActive = true - contentLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8).isActive = true - contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8).isActive = true - contentLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 24).isActive = true + NSLayoutConstraint.activate([ + authorLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), + authorLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + authorLabel.topAnchor.constraint(equalTo: ratingVersionLabel.bottomAnchor, constant: 8), + authorLabel.heightAnchor.constraint(equalToConstant: 24), + commentLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), + commentLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + commentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + commentLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 24), + ratingVersionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), + ratingVersionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + ratingVersionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), + ratingVersionLabel.heightAnchor.constraint(equalToConstant: 24), + titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), + titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), + titleLabel.topAnchor.constraint(equalTo: authorLabel.bottomAnchor, constant: 8), + titleLabel.heightAnchor.constraint(lessThanOrEqualToConstant: 72), + ]) } + +} + +// MARK: - Previews +@available(iOS 17.0, *) +#Preview("Details View Controller with a review") { + UINavigationController(rootViewController: DetailsViewController(.init( + author: "Some author name here...", + comment: "Some long, explanatory review comment goes here...", + id: 1, + rating: .init(stars: 3, appVersion: "v1.0.0"), + title: "Some review title goes here..." + ))) } diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift index d1c86bd..0e5e144 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift @@ -80,7 +80,7 @@ public class FeedViewController: UITableViewController { _ tableView: UITableView, didSelectRowAt indexPath: IndexPath ) { - let details = DetailsViewController(review: viewModel.items[indexPath.row]) + let details = DetailsViewController(viewModel.items[indexPath.row]) tableView.deselectRow( at: indexPath, -- 2.47.1 From 0512f2fa42a13214fad97a4a3c1fbae0f01402dd Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 19 Mar 2024 19:12:25 +0100 Subject: [PATCH 03/12] Implemented a basic navigation bar for the FeedViewController view controller in the Feed framework. --- .../View Controllers/FeedViewController.swift | 58 ++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift index 0e5e144..9163c72 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift @@ -33,12 +33,9 @@ public class FeedViewController: UITableViewController { // MARK: UIViewController public override func viewDidLoad() { super.viewDidLoad() - - tableView.register( - UITableViewCell.self, - forCellReuseIdentifier: .Cell.feedItem - ) + setNavigationBar() + registerTableCells() bindViewModel() viewModel.fetch() @@ -113,6 +110,17 @@ private extension FeedViewController { .store(in: &cancellables) } + func registerTableCells() { + tableView.register(UITableViewCell.self, forCellReuseIdentifier: .Cell.feedItem) + } + + func setNavigationBar() { + navigationController?.navigationBar.prefersLargeTitles = true + navigationController?.navigationBar.isTranslucent = true + + navigationItem.title = "Latest reviews" + } + } // MARK: - Configuration @@ -150,6 +158,15 @@ private extension String { import ReviewsFoundationKit import ReviewsiTunesKit +@available(iOS 17.0, *) +#Preview("Feed View Controller loading reviews") { + MockURLProtocol.response = .init(statusCode: 200) + + return UINavigationController( + rootViewController: FeedViewController(configuration: .init(session: .mock)) + ) +} + @available(iOS 17.0, *) #Preview("Feed View Controller with few reviews") { MockURLProtocol.response = .init( @@ -182,10 +199,30 @@ import ReviewsiTunesKit version: "v1.0.0", updated: .init() ), + .init( + id: 4, + author: "Some author name #4 here", + title: "Some review title #4 goes here...", + content: "Some long, explanatory review comment #4 goes here...", + rating: 4, + version: "v1.0.0", + updated: .init() + ), + .init( + id: 5, + author: "Some author name #5 here", + title: "Some review title #5 goes here...", + content: "Some long, explanatory review comment #5 goes here...", + rating: 2, + version: "v1.0.0", + updated: .init() + ), ]) ) - return FeedViewController(configuration: .init(session: .mock)) + return UINavigationController( + rootViewController: FeedViewController(configuration: .init(session: .mock)) + ) } @available(iOS 17.0, *) @@ -195,6 +232,13 @@ import ReviewsiTunesKit object: Feed(entries: []) ) - return FeedViewController(configuration: .init(session: .mock)) + return UINavigationController( + rootViewController: FeedViewController(configuration: .init(session: .mock)) + ) +} + +@available(iOS 17.0, *) +#Preview("Feed View Controller with live reviews") { + UINavigationController(rootViewController: FeedViewController()) } #endif -- 2.47.1 From 461adc1be4d4ed624484c04fe8a60beb4d769cc5 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 19 Mar 2024 19:15:55 +0100 Subject: [PATCH 04/12] Renamed the FeedViewController view controller in the Feed framework as FeedListViewController. --- App/Sources/AppDelegate.swift | 2 +- .../Logic/View Models/FeedViewModel.swift | 4 +-- ...ler.swift => FeedListViewController.swift} | 26 +++++++++---------- Reviews.xcodeproj/project.pbxproj | 8 +++--- 4 files changed, 20 insertions(+), 20 deletions(-) rename Frameworks/Feed/Bundle/Sources/UI/View Controllers/{FeedViewController.swift => FeedListViewController.swift} (89%) diff --git a/App/Sources/AppDelegate.swift b/App/Sources/AppDelegate.swift index 13338b5..5975da1 100644 --- a/App/Sources/AppDelegate.swift +++ b/App/Sources/AppDelegate.swift @@ -16,7 +16,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) - let viewController = FeedViewController() + let viewController = FeedListViewController() window?.rootViewController = UINavigationController(rootViewController: viewController) window?.makeKeyAndVisible() return true diff --git a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift index 8198e6d..6a0cf7d 100644 --- a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift +++ b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift @@ -9,11 +9,11 @@ import Foundation import ReviewsiTunesKit -extension FeedViewController { +extension FeedListViewController { final class ViewModel: ObservableObject { // MARK: Type aliases - typealias Configuration = FeedViewController.Configuration + typealias Configuration = FeedListViewController.Configuration // MARK: Constants private let configuration: Configuration diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift similarity index 89% rename from Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift rename to Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift index 9163c72..1295c03 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift @@ -1,5 +1,5 @@ // -// AppDelegate.swift +// FeedListViewController.swift // ReviewsFeed // // Created by Dmitrii Ivanov on 21/07/2020. @@ -11,7 +11,7 @@ import ReviewsUIKit import SwiftUI import UIKit -public class FeedViewController: UITableViewController { +public class FeedListViewController: UITableViewController { // MARK: Constants private let viewModel: ViewModel @@ -90,7 +90,7 @@ public class FeedViewController: UITableViewController { } // MARK: - Helpers -private extension FeedViewController { +private extension FeedListViewController { // MARK: Functions func bindViewModel() { @@ -124,7 +124,7 @@ private extension FeedViewController { } // MARK: - Configuration -extension FeedViewController { +extension FeedListViewController { public struct Configuration { // MARK: Constants @@ -159,16 +159,16 @@ import ReviewsFoundationKit import ReviewsiTunesKit @available(iOS 17.0, *) -#Preview("Feed View Controller loading reviews") { +#Preview("Feed List loading reviews") { MockURLProtocol.response = .init(statusCode: 200) return UINavigationController( - rootViewController: FeedViewController(configuration: .init(session: .mock)) - ) + rootViewController: FeedListViewController(configuration: .init(session: .mock)) + ) } @available(iOS 17.0, *) -#Preview("Feed View Controller with few reviews") { +#Preview("Feed List with few reviews") { MockURLProtocol.response = .init( statusCode: 200, object: Feed(entries: [ @@ -221,24 +221,24 @@ import ReviewsiTunesKit ) return UINavigationController( - rootViewController: FeedViewController(configuration: .init(session: .mock)) + rootViewController: FeedListViewController(configuration: .init(session: .mock)) ) } @available(iOS 17.0, *) -#Preview("Feed View Controller with no reviews") { +#Preview("Feed List with no reviews") { MockURLProtocol.response = .init( statusCode: 200, object: Feed(entries: []) ) return UINavigationController( - rootViewController: FeedViewController(configuration: .init(session: .mock)) + rootViewController: FeedListViewController(configuration: .init(session: .mock)) ) } @available(iOS 17.0, *) -#Preview("Feed View Controller with live reviews") { - UINavigationController(rootViewController: FeedViewController()) +#Preview("Feed List with live reviews") { + UINavigationController(rootViewController: FeedListViewController()) } #endif diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index b9c0791..cfbedbc 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -13,7 +13,7 @@ 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, ); }; }; 02DC7FAC2BA51B4C000EEEBE /* DetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */; }; - 02DC7FAE2BA51B4C000EEEBE /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */; }; + 02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD12F24C6EE64004E2EE1 /* FeedListViewController.swift */; }; 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13124C6EE64004E2EE1 /* Review.swift */; }; 02DC7FB32BA52518000EEEBE /* ReviewsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 02DC7FB22BA52518000EEEBE /* ReviewsKit */; }; 02DC7FB52BA52520000EEEBE /* ReviewsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 02DC7FB42BA52520000EEEBE /* ReviewsKit */; }; @@ -57,7 +57,7 @@ 345AD12424C6EDDC004E2EE1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 345AD12724C6EDDC004E2EE1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 345AD12924C6EDDC004E2EE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = ""; }; + 345AD12F24C6EE64004E2EE1 /* FeedListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedListViewController.swift; sourceTree = ""; }; 345AD13124C6EE64004E2EE1 /* Review.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Review.swift; sourceTree = ""; }; 345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailsViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -121,7 +121,7 @@ isa = PBXGroup; children = ( 345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */, - 345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */, + 345AD12F24C6EE64004E2EE1 /* FeedListViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -377,7 +377,7 @@ 02DC7FAC2BA51B4C000EEEBE /* DetailsViewController.swift in Sources */, 0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */, 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */, - 02DC7FAE2BA51B4C000EEEBE /* FeedViewController.swift in Sources */, + 02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; -- 2.47.1 From dbf71cc50d4834e636aa7b5dce31b03013d20f84 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 19 Mar 2024 19:22:34 +0100 Subject: [PATCH 05/12] Renamed the DetailsViewController view controller in the Feed framework as FeedItemViewController. --- ...ler.swift => FeedItemViewController.swift} | 39 ++++++++++++------- .../FeedListViewController.swift | 2 +- Reviews.xcodeproj/project.pbxproj | 8 ++-- 3 files changed, 30 insertions(+), 19 deletions(-) rename Frameworks/Feed/Bundle/Sources/UI/View Controllers/{DetailsViewController.swift => FeedItemViewController.swift} (86%) diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/DetailsViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedItemViewController.swift similarity index 86% rename from Frameworks/Feed/Bundle/Sources/UI/View Controllers/DetailsViewController.swift rename to Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedItemViewController.swift index f1f8708..621471e 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/DetailsViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedItemViewController.swift @@ -1,5 +1,5 @@ // -// DetailsViewController.swift +// FeedItemViewController.swift // ReviewsFeed // // Created by Dmitrii Ivanov on 21/07/2020. @@ -8,7 +8,7 @@ import UIKit -final class DetailsViewController: UIViewController { +final class FeedItemViewController: UIViewController { // MARK: Constants private let item: Review @@ -73,23 +73,18 @@ final class DetailsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - setupView() + setView() + setNavigationBar() + setLayout() } } // MARK: - Helpers -private extension DetailsViewController { +private extension FeedItemViewController { // MARK: Functions - func setupView() { - view.backgroundColor = .white - - view.addSubview(ratingVersionLabel) - view.addSubview(authorLabel) - view.addSubview(titleLabel) - view.addSubview(commentLabel) - + func setLayout() { NSLayoutConstraint.activate([ authorLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), authorLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), @@ -110,12 +105,28 @@ private extension DetailsViewController { ]) } + func setNavigationBar() { + navigationController?.navigationBar.prefersLargeTitles = true + navigationController?.navigationBar.isTranslucent = true + + navigationItem.title = "#\(String(item.id))" + } + + func setView() { + view.backgroundColor = .white + + view.addSubview(ratingVersionLabel) + view.addSubview(authorLabel) + view.addSubview(titleLabel) + view.addSubview(commentLabel) + } + } // MARK: - Previews @available(iOS 17.0, *) -#Preview("Details View Controller with a review") { - UINavigationController(rootViewController: DetailsViewController(.init( +#Preview("Feed Item with a review") { + UINavigationController(rootViewController: FeedItemViewController(.init( author: "Some author name here...", comment: "Some long, explanatory review comment goes here...", id: 1, diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift index 1295c03..09ee7bd 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedListViewController.swift @@ -77,7 +77,7 @@ public class FeedListViewController: UITableViewController { _ tableView: UITableView, didSelectRowAt indexPath: IndexPath ) { - let details = DetailsViewController(viewModel.items[indexPath.row]) + let details = FeedItemViewController(viewModel.items[indexPath.row]) tableView.deselectRow( at: indexPath, diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index cfbedbc..4eadb43 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -12,7 +12,7 @@ 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, ); }; }; - 02DC7FAC2BA51B4C000EEEBE /* DetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */; }; + 02DC7FAC2BA51B4C000EEEBE /* FeedItemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13224C6EE64004E2EE1 /* FeedItemViewController.swift */; }; 02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD12F24C6EE64004E2EE1 /* FeedListViewController.swift */; }; 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13124C6EE64004E2EE1 /* Review.swift */; }; 02DC7FB32BA52518000EEEBE /* ReviewsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 02DC7FB22BA52518000EEEBE /* ReviewsKit */; }; @@ -59,7 +59,7 @@ 345AD12924C6EDDC004E2EE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 345AD12F24C6EE64004E2EE1 /* FeedListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedListViewController.swift; sourceTree = ""; }; 345AD13124C6EE64004E2EE1 /* Review.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Review.swift; sourceTree = ""; }; - 345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailsViewController.swift; sourceTree = ""; }; + 345AD13224C6EE64004E2EE1 /* FeedItemViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedItemViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -120,7 +120,7 @@ 02620B882BA89C1000DE7137 /* View Controllers */ = { isa = PBXGroup; children = ( - 345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */, + 345AD13224C6EE64004E2EE1 /* FeedItemViewController.swift */, 345AD12F24C6EE64004E2EE1 /* FeedListViewController.swift */, ); path = "View Controllers"; @@ -374,7 +374,7 @@ buildActionMask = 2147483647; files = ( 02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */, - 02DC7FAC2BA51B4C000EEEBE /* DetailsViewController.swift in Sources */, + 02DC7FAC2BA51B4C000EEEBE /* FeedItemViewController.swift in Sources */, 0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */, 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */, 02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */, -- 2.47.1 From 2313afb8c41c00b096bc2c229c7379a97295e7f3 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 00:48:27 +0100 Subject: [PATCH 06/12] Implemented the StarRating component in the UI library. --- .../Kit/Sources/Components/StarRating.swift | 81 +++++++++++++++++++ .../HostingConfiguration.swift | 0 2 files changed, 81 insertions(+) create mode 100644 Libraries/UI/Kit/Sources/Components/StarRating.swift rename Libraries/UI/Kit/Sources/{Components => Configurations}/HostingConfiguration.swift (100%) diff --git a/Libraries/UI/Kit/Sources/Components/StarRating.swift b/Libraries/UI/Kit/Sources/Components/StarRating.swift new file mode 100644 index 0000000..1c3b8e8 --- /dev/null +++ b/Libraries/UI/Kit/Sources/Components/StarRating.swift @@ -0,0 +1,81 @@ +// +// StarRating.swift +// Feed +// +// Created by Javier Cicchelli on 19/03/2024. +// Copyright © 2024 Röck+Cöde. All rights reserved. +// + +import SwiftUI + +public struct StarRating: View { + + // MARK: Constants + private let rating: Int + private let total: Int + + // MARK: Initialisers + public init( + _ rating: Int, + of total: Int + ) { + self.rating = rating + self.total = total + } + + // MARK: Body + public var body: some View { + HStack { + ForEach( + 1...total, + id: \.self + ) { index in + if #available(iOS 15.0, *) { + Image.Icon.star + .symbolVariant(symbolVariant(for: index)) + } else { + image(for: index) + } + } + } + } + +} + +// MARK: - Helpers +private extension StarRating { + + // MARK: Functions + func image(for index: Int) -> Image { + index <= rating + ? .Icon.starFill + : .Icon.star + } + + @available(iOS 15.0, *) + func symbolVariant(for index: Int) -> SymbolVariants { + index <= rating + ? .fill + : .none + } + +} + +// MARK: - Image+Constants +private extension Image { + enum Icon { + static let star: Image = .init(systemName: "star") + static let starFill: Image = .init(systemName: "star.fill") + } +} + +// MARK: - Previews +#Preview("Star Rating") { + VStack(spacing: 8) { + StarRating(1, of: 5) + StarRating(2, of: 5) + StarRating(3, of: 5) + StarRating(4, of: 5) + StarRating(5, of: 5) + } +} diff --git a/Libraries/UI/Kit/Sources/Components/HostingConfiguration.swift b/Libraries/UI/Kit/Sources/Configurations/HostingConfiguration.swift similarity index 100% rename from Libraries/UI/Kit/Sources/Components/HostingConfiguration.swift rename to Libraries/UI/Kit/Sources/Configurations/HostingConfiguration.swift -- 2.47.1 From 32c0ef500d6200d7b328ccb4e1a1a2c165dcc0a4 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 00:49:40 +0100 Subject: [PATCH 07/12] Integrated the StarRating component from the UI library into the FeedItemCell component in the Feed framework. --- .../Sources/Logic/Extensions/Int+Constants.swift | 16 ++++++++++++++++ .../UI/Components/Cells/FeedItemCell.swift | 13 +++++-------- Reviews.xcodeproj/project.pbxproj | 12 ++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 Frameworks/Feed/Bundle/Sources/Logic/Extensions/Int+Constants.swift diff --git a/Frameworks/Feed/Bundle/Sources/Logic/Extensions/Int+Constants.swift b/Frameworks/Feed/Bundle/Sources/Logic/Extensions/Int+Constants.swift new file mode 100644 index 0000000..1c6f9b2 --- /dev/null +++ b/Frameworks/Feed/Bundle/Sources/Logic/Extensions/Int+Constants.swift @@ -0,0 +1,16 @@ +// +// Int+Constants.swift +// ReviewsFeed +// +// Created by Javier Cicchelli on 19/03/2024. +// Copyright © 2024 Röck+Cöde. All rights reserved. +// + +extension Int { + + // MARK: Constants + enum Rating { + static let total: Int = 5 + } + +} diff --git a/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift b/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift index 7225169..e37339c 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift @@ -6,6 +6,7 @@ // Copyright © 2024 Röck+Cöde. All rights reserved. // +import ReviewsUIKit import SwiftUI struct FeedItemCell: View { @@ -48,14 +49,10 @@ struct FeedItemCell: View { .multilineTextAlignment(.leading) HStack(alignment: .bottom) { - ForEach(1...5, id: \.self) { index in - if #available(iOS 15.0, *) { - Image(systemName: "star") - .symbolVariant(index <= item.rating.stars ? .fill : .none) - } else { - Image(systemName: index <= item.rating.stars ? "star.fill" : "star") - } - } + StarRating( + item.rating.stars, + of: .Rating.total + ) Spacer() diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index 4eadb43..7bcda2a 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -8,6 +8,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 */; }; 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 */; }; @@ -48,6 +49,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 = ""; }; 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 = ""; }; @@ -91,6 +93,14 @@ path = Cells; sourceTree = ""; }; + 023AC7FA2BAA3EB60027D064 /* Extensions */ = { + isa = PBXGroup; + children = ( + 023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 02620B852BA89BF900DE7137 /* UI */ = { isa = PBXGroup; children = ( @@ -103,6 +113,7 @@ 02620B862BA89C0000DE7137 /* Logic */ = { isa = PBXGroup; children = ( + 023AC7FA2BAA3EB60027D064 /* Extensions */, 02620B8A2BA89C3300DE7137 /* Models */, 02620B872BA89C0700DE7137 /* View Models */, ); @@ -374,6 +385,7 @@ buildActionMask = 2147483647; files = ( 02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */, + 023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */, 02DC7FAC2BA51B4C000EEEBE /* FeedItemViewController.swift in Sources */, 0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */, 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */, -- 2.47.1 From 5b1ca622b7875769b8fdfedc57479d2d85f7e07a Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 01:34:20 +0100 Subject: [PATCH 08/12] Defined the "info" and "person" static constants for the Image+Icons, String+Icons, and UIImage+Icons extensions for the UI library. --- .../Kit/Sources/Components/StarRating.swift | 8 +++----- .../Kit/Sources/Extensions/Image+Icons.swift | 19 +++++++++++++++++++ .../Kit/Sources/Extensions/String+Icons.swift | 17 +++++++++++++++++ .../Sources/Extensions/UIImage+Icons.swift | 19 +++++++++++++++++++ 4 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 Libraries/UI/Kit/Sources/Extensions/Image+Icons.swift create mode 100644 Libraries/UI/Kit/Sources/Extensions/String+Icons.swift create mode 100644 Libraries/UI/Kit/Sources/Extensions/UIImage+Icons.swift diff --git a/Libraries/UI/Kit/Sources/Components/StarRating.swift b/Libraries/UI/Kit/Sources/Components/StarRating.swift index 1c3b8e8..70f41c7 100644 --- a/Libraries/UI/Kit/Sources/Components/StarRating.swift +++ b/Libraries/UI/Kit/Sources/Components/StarRating.swift @@ -62,11 +62,9 @@ private extension StarRating { } // MARK: - Image+Constants -private extension Image { - enum Icon { - static let star: Image = .init(systemName: "star") - static let starFill: Image = .init(systemName: "star.fill") - } +private extension Image.Icon { + static let star: Image = .init(systemName: "star") + static let starFill: Image = .init(systemName: "star.fill") } // MARK: - Previews diff --git a/Libraries/UI/Kit/Sources/Extensions/Image+Icons.swift b/Libraries/UI/Kit/Sources/Extensions/Image+Icons.swift new file mode 100644 index 0000000..74ec6a3 --- /dev/null +++ b/Libraries/UI/Kit/Sources/Extensions/Image+Icons.swift @@ -0,0 +1,19 @@ +// +// Image+Icons.swift +// ReviewsUIKit +// +// Created by Javier Cicchelli on 20/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import SwiftUI + +public extension Image { + enum Icon { + + // MARK: Constants + public static let info = Image(systemName: .Icon.info) + public static let person = Image(systemName: .Icon.person) + + } +} diff --git a/Libraries/UI/Kit/Sources/Extensions/String+Icons.swift b/Libraries/UI/Kit/Sources/Extensions/String+Icons.swift new file mode 100644 index 0000000..b535928 --- /dev/null +++ b/Libraries/UI/Kit/Sources/Extensions/String+Icons.swift @@ -0,0 +1,17 @@ +// +// String+Icons.swift +// ReviewsUIKit +// +// Created by Javier Cicchelli on 20/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +extension String { + enum Icon { + + // MARK: Constants + static let info = "info.circle.fill" + static let person = "person.crop.circle.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..93cbb37 --- /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 info = UIImage(systemName: .Icon.info) + public static let person = UIImage(systemName: .Icon.person) + + } +} -- 2.47.1 From 2da8f417c4dacd75ee316851375aeff8c0c29c94 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 01:34:54 +0100 Subject: [PATCH 09/12] Improved the overall layout of the FeedItemCell component in the Feed framework. --- .../UI/Components/Cells/FeedItemCell.swift | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift b/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift index e37339c..7b37001 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift @@ -29,11 +29,12 @@ struct FeedItemCell: View { alignment: .bottom, spacing: 8 ) { - Image(systemName: "person.crop.circle") + Image.Icon.person Text(item.author) - .font(.subheadline) + .font(.body) } + .foregroundColor(.secondary) VStack(spacing: 8) { Text(item.title) @@ -48,24 +49,26 @@ struct FeedItemCell: View { } .multilineTextAlignment(.leading) - HStack(alignment: .bottom) { + HStack( + alignment: .center, + spacing: 32 + ) { StarRating( item.rating.stars, of: .Rating.total ) - - Spacer() HStack( - alignment: .bottom, - spacing: 4 + alignment: .center, + spacing: 8 ) { - Text(item.rating.appVersion) + Image.Icon.info - Image(systemName: "iphone.gen3.circle") + Text(item.rating.appVersion) + .font(.body) } + .foregroundColor(.secondary) } - .font(.subheadline) } .padding(.vertical, 8) } -- 2.47.1 From 408e90d53f205adb93f9ed6e9b833d853d9f869e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 02:33:11 +0100 Subject: [PATCH 10/12] Implemented the FakeLabel component in the UI library. --- .../UI/Kit/Sources/Components/FakeLabel.swift | 95 +++++++++++++++++++ .../Kit/Sources/Components/StarRating.swift | 10 +- .../Kit/Sources/Extensions/Image+Icons.swift | 19 ---- .../Kit/Sources/Extensions/String+Icons.swift | 9 +- .../Sources/Extensions/UIImage+Icons.swift | 19 ---- 5 files changed, 106 insertions(+), 46 deletions(-) create mode 100644 Libraries/UI/Kit/Sources/Components/FakeLabel.swift delete mode 100644 Libraries/UI/Kit/Sources/Extensions/Image+Icons.swift delete mode 100644 Libraries/UI/Kit/Sources/Extensions/UIImage+Icons.swift diff --git a/Libraries/UI/Kit/Sources/Components/FakeLabel.swift b/Libraries/UI/Kit/Sources/Components/FakeLabel.swift new file mode 100644 index 0000000..a365342 --- /dev/null +++ b/Libraries/UI/Kit/Sources/Components/FakeLabel.swift @@ -0,0 +1,95 @@ +// +// FakeLabel.swift +// ReviewsUIKit +// +// Created by Javier Cicchelli on 20/03/2024. +// Copyright © 2024 Röck+Cöde. All rights reserved. +// + +import ReviewsFoundationKit +import SwiftUI + +public struct FakeLabel: View { + + // MARK: Constants + private let systemIcon: String + private let title: String + + // MARK: Initialisers + public init( + systemIcon: String, + title: String + ) { + self.systemIcon = systemIcon + self.title = title + } + + // MARK: Body + public var body: some View { + HStack( + alignment: .bottom, + spacing: 8 + ) { + iconOrQuestionMark + + Text(titleOrDash) + .font(.body) + } + } + +} + +// MARK: - Helpers +private extension FakeLabel { + + // MARK: Computed + var iconOrQuestionMark: Image { + .init(systemName: systemIcon.isEmpty + ? .Icon.questionMark + : systemIcon + ) + } + + var titleOrDash: String { + title.isEmpty + ? .Title.none + : title + } + +} + +// MARK: - String+Constants +private extension String { + enum Title { + static let none = "-" + } +} + +// MARK: - Previews +#Preview("Fake Label with system icon and title") { + FakeLabel( + systemIcon: .Icon.person, + title: "Some title goes here..." + ) +} + +#Preview("Fake Label with empty system icon and title") { + FakeLabel( + systemIcon: .empty, + title: "Some title goes here..." + ) +} + +#Preview("Fake Label with system icon and empty title") { + FakeLabel( + systemIcon: .Icon.person, + title: .empty + ) +} + +#Preview("Fake Label with both empty system icon and title") { + FakeLabel( + systemIcon: .empty, + title: .empty + ) +} diff --git a/Libraries/UI/Kit/Sources/Components/StarRating.swift b/Libraries/UI/Kit/Sources/Components/StarRating.swift index 70f41c7..ec7f189 100644 --- a/Libraries/UI/Kit/Sources/Components/StarRating.swift +++ b/Libraries/UI/Kit/Sources/Components/StarRating.swift @@ -1,6 +1,6 @@ // // StarRating.swift -// Feed +// ReviewsUIKit // // Created by Javier Cicchelli on 19/03/2024. // Copyright © 2024 Röck+Cöde. All rights reserved. @@ -62,9 +62,11 @@ private extension StarRating { } // MARK: - Image+Constants -private extension Image.Icon { - static let star: Image = .init(systemName: "star") - static let starFill: Image = .init(systemName: "star.fill") +private extension Image { + enum Icon { + static let star: Image = .init(systemName: "star") + static let starFill: Image = .init(systemName: "star.fill") + } } // MARK: - Previews diff --git a/Libraries/UI/Kit/Sources/Extensions/Image+Icons.swift b/Libraries/UI/Kit/Sources/Extensions/Image+Icons.swift deleted file mode 100644 index 74ec6a3..0000000 --- a/Libraries/UI/Kit/Sources/Extensions/Image+Icons.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Image+Icons.swift -// ReviewsUIKit -// -// Created by Javier Cicchelli on 20/03/2024. -// Copyright © 2024 Röck+Cöde VoF. All rights reserved. -// - -import SwiftUI - -public extension Image { - enum Icon { - - // MARK: Constants - public static let info = Image(systemName: .Icon.info) - public static let person = Image(systemName: .Icon.person) - - } -} diff --git a/Libraries/UI/Kit/Sources/Extensions/String+Icons.swift b/Libraries/UI/Kit/Sources/Extensions/String+Icons.swift index b535928..bfe3ffc 100644 --- a/Libraries/UI/Kit/Sources/Extensions/String+Icons.swift +++ b/Libraries/UI/Kit/Sources/Extensions/String+Icons.swift @@ -6,12 +6,13 @@ // Copyright © 2024 Röck+Cöde VoF. All rights reserved. // -extension String { +public extension String { enum Icon { // MARK: Constants - static let info = "info.circle.fill" - static let person = "person.crop.circle.fill" - + static let questionMark = "questionmark.circle.fill" + + public static let info = "info.circle.fill" + public static let person = "person.crop.circle.fill" } } diff --git a/Libraries/UI/Kit/Sources/Extensions/UIImage+Icons.swift b/Libraries/UI/Kit/Sources/Extensions/UIImage+Icons.swift deleted file mode 100644 index 93cbb37..0000000 --- a/Libraries/UI/Kit/Sources/Extensions/UIImage+Icons.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// 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 info = UIImage(systemName: .Icon.info) - public static let person = UIImage(systemName: .Icon.person) - - } -} -- 2.47.1 From ca50f5ff3af88fbb50914343886a5c75daeb1335 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 02:33:41 +0100 Subject: [PATCH 11/12] Integrated the FakeLabel component into the FeedItemCell component in the Feed framework. --- .../UI/Components/Cells/FeedItemCell.swift | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift b/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift index 7b37001..4b5cef9 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/Components/Cells/FeedItemCell.swift @@ -25,15 +25,10 @@ struct FeedItemCell: View { alignment: .leading, spacing: 16 ) { - HStack( - alignment: .bottom, - spacing: 8 - ) { - Image.Icon.person - - Text(item.author) - .font(.body) - } + FakeLabel( + systemIcon: .Icon.person, + title: item.author + ) .foregroundColor(.secondary) VStack(spacing: 8) { @@ -58,15 +53,10 @@ struct FeedItemCell: View { of: .Rating.total ) - HStack( - alignment: .center, - spacing: 8 - ) { - Image.Icon.info - - Text(item.rating.appVersion) - .font(.body) - } + FakeLabel( + systemIcon: .Icon.info, + title: item.rating.appVersion + ) .foregroundColor(.secondary) } } -- 2.47.1 From d29dae5c79d6450d43840bc2f30240410315829e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 20 Mar 2024 02:34:25 +0100 Subject: [PATCH 12/12] Implemented the FeedItemViewController view controller in the Feed framework. --- .../FeedItemViewController.swift | 189 +++++++++++++----- 1 file changed, 139 insertions(+), 50 deletions(-) diff --git a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedItemViewController.swift b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedItemViewController.swift index 621471e..791b869 100644 --- a/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedItemViewController.swift +++ b/Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedItemViewController.swift @@ -6,6 +6,8 @@ // Copyright © 2020 ING. All rights reserved. // +import ReviewsUIKit +import SwiftUI import UIKit final class FeedItemViewController: UIViewController { @@ -13,29 +15,49 @@ final class FeedItemViewController: UIViewController { // MARK: Constants private let item: Review + // MARK: Properties + private lazy var appVersionController = { + UIHostingController(rootView: FakeLabel( + systemIcon: .Icon.info, + title: item.rating.appVersion + )) + }() + + private lazy var authorController = { + UIHostingController(rootView: FakeLabel( + systemIcon: .Icon.person, + title: item.author + )) + }() + + private lazy var starRatingController = { + UIHostingController(rootView: StarRating( + item.rating.stars, + of: .Rating.total + )) + }() + // MARK: Outlets - private lazy var titleLabel = { - let label = UILabel() - - label.font = UIFont.preferredFont(forTextStyle: .title3) - label.numberOfLines = 0 - label.translatesAutoresizingMaskIntoConstraints = false - label.text = item.title - - return label - }() - - private lazy var authorLabel = { - let label = UILabel() + private lazy var appVersionView = { + guard let view = appVersionController.view else { + fatalError("The StarRating component must be initialised") + } - label.font = UIFont.preferredFont(forTextStyle: .headline) - label.numberOfLines = 1 - label.translatesAutoresizingMaskIntoConstraints = false - label.text = item.author - - return label + view.translatesAutoresizingMaskIntoConstraints = false + + return view }() - + + private lazy var authorView = { + guard let view = authorController.view else { + fatalError("The StarRating component must be initialised") + } + + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + private lazy var commentLabel = { let label = UILabel() @@ -46,18 +68,65 @@ final class FeedItemViewController: UIViewController { return label }() + + private lazy var ratingView = { + let stack = UIStackView() + + stack.axis = .horizontal + stack.backgroundColor = .clear + stack.distribution = .fillProportionally + stack.translatesAutoresizingMaskIntoConstraints = false + + stack.addArrangedSubview(starRatingView) + stack.addArrangedSubview(appVersionView) - private lazy var ratingVersionLabel = { - let label = UILabel() + return stack + }() + + private lazy var scrollView = { + let scroll = UIScrollView() - label.font = UIFont.preferredFont(forTextStyle: .subheadline) - label.numberOfLines = 1 - label.translatesAutoresizingMaskIntoConstraints = false - label.text = item.rating.appVersion + scroll.backgroundColor = .clear + scroll.showsVerticalScrollIndicator = true + scroll.translatesAutoresizingMaskIntoConstraints = false - return label + return scroll }() + private lazy var stackView = { + let stack = UIStackView() + + stack.axis = .vertical + stack.alignment = .leading + stack.backgroundColor = .clear + stack.distribution = .fill + stack.spacing = 16 + stack.translatesAutoresizingMaskIntoConstraints = false + + return stack + }() + + private lazy var starRatingView = { + guard let view = starRatingController.view else { + fatalError("The StarRating component must be initialised") + } + + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + private lazy var titleLabel = { + let label = UILabel() + + label.font = UIFont.preferredFont(forTextStyle: .headline) + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + label.text = item.title + + return label + }() + // MARK: Initialisers init(_ item: Review) { self.item = item @@ -68,14 +137,22 @@ final class FeedItemViewController: UIViewController { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: UIViewController override func viewDidLoad() { super.viewDidLoad() + addChild(appVersionController) + addChild(authorController) + addChild(starRatingController) + setView() setNavigationBar() setLayout() + + appVersionController.didMove(toParent: self) + authorController.didMove(toParent: self) + starRatingController.didMove(toParent: self) } } @@ -85,23 +162,31 @@ private extension FeedItemViewController { // MARK: Functions func setLayout() { + let scrollContentGuide = scrollView.contentLayoutGuide + let scrollFrameGuide = scrollView.frameLayoutGuide + NSLayoutConstraint.activate([ - authorLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), - authorLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), - authorLabel.topAnchor.constraint(equalTo: ratingVersionLabel.bottomAnchor, constant: 8), - authorLabel.heightAnchor.constraint(equalToConstant: 24), - commentLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), - commentLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), - commentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), - commentLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 24), - ratingVersionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), - ratingVersionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), - ratingVersionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), - ratingVersionLabel.heightAnchor.constraint(equalToConstant: 24), - titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8), - titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8), - titleLabel.topAnchor.constraint(equalTo: authorLabel.bottomAnchor, constant: 8), - titleLabel.heightAnchor.constraint(lessThanOrEqualToConstant: 72), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + stackView.bottomAnchor.constraint(equalTo: scrollContentGuide.bottomAnchor, constant: -16), + stackView.leadingAnchor.constraint(equalTo: scrollContentGuide.leadingAnchor), + stackView.topAnchor.constraint(equalTo: scrollContentGuide.topAnchor, constant: 8), + stackView.trailingAnchor.constraint(equalTo: scrollContentGuide.trailingAnchor), + stackView.leadingAnchor.constraint(equalTo: scrollFrameGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: scrollFrameGuide.trailingAnchor), + + authorView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 16), + + ratingView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 16), + + titleLabel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 16), + titleLabel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -16), + + commentLabel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 16), + commentLabel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -16), ]) } @@ -109,16 +194,20 @@ private extension FeedItemViewController { navigationController?.navigationBar.prefersLargeTitles = true navigationController?.navigationBar.isTranslucent = true - navigationItem.title = "#\(String(item.id))" + navigationItem.title = "# \(String(item.id))" } func setView() { - view.backgroundColor = .white + view.backgroundColor = .systemBackground + + view.addSubview(scrollView) - view.addSubview(ratingVersionLabel) - view.addSubview(authorLabel) - view.addSubview(titleLabel) - view.addSubview(commentLabel) + scrollView.addSubview(stackView) + + stackView.addArrangedSubview(authorView) + stackView.addArrangedSubview(ratingView) + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(commentLabel) } } -- 2.47.1