[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: #9
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
Javier Cicchelli 2024-03-19 08:31:13 +00:00 committed by Javier Cicchelli
parent 26c2c0c581
commit 09df006ab9
16 changed files with 664 additions and 166 deletions

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}
}

View File

@ -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..<rating {
stars += "⭐️"
}
return "\(stars) (ver: \(version))"
}
}

View File

@ -1,66 +0,0 @@
//
// AppDelegate.swift
// AppStoreReviews
//
// Created by Dmitrii Ivanov on 21/07/2020.
// Copyright © 2020 ING. All rights reserved.
//
import UIKit
class ReviewCell: UITableViewCell {
private var titleLabel = UILabel()
private var authorLabel = UILabel()
private var textPreviewLabel = UILabel()
private var ratingVersionLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupLabels()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(item: Review) {
ratingVersionLabel.text = item.ratingVersionText()
authorLabel.text = "from: \(item.author)"
titleLabel.text = "\(item.title)"
textPreviewLabel.text = "\(item.content)"
}
private func setupLabels() {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
authorLabel.translatesAutoresizingMaskIntoConstraints = false
textPreviewLabel.translatesAutoresizingMaskIntoConstraints = false
ratingVersionLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.font = UIFont.boldSystemFont(ofSize: 18)
titleLabel.numberOfLines = 2
ratingVersionLabel.font = UIFont.italicSystemFont(ofSize: 18)
authorLabel.font = UIFont.italicSystemFont(ofSize: 18)
textPreviewLabel.font = UIFont.systemFont(ofSize: 14)
textPreviewLabel.numberOfLines = 3
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stack)
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8).isActive = true
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8).isActive = true
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8).isActive = true
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8).isActive = true
stack.axis = .vertical
stack.distribution = .equalSpacing
stack.alignment = .leading
stack.addArrangedSubview(ratingVersionLabel)
stack.addArrangedSubview(authorLabel)
stack.addArrangedSubview(titleLabel)
stack.addArrangedSubview(textPreviewLabel)
}
}

View File

@ -0,0 +1,90 @@
//
// FeedItem.swift
// ReviewsFeed
//
// Created by Javier Cicchelli on 19/03/2024.
// Copyright © 2024 Röck+Cöde. All rights reserved.
//
import SwiftUI
struct FeedItem: View {
// MARK: Constants
private let item: Review
// MARK: Initialisers
init(_ item: Review) {
self.item = item
}
// MARK: Body
var body: some View {
VStack(
alignment: .leading,
spacing: 16
) {
HStack(
alignment: .bottom,
spacing: 8
) {
Image(systemName: "person.crop.circle")
Text(item.author)
.font(.subheadline)
}
VStack(spacing: 8) {
Text(item.title)
.font(.headline)
.lineLimit(2)
.fullWidth()
Text(item.comment)
.font(.body)
.lineLimit(4)
.fullWidth()
}
.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")
}
}
Spacer()
HStack(
alignment: .bottom,
spacing: 4
) {
Text(item.rating.appVersion)
Image(systemName: "iphone.gen3.circle")
}
}
.font(.subheadline)
}
.padding(.vertical, 8)
}
}
// MARK: - Previews
#Preview("Feed Item") {
FeedItem(.init(
author: "Some author name here...",
comment: "Some review comment here...",
id: 0,
rating: .init(
stars: 1,
appVersion: "v1.2.3"
),
title: "Some review title here..."
))
}

View File

@ -1,6 +1,6 @@
// //
// AppDelegate.swift // AppDelegate.swift
// AppStoreReviews // ReviewsFeed
// //
// Created by Dmitrii Ivanov on 21/07/2020. // Created by Dmitrii Ivanov on 21/07/2020.
// Copyright © 2020 ING. All rights reserved. // Copyright © 2020 ING. All rights reserved.
@ -43,7 +43,7 @@ class DetailsViewController: UIViewController {
view.addSubview(titleLabel) view.addSubview(titleLabel)
view.addSubview(contentLabel) view.addSubview(contentLabel)
ratingVersionLabel.text = review.ratingVersionText() ratingVersionLabel.text = review.rating.appVersion
ratingVersionLabel.font = UIFont.italicSystemFont(ofSize: 18) ratingVersionLabel.font = UIFont.italicSystemFont(ofSize: 18)
authorLabel.text = review.author authorLabel.text = review.author
@ -53,7 +53,7 @@ class DetailsViewController: UIViewController {
titleLabel.numberOfLines = 0 titleLabel.numberOfLines = 0
titleLabel.font = UIFont.boldSystemFont(ofSize: 22) titleLabel.font = UIFont.boldSystemFont(ofSize: 22)
contentLabel.text = review.content contentLabel.text = review.comment
contentLabel.numberOfLines = 0 contentLabel.numberOfLines = 0
ratingVersionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8).isActive = true ratingVersionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8).isActive = true

View File

@ -0,0 +1,200 @@
//
// AppDelegate.swift
// ReviewsFeed
//
// Created by Dmitrii Ivanov on 21/07/2020.
// Copyright © 2020 ING. All rights reserved.
//
import Combine
import ReviewsUIKit
import SwiftUI
import UIKit
public class FeedViewController: UITableViewController {
// MARK: Constants
private let viewModel: ViewModel
// MARK: Properties
private var cancellables: Set<AnyCancellable> = []
// 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

View File

@ -15,6 +15,7 @@ let package = Package(
.Target.filter.kit, .Target.filter.kit,
.Target.foundation.kit, .Target.foundation.kit,
.Target.iTunes.kit, .Target.iTunes.kit,
.Target.ui.kit,
] ]
), ),
], ],
@ -45,6 +46,13 @@ let package = Package(
], ],
path: "iTunes/Kit" path: "iTunes/Kit"
), ),
.target(
name: .Target.ui.kit,
dependencies: [
.byName(name: .Target.foundation.kit),
],
path: "UI/Kit"
),
.testTarget( .testTarget(
name: .Target.feed.test, name: .Target.feed.test,
dependencies: [ dependencies: [
@ -73,6 +81,13 @@ let package = Package(
], ],
path: "iTunes/Test" 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 filter = "\(String.Product.name)Filter"
static let foundation = "\(String.Product.name)Foundation" static let foundation = "\(String.Product.name)Foundation"
static let iTunes = "\(String.Product.name)iTunes" static let iTunes = "\(String.Product.name)iTunes"
static let ui = "\(String.Product.name)UI"
} }
} }

View File

@ -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<Content: View>: UIContentConfiguration {
// MARK: Type aliases
public typealias ContentClosure = () -> Content
// MARK: Constants
fileprivate let hostingController: UIHostingController<Content>
// MARK: Initialisers
public init(@ViewBuilder content: ContentClosure) {
hostingController = UIHostingController(rootView: content())
}
// MARK: Functions
public func makeContentView() -> UIView & UIContentView {
ContentView<Content>(self)
}
public func updated(
for state: UIConfigurationState
) -> HostingConfiguration<Content> {
self
}
}
// MARK: - Classes
class ContentView<Content: View>: 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<Content>,
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)
}
}

View File

@ -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
}
}

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -13,7 +13,7 @@ import ReviewsFeedKit
extension ServiceConfiguration { extension ServiceConfiguration {
// MARK: Initialisers // MARK: Initialisers
init(session: URLSessionConfiguration = .ephemeral) { public init(session: URLSessionConfiguration = .ephemeral) {
self.init( self.init(
host: .iTunes, host: .iTunes,
session: session, session: session,

View File

@ -8,11 +8,16 @@
import ReviewsFeedKit import ReviewsFeedKit
struct Feed { public struct Feed {
// MARK: Constants // MARK: Constants
let entries: [Review] let entries: [Review]
// MARK: Initialisers
public init(entries: [Review]) {
self.entries = entries
}
} }
// MARK: - Decodable // MARK: - Decodable
@ -28,7 +33,7 @@ extension Feed: Decodable {
} }
// MARK: Initialisers // MARK: Initialisers
init(from decoder: any Decoder) throws { public init(from decoder: any Decoder) throws {
let feed = try decoder.container(keyedBy: FeedKeys.self) let feed = try decoder.container(keyedBy: FeedKeys.self)
let feedEntry = try feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed) let feedEntry = try feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed)
@ -41,7 +46,7 @@ extension Feed: Decodable {
extension Feed: Encodable { extension Feed: Encodable {
// MARK: Functions // 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 feed = encoder.container(keyedBy: FeedKeys.self)
var feedEntry = feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed) var feedEntry = feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed)

View File

@ -7,11 +7,12 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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, ); }; }; 02DC7F9F2BA51793000EEEBE /* ReviewsFeed.h in Headers */ = {isa = PBXBuildFile; fileRef = 02DC7F912BA51793000EEEBE /* ReviewsFeed.h */; settings = {ATTRIBUTES = (Public, ); }; };
02DC7FA22BA51793000EEEBE /* ReviewsFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; }; 02DC7FA22BA51793000EEEBE /* ReviewsFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; };
02DC7FA32BA51793000EEEBE /* ReviewsFeed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 02DC7FA32BA51793000EEEBE /* ReviewsFeed.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
02DC7FAC2BA51B4C000EEEBE /* DetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */; }; 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 */; }; 02DC7FAE2BA51B4C000EEEBE /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */; };
02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13124C6EE64004E2EE1 /* Review.swift */; }; 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13124C6EE64004E2EE1 /* Review.swift */; };
02DC7FB32BA52518000EEEBE /* ReviewsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 02DC7FB22BA52518000EEEBE /* ReviewsKit */; }; 02DC7FB32BA52518000EEEBE /* ReviewsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 02DC7FB22BA52518000EEEBE /* ReviewsKit */; };
@ -46,6 +47,8 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0220ADA22BA90646001E6A9F /* FeedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItem.swift; sourceTree = "<group>"; };
02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.swift; sourceTree = "<group>"; };
02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReviewsFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReviewsFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; };
02DC7F912BA51793000EEEBE /* ReviewsFeed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReviewsFeed.h; sourceTree = "<group>"; }; 02DC7F912BA51793000EEEBE /* ReviewsFeed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReviewsFeed.h; sourceTree = "<group>"; };
02DC7FB12BA52084000EEEBE /* Libraries */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Libraries; sourceTree = "<group>"; }; 02DC7FB12BA52084000EEEBE /* Libraries */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Libraries; sourceTree = "<group>"; };
@ -55,7 +58,6 @@
345AD12724C6EDDC004E2EE1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 345AD12724C6EDDC004E2EE1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
345AD12924C6EDDC004E2EE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 345AD12924C6EDDC004E2EE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = "<group>"; }; 345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = "<group>"; };
345AD13024C6EE64004E2EE1 /* ReviewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReviewCell.swift; sourceTree = "<group>"; };
345AD13124C6EE64004E2EE1 /* Review.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Review.swift; sourceTree = "<group>"; }; 345AD13124C6EE64004E2EE1 /* Review.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Review.swift; sourceTree = "<group>"; };
345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailsViewController.swift; sourceTree = "<group>"; }; 345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailsViewController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -81,6 +83,57 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
02620B852BA89BF900DE7137 /* UI */ = {
isa = PBXGroup;
children = (
02620B892BA89C2400DE7137 /* Components */,
02620B882BA89C1000DE7137 /* View Controllers */,
);
path = UI;
sourceTree = "<group>";
};
02620B862BA89C0000DE7137 /* Logic */ = {
isa = PBXGroup;
children = (
02620B8A2BA89C3300DE7137 /* Models */,
02620B872BA89C0700DE7137 /* View Models */,
);
path = Logic;
sourceTree = "<group>";
};
02620B872BA89C0700DE7137 /* View Models */ = {
isa = PBXGroup;
children = (
02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */,
);
path = "View Models";
sourceTree = "<group>";
};
02620B882BA89C1000DE7137 /* View Controllers */ = {
isa = PBXGroup;
children = (
345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */,
345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */,
);
path = "View Controllers";
sourceTree = "<group>";
};
02620B892BA89C2400DE7137 /* Components */ = {
isa = PBXGroup;
children = (
0220ADA22BA90646001E6A9F /* FeedItem.swift */,
);
path = Components;
sourceTree = "<group>";
};
02620B8A2BA89C3300DE7137 /* Models */ = {
isa = PBXGroup;
children = (
345AD13124C6EE64004E2EE1 /* Review.swift */,
);
path = Models;
sourceTree = "<group>";
};
02A6DA2F2BA591C000B943E2 /* Bundle */ = { 02A6DA2F2BA591C000B943E2 /* Bundle */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -159,10 +212,8 @@
02DC7FB02BA51B4F000EEEBE /* Sources */ = { 02DC7FB02BA51B4F000EEEBE /* Sources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */, 02620B862BA89C0000DE7137 /* Logic */,
345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */, 02620B852BA89BF900DE7137 /* UI */,
345AD13124C6EE64004E2EE1 /* Review.swift */,
345AD13024C6EE64004E2EE1 /* ReviewCell.swift */,
); );
path = Sources; path = Sources;
sourceTree = "<group>"; sourceTree = "<group>";
@ -314,10 +365,11 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */,
02DC7FAC2BA51B4C000EEEBE /* DetailsViewController.swift in Sources */, 02DC7FAC2BA51B4C000EEEBE /* DetailsViewController.swift in Sources */,
0220ADA32BA90646001E6A9F /* FeedItem.swift in Sources */,
02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */, 02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */,
02DC7FAE2BA51B4C000EEEBE /* FeedViewController.swift in Sources */, 02DC7FAE2BA51B4C000EEEBE /* FeedViewController.swift in Sources */,
02DC7FAD2BA51B4C000EEEBE /* ReviewCell.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -355,7 +407,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 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_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "";
@ -404,7 +456,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 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_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "";