From 09df006ab98c68cbda61ba14164589adee513b2b Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 19 Mar 2024 08:31:13 +0000 Subject: [PATCH] [Framework] Feed list view in the Feed framework (#9) This PR contains the work done to provide the existing `FeedViewController` view controller with real life data by integrating the `iTunesService` service into its view model. In addition, the list item cell has been design has been updated, and re-implemented using the `SwiftUI` framework. Reviewed-on: https://repo.rock-n-code.com/rock-n-code/app-reviews/pulls/9 Co-authored-by: Javier Cicchelli Co-committed-by: Javier Cicchelli --- .../Bundle/Sources/FeedViewController.swift | 58 ----- .../Bundle/Sources/Logic/Models/Review.swift | 31 +++ .../Logic/View Models/FeedViewModel.swift | 69 ++++++ Frameworks/Feed/Bundle/Sources/Review.swift | 26 --- .../Feed/Bundle/Sources/ReviewCell.swift | 66 ------ .../Sources/UI/Components/FeedItem.swift | 90 ++++++++ .../DetailsViewController.swift | 6 +- .../View Controllers/FeedViewController.swift | 200 ++++++++++++++++++ Libraries/Package.swift | 16 ++ .../Components/HostingConfiguration.swift | 100 +++++++++ .../Sources/Extensions/UIView+Functions.swift | 24 +++ .../Sources/Extensions/View+Modifiers.swift | 18 ++ .../Sources/Modifiers/FullWidthModifier.swift | 43 ++++ .../ServiceConfiguration+Inits.swift | 2 +- Libraries/iTunes/Kit/Models/Feed.swift | 11 +- Reviews.xcodeproj/project.pbxproj | 70 +++++- 16 files changed, 664 insertions(+), 166 deletions(-) delete mode 100644 Frameworks/Feed/Bundle/Sources/FeedViewController.swift create mode 100644 Frameworks/Feed/Bundle/Sources/Logic/Models/Review.swift create mode 100644 Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift delete mode 100644 Frameworks/Feed/Bundle/Sources/Review.swift delete mode 100644 Frameworks/Feed/Bundle/Sources/ReviewCell.swift create mode 100644 Frameworks/Feed/Bundle/Sources/UI/Components/FeedItem.swift rename Frameworks/Feed/Bundle/Sources/{ => UI/View Controllers}/DetailsViewController.swift (96%) create mode 100644 Frameworks/Feed/Bundle/Sources/UI/View Controllers/FeedViewController.swift create mode 100644 Libraries/UI/Kit/Sources/Components/HostingConfiguration.swift create mode 100644 Libraries/UI/Kit/Sources/Extensions/UIView+Functions.swift create mode 100644 Libraries/UI/Kit/Sources/Extensions/View+Modifiers.swift create mode 100644 Libraries/UI/Kit/Sources/Modifiers/FullWidthModifier.swift diff --git a/Frameworks/Feed/Bundle/Sources/FeedViewController.swift b/Frameworks/Feed/Bundle/Sources/FeedViewController.swift deleted file mode 100644 index 96db814..0000000 --- a/Frameworks/Feed/Bundle/Sources/FeedViewController.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// AppDelegate.swift -// AppStoreReviews -// -// Created by Dmitrii Ivanov on 21/07/2020. -// Copyright © 2020 ING. All rights reserved. -// - -import UIKit - -public class FeedViewController: UITableViewController { - - public init() { - super.init(style: .plain) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func viewDidLoad() { - super.viewDidLoad() - tableView.register(ReviewCell.self, forCellReuseIdentifier: "cellId") - tableView.rowHeight = 160 - } - - public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 100 - } - - public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let c = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! ReviewCell - c.update(item: randomReview()) - return c - } - - public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - let vc = DetailsViewController(review: randomReview()) - navigationController!.pushViewController(vc, animated: true) - } - - func randomReview() -> Review { - let author = ["Dan Auerbach", "Bo Diddley", "Otis Rush", "Jimi Hendrix", "Albert King", "Buddy Guy", "Muddy Waters", "Eric Clapton"].randomElement()! - let version = ["3.11", "3.12"].randomElement()! - let rating = Int.random(in: 1...5) - let title = ["Awesome app", "Could be better", "Gimme my money back!!", "Lemme tell you a story..."].randomElement()! - let id = UUID().uuidString - let content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - return Review(author: author, - version: version, - rating: rating, - title: title, - id: id, - content: content) - } -} - diff --git a/Frameworks/Feed/Bundle/Sources/Logic/Models/Review.swift b/Frameworks/Feed/Bundle/Sources/Logic/Models/Review.swift new file mode 100644 index 0000000..a315a88 --- /dev/null +++ b/Frameworks/Feed/Bundle/Sources/Logic/Models/Review.swift @@ -0,0 +1,31 @@ +// +// Review.swift +// ReviewsFeed +// +// Created by Dmitrii Ivanov on 21/07/2020. +// Copyright © 2020 ING. All rights reserved. +// + +import Foundation + +struct Review { + + // MARK: Constants + let author: String + let comment: String + let id: Int + let rating: Rating + let title: String + +} + +// MARK: - Structs +extension Review { + struct Rating { + + // MARK: Constants + let stars: Int + let appVersion: String + + } +} diff --git a/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift new file mode 100644 index 0000000..8198e6d --- /dev/null +++ b/Frameworks/Feed/Bundle/Sources/Logic/View Models/FeedViewModel.swift @@ -0,0 +1,69 @@ +// +// FeedViewModel.swift +// ReviewsFeed +// +// Created by Javier Cicchelli on 18/03/2024. +// Copyright © 2024 Röck+Cöde. All rights reserved. +// + +import Foundation +import ReviewsiTunesKit + +extension FeedViewController { + final class ViewModel: ObservableObject { + + // MARK: Type aliases + typealias Configuration = FeedViewController.Configuration + + // MARK: Constants + private let configuration: Configuration + + // MARK: Properties + @Published var loading: Bool = false + + var items: [Review] = [] + + // MARK: Initialisers + init(configuration: Configuration = .init()) { + self.configuration = configuration + } + + // MARK: Computed + lazy private var iTunesService: iTunesService = { + .init(configuration: .init(session: configuration.session)) + }() + + // MARK: Functions + func fetch() { + Task { + loading = true + + do { + let output = try await iTunesService.getReviews(.init( + appID: configuration.appID, + countryCode: configuration.countryCode + )) + + items = output.reviews + .map { review -> Review in + .init( + author: review.author, + comment: review.content, + id: review.id, + rating: .init( + stars: review.rating, + appVersion: review.version + ), + title: review.title + ) + } + } catch { + // TODO: handle this error gracefully. + } + + loading = false + } + } + + } +} diff --git a/Frameworks/Feed/Bundle/Sources/Review.swift b/Frameworks/Feed/Bundle/Sources/Review.swift deleted file mode 100644 index d83086c..0000000 --- a/Frameworks/Feed/Bundle/Sources/Review.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// AppDelegate.swift -// AppStoreReviews -// -// Created by Dmitrii Ivanov on 21/07/2020. -// Copyright © 2020 ING. All rights reserved. -// - -import Foundation - -struct Review { - let author: String - let version: String - let rating: Int - let title: String - let id: String - let content: String - - func ratingVersionText() -> String { - var stars = "" - for _ in 0.. = [] + + // MARK: Initialisers + public init(configuration: Configuration = .init()) { + self.viewModel = .init(configuration: configuration) + + super.init(style: .plain) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: UIViewController + public override func viewDidLoad() { + super.viewDidLoad() + + tableView.register( + UITableViewCell.self, + forCellReuseIdentifier: .Cell.feedItem + ) + + bindViewModel() + + viewModel.fetch() + } + + // MARK: UITableViewDataSource + public override func tableView( + _ tableView: UITableView, + numberOfRowsInSection section: Int + ) -> Int { + viewModel.items.count + } + + public override func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: .Cell.feedItem) else { + return .init() + } + + cell.contentConfiguration = { + if #available(iOS 16.0, *) { + UIHostingConfiguration { + FeedItem(viewModel.items[indexPath.row]) + } + } else { + HostingConfiguration { + FeedItem(viewModel.items[indexPath.row]) + } + } + }() + + return cell + } + + // MARK: UITableViewDelegate + public override func tableView( + _ tableView: UITableView, + didSelectRowAt indexPath: IndexPath + ) { + let details = DetailsViewController(review: viewModel.items[indexPath.row]) + + tableView.deselectRow( + at: indexPath, + animated: true + ) + + navigationController?.pushViewController(details, animated: true) + } + +} + +// MARK: - Helpers +private extension FeedViewController { + + // MARK: Functions + func bindViewModel() { + viewModel.$loading + .sink { loading in + print("LOADING: \(loading)") + } + .store(in: &cancellables) + + viewModel.$loading + .dropFirst() + .filter { $0 == false } + .receive(on: RunLoop.main) + .sink { [weak self] _ in + self?.tableView.reloadData() + } + .store(in: &cancellables) + } + +} + +// MARK: - Configuration +extension FeedViewController { + public struct Configuration { + + // MARK: Constants + let appID: String + let countryCode: String + let session: URLSessionConfiguration + + // MARK: Initialisers + public init( + appID: String = "474495017", + countryCode: String = "nl", + session: URLSessionConfiguration = .ephemeral + ) { + self.appID = appID + self.countryCode = countryCode + self.session = session + } + + } +} + +// MARK: - String+Constants +private extension String { + enum Cell { + static let feedItem = "FeedItemCell" + } +} + +// MARK: - Previews +#if DEBUG +import ReviewsFoundationKit +import ReviewsiTunesKit + +@available(iOS 17.0, *) +#Preview("Feed View Controller with few reviews") { + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: [ + .init( + id: 1, + author: "Some author name #1 here", + title: "Some review title #1 goes here...", + content: "Some long, explanatory review comment #1 goes here...", + rating: 3, + version: "v1.0.0", + updated: .init() + ), + .init( + id: 2, + author: "Some author name #2 here", + title: "Some review title #2 goes here...", + content: "Some long, explanatory review comment #2 goes here...", + rating: 5, + version: "v1.0.0", + updated: .init() + ), + .init( + id: 3, + author: "Some author name #3 here", + title: "Some review title #3 goes here...", + content: "Some long, explanatory review comment #3 goes here...", + rating: 1, + version: "v1.0.0", + updated: .init() + ), + ]) + ) + + return FeedViewController(configuration: .init(session: .mock)) +} + +@available(iOS 17.0, *) +#Preview("Feed View Controller with no reviews") { + MockURLProtocol.response = .init( + statusCode: 200, + object: Feed(entries: []) + ) + + return FeedViewController(configuration: .init(session: .mock)) +} +#endif diff --git a/Libraries/Package.swift b/Libraries/Package.swift index dcc0630..387f970 100644 --- a/Libraries/Package.swift +++ b/Libraries/Package.swift @@ -15,6 +15,7 @@ let package = Package( .Target.filter.kit, .Target.foundation.kit, .Target.iTunes.kit, + .Target.ui.kit, ] ), ], @@ -45,6 +46,13 @@ let package = Package( ], path: "iTunes/Kit" ), + .target( + name: .Target.ui.kit, + dependencies: [ + .byName(name: .Target.foundation.kit), + ], + path: "UI/Kit" + ), .testTarget( name: .Target.feed.test, dependencies: [ @@ -73,6 +81,13 @@ let package = Package( ], path: "iTunes/Test" ), + .testTarget( + name: .Target.ui.test, + dependencies: [ + .byName(name: .Target.ui.kit), + ], + path: "UI/Test" + ), ] ) @@ -91,6 +106,7 @@ private extension String { static let filter = "\(String.Product.name)Filter" static let foundation = "\(String.Product.name)Foundation" static let iTunes = "\(String.Product.name)iTunes" + static let ui = "\(String.Product.name)UI" } } diff --git a/Libraries/UI/Kit/Sources/Components/HostingConfiguration.swift b/Libraries/UI/Kit/Sources/Components/HostingConfiguration.swift new file mode 100644 index 0000000..0f2550f --- /dev/null +++ b/Libraries/UI/Kit/Sources/Components/HostingConfiguration.swift @@ -0,0 +1,100 @@ +// +// HostingConfiguration.swift +// ReviewsUIKit +// +// Created by Javier Cicchelli on 18/03/2024. +// Copyright © 2024 Röck+Cöde. All rights reserved. +// + +import SwiftUI + +public struct HostingConfiguration: UIContentConfiguration { + + // MARK: Type aliases + public typealias ContentClosure = () -> Content + + // MARK: Constants + fileprivate let hostingController: UIHostingController + + // MARK: Initialisers + public init(@ViewBuilder content: ContentClosure) { + hostingController = UIHostingController(rootView: content()) + } + + // MARK: Functions + public func makeContentView() -> UIView & UIContentView { + ContentView(self) + } + + public func updated( + for state: UIConfigurationState + ) -> HostingConfiguration { + self + } + +} + +// MARK: - Classes +class ContentView: UIView, UIContentView { + + // MARK: Properties + var configuration: UIContentConfiguration { + didSet { + configure(configuration) + } + } + + // MARK: Initialisers + init(_ configuration: UIContentConfiguration) { + self.configuration = configuration + + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + // This view shouldn't be initialized this way so we crash + fatalError("init(coder:) has not been implemented") + } + +} + +// MARK: - Helpers +private extension ContentView { + + // MARK: Functions + func configure(_ configuration: UIContentConfiguration) { + guard + let configuration = configuration as? HostingConfiguration, + let parent = findNextViewController() + else { + return + } + + let hostingController = configuration.hostingController + + guard + let swiftUICellView = hostingController.view, + subviews.isEmpty + else { + hostingController.view.invalidateIntrinsicContentSize() + return + } + + hostingController.view.backgroundColor = .clear + + parent.addChild(hostingController) + addSubview(hostingController.view) + + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + leadingAnchor.constraint(equalTo: swiftUICellView.leadingAnchor), + trailingAnchor.constraint(equalTo: swiftUICellView.trailingAnchor), + topAnchor.constraint(equalTo: swiftUICellView.topAnchor), + bottomAnchor.constraint(equalTo: swiftUICellView.bottomAnchor) + ]) + + hostingController.didMove(toParent: parent) + } + +} diff --git a/Libraries/UI/Kit/Sources/Extensions/UIView+Functions.swift b/Libraries/UI/Kit/Sources/Extensions/UIView+Functions.swift new file mode 100644 index 0000000..ead5fb8 --- /dev/null +++ b/Libraries/UI/Kit/Sources/Extensions/UIView+Functions.swift @@ -0,0 +1,24 @@ +// +// UIView+Functions.swift +// ReviewsUIKit +// +// Created by Javier Cicchelli on 19/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import UIKit + +extension UIView { + + // MARK: Functions + func findNextViewController() -> UIViewController? { + if let nextResponder = next as? UIViewController { + return nextResponder + } else if let nextResponder = next as? UIView { + return nextResponder.findNextViewController() + } + + return nil + } + +} diff --git a/Libraries/UI/Kit/Sources/Extensions/View+Modifiers.swift b/Libraries/UI/Kit/Sources/Extensions/View+Modifiers.swift new file mode 100644 index 0000000..a3cf82a --- /dev/null +++ b/Libraries/UI/Kit/Sources/Extensions/View+Modifiers.swift @@ -0,0 +1,18 @@ +// +// View+Modifiers.swift +// ReviewsUIKit +// +// Created by Javier Cicchelli on 19/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import SwiftUI + +extension View { + + // MARK: Functions + public func fullWidth(alignment: Alignment = .leading) -> some View { + modifier(FullWidthModifier(alignment: alignment)) + } + +} diff --git a/Libraries/UI/Kit/Sources/Modifiers/FullWidthModifier.swift b/Libraries/UI/Kit/Sources/Modifiers/FullWidthModifier.swift new file mode 100644 index 0000000..d834b47 --- /dev/null +++ b/Libraries/UI/Kit/Sources/Modifiers/FullWidthModifier.swift @@ -0,0 +1,43 @@ +// +// FullWidthModifier.swift +// ReviewsUIKit +// +// Created by Javier Cicchelli on 19/03/2024. +// Copyright © 2024 Röck+Cöde VoF. All rights reserved. +// + +import SwiftUI + +struct FullWidthModifier: ViewModifier { + + // MARK: Constants + private let alignment: Alignment + + // MARK: Initialisers + init(alignment: Alignment) { + self.alignment = alignment + } + + // MARK: Functions + func body(content: Content) -> some View { + content + .frame( + maxWidth: .infinity, + alignment: alignment + ) + } + +} + +// MARK: - Previews +#Preview { + Group { + Text("Hello, world!") + .modifier(FullWidthModifier(alignment: .leading)) + Text("Hello, world!") + .modifier(FullWidthModifier(alignment: .center)) + Text("Hello, world!") + .modifier(FullWidthModifier(alignment: .trailing)) + } + .padding(.horizontal) +} diff --git a/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift b/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift index f156e71..2962ba9 100644 --- a/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift +++ b/Libraries/iTunes/Kit/Extensions/ServiceConfiguration+Inits.swift @@ -13,7 +13,7 @@ import ReviewsFeedKit extension ServiceConfiguration { // MARK: Initialisers - init(session: URLSessionConfiguration = .ephemeral) { + public init(session: URLSessionConfiguration = .ephemeral) { self.init( host: .iTunes, session: session, diff --git a/Libraries/iTunes/Kit/Models/Feed.swift b/Libraries/iTunes/Kit/Models/Feed.swift index 718004c..1c0236e 100644 --- a/Libraries/iTunes/Kit/Models/Feed.swift +++ b/Libraries/iTunes/Kit/Models/Feed.swift @@ -8,11 +8,16 @@ import ReviewsFeedKit -struct Feed { +public struct Feed { // MARK: Constants let entries: [Review] + // MARK: Initialisers + public init(entries: [Review]) { + self.entries = entries + } + } // MARK: - Decodable @@ -28,7 +33,7 @@ extension Feed: Decodable { } // MARK: Initialisers - init(from decoder: any Decoder) throws { + public init(from decoder: any Decoder) throws { let feed = try decoder.container(keyedBy: FeedKeys.self) let feedEntry = try feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed) @@ -41,7 +46,7 @@ extension Feed: Decodable { extension Feed: Encodable { // MARK: Functions - func encode(to encoder: any Encoder) throws { + public func encode(to encoder: any Encoder) throws { var feed = encoder.container(keyedBy: FeedKeys.self) var feedEntry = feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed) diff --git a/Reviews.xcodeproj/project.pbxproj b/Reviews.xcodeproj/project.pbxproj index 6bbe025..62a1186 100644 --- a/Reviews.xcodeproj/project.pbxproj +++ b/Reviews.xcodeproj/project.pbxproj @@ -7,11 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 0220ADA32BA90646001E6A9F /* FeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0220ADA22BA90646001E6A9F /* FeedItem.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 */; }; 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 */; }; - 02DC7FAD2BA51B4C000EEEBE /* ReviewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13024C6EE64004E2EE1 /* ReviewCell.swift */; }; 02DC7FAE2BA51B4C000EEEBE /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */; }; 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13124C6EE64004E2EE1 /* Review.swift */; }; 02DC7FB32BA52518000EEEBE /* ReviewsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 02DC7FB22BA52518000EEEBE /* ReviewsKit */; }; @@ -46,6 +47,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0220ADA22BA90646001E6A9F /* FeedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItem.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 = ""; }; 02DC7FB12BA52084000EEEBE /* Libraries */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Libraries; sourceTree = ""; }; @@ -55,7 +58,6 @@ 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 = ""; }; - 345AD13024C6EE64004E2EE1 /* ReviewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReviewCell.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 */ @@ -81,6 +83,57 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 02620B852BA89BF900DE7137 /* UI */ = { + isa = PBXGroup; + children = ( + 02620B892BA89C2400DE7137 /* Components */, + 02620B882BA89C1000DE7137 /* View Controllers */, + ); + path = UI; + sourceTree = ""; + }; + 02620B862BA89C0000DE7137 /* Logic */ = { + isa = PBXGroup; + children = ( + 02620B8A2BA89C3300DE7137 /* Models */, + 02620B872BA89C0700DE7137 /* View Models */, + ); + path = Logic; + sourceTree = ""; + }; + 02620B872BA89C0700DE7137 /* View Models */ = { + isa = PBXGroup; + children = ( + 02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */, + ); + path = "View Models"; + sourceTree = ""; + }; + 02620B882BA89C1000DE7137 /* View Controllers */ = { + isa = PBXGroup; + children = ( + 345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */, + 345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + 02620B892BA89C2400DE7137 /* Components */ = { + isa = PBXGroup; + children = ( + 0220ADA22BA90646001E6A9F /* FeedItem.swift */, + ); + path = Components; + sourceTree = ""; + }; + 02620B8A2BA89C3300DE7137 /* Models */ = { + isa = PBXGroup; + children = ( + 345AD13124C6EE64004E2EE1 /* Review.swift */, + ); + path = Models; + sourceTree = ""; + }; 02A6DA2F2BA591C000B943E2 /* Bundle */ = { isa = PBXGroup; children = ( @@ -159,10 +212,8 @@ 02DC7FB02BA51B4F000EEEBE /* Sources */ = { isa = PBXGroup; children = ( - 345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */, - 345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */, - 345AD13124C6EE64004E2EE1 /* Review.swift */, - 345AD13024C6EE64004E2EE1 /* ReviewCell.swift */, + 02620B862BA89C0000DE7137 /* Logic */, + 02620B852BA89BF900DE7137 /* UI */, ); path = Sources; sourceTree = ""; @@ -314,10 +365,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */, 02DC7FAC2BA51B4C000EEEBE /* DetailsViewController.swift in Sources */, + 0220ADA32BA90646001E6A9F /* FeedItem.swift in Sources */, 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */, 02DC7FAE2BA51B4C000EEEBE /* FeedViewController.swift in Sources */, - 02DC7FAD2BA51B4C000EEEBE /* ReviewCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -355,7 +407,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_IDENTITY = ""; @@ -404,7 +456,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_IDENTITY = "";