[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:
parent
26c2c0c581
commit
09df006ab9
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
31
Frameworks/Feed/Bundle/Sources/Logic/Models/Review.swift
Normal file
31
Frameworks/Feed/Bundle/Sources/Logic/Models/Review.swift
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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))"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
90
Frameworks/Feed/Bundle/Sources/UI/Components/FeedItem.swift
Normal file
90
Frameworks/Feed/Bundle/Sources/UI/Components/FeedItem.swift
Normal 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..."
|
||||||
|
))
|
||||||
|
}
|
@ -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
|
@ -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
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
100
Libraries/UI/Kit/Sources/Components/HostingConfiguration.swift
Normal file
100
Libraries/UI/Kit/Sources/Components/HostingConfiguration.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
24
Libraries/UI/Kit/Sources/Extensions/UIView+Functions.swift
Normal file
24
Libraries/UI/Kit/Sources/Extensions/UIView+Functions.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
18
Libraries/UI/Kit/Sources/Extensions/View+Modifiers.swift
Normal file
18
Libraries/UI/Kit/Sources/Extensions/View+Modifiers.swift
Normal 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
43
Libraries/UI/Kit/Sources/Modifiers/FullWidthModifier.swift
Normal file
43
Libraries/UI/Kit/Sources/Modifiers/FullWidthModifier.swift
Normal 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)
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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 = "";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user