[Framework] Feed item view in the Feed framework (#10)
This PR contains the work done to implement the `FeedItemViewController` view controller, that shows in details a selected review from the `FeedListViewController` view controller. Reviewed-on: #10 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
09df006ab9
commit
c9f4b9a677
@ -16,7 +16,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
window = UIWindow(frame: UIScreen.main.bounds)
|
window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
let viewController = FeedViewController()
|
let viewController = FeedListViewController()
|
||||||
window?.rootViewController = UINavigationController(rootViewController: viewController)
|
window?.rootViewController = UINavigationController(rootViewController: viewController)
|
||||||
window?.makeKeyAndVisible()
|
window?.makeKeyAndVisible()
|
||||||
return true
|
return true
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// Int+Constants.swift
|
||||||
|
// ReviewsFeed
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 19/03/2024.
|
||||||
|
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
extension Int {
|
||||||
|
|
||||||
|
// MARK: Constants
|
||||||
|
enum Rating {
|
||||||
|
static let total: Int = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,11 +9,11 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import ReviewsiTunesKit
|
import ReviewsiTunesKit
|
||||||
|
|
||||||
extension FeedViewController {
|
extension FeedListViewController {
|
||||||
final class ViewModel: ObservableObject {
|
final class ViewModel: ObservableObject {
|
||||||
|
|
||||||
// MARK: Type aliases
|
// MARK: Type aliases
|
||||||
typealias Configuration = FeedViewController.Configuration
|
typealias Configuration = FeedListViewController.Configuration
|
||||||
|
|
||||||
// MARK: Constants
|
// MARK: Constants
|
||||||
private let configuration: Configuration
|
private let configuration: Configuration
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
//
|
//
|
||||||
// FeedItem.swift
|
// FeedItemCell.swift
|
||||||
// ReviewsFeed
|
// ReviewsFeed
|
||||||
//
|
//
|
||||||
// Created by Javier Cicchelli on 19/03/2024.
|
// Created by Javier Cicchelli on 19/03/2024.
|
||||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import ReviewsUIKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct FeedItem: View {
|
struct FeedItemCell: View {
|
||||||
|
|
||||||
// MARK: Constants
|
// MARK: Constants
|
||||||
private let item: Review
|
private let item: Review
|
||||||
@ -24,15 +25,11 @@ struct FeedItem: View {
|
|||||||
alignment: .leading,
|
alignment: .leading,
|
||||||
spacing: 16
|
spacing: 16
|
||||||
) {
|
) {
|
||||||
HStack(
|
FakeLabel(
|
||||||
alignment: .bottom,
|
systemIcon: .Icon.person,
|
||||||
spacing: 8
|
title: item.author
|
||||||
) {
|
)
|
||||||
Image(systemName: "person.crop.circle")
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Text(item.author)
|
|
||||||
.font(.subheadline)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
Text(item.title)
|
Text(item.title)
|
||||||
@ -47,28 +44,21 @@ struct FeedItem: View {
|
|||||||
}
|
}
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
|
||||||
HStack(alignment: .bottom) {
|
HStack(
|
||||||
ForEach(1...5, id: \.self) { index in
|
alignment: .center,
|
||||||
if #available(iOS 15.0, *) {
|
spacing: 32
|
||||||
Image(systemName: "star")
|
) {
|
||||||
.symbolVariant(index <= item.rating.stars ? .fill : .none)
|
StarRating(
|
||||||
} else {
|
item.rating.stars,
|
||||||
Image(systemName: index <= item.rating.stars ? "star.fill" : "star")
|
of: .Rating.total
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack(
|
FakeLabel(
|
||||||
alignment: .bottom,
|
systemIcon: .Icon.info,
|
||||||
spacing: 4
|
title: item.rating.appVersion
|
||||||
) {
|
)
|
||||||
Text(item.rating.appVersion)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Image(systemName: "iphone.gen3.circle")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.font(.subheadline)
|
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
@ -76,8 +66,8 @@ struct FeedItem: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
#Preview("Feed Item") {
|
#Preview("Feed Item Cell") {
|
||||||
FeedItem(.init(
|
FeedItemCell(.init(
|
||||||
author: "Some author name here...",
|
author: "Some author name here...",
|
||||||
comment: "Some review comment here...",
|
comment: "Some review comment here...",
|
||||||
id: 0,
|
id: 0,
|
||||||
@ -87,4 +77,5 @@ struct FeedItem: View {
|
|||||||
),
|
),
|
||||||
title: "Some review title here..."
|
title: "Some review title here..."
|
||||||
))
|
))
|
||||||
|
.padding(.horizontal)
|
||||||
}
|
}
|
@ -1,79 +0,0 @@
|
|||||||
//
|
|
||||||
// AppDelegate.swift
|
|
||||||
// ReviewsFeed
|
|
||||||
//
|
|
||||||
// Created by Dmitrii Ivanov on 21/07/2020.
|
|
||||||
// Copyright © 2020 ING. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class DetailsViewController: UIViewController {
|
|
||||||
|
|
||||||
private let review: Review
|
|
||||||
|
|
||||||
private var titleLabel = UILabel()
|
|
||||||
private var authorLabel = UILabel()
|
|
||||||
private var contentLabel = UILabel()
|
|
||||||
private var ratingVersionLabel = UILabel()
|
|
||||||
|
|
||||||
init(review: Review) {
|
|
||||||
self.review = review
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
view.backgroundColor = UIColor.white
|
|
||||||
setupViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupViews() {
|
|
||||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
authorLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
contentLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
ratingVersionLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
view.addSubview(ratingVersionLabel)
|
|
||||||
view.addSubview(authorLabel)
|
|
||||||
view.addSubview(titleLabel)
|
|
||||||
view.addSubview(contentLabel)
|
|
||||||
|
|
||||||
ratingVersionLabel.text = review.rating.appVersion
|
|
||||||
ratingVersionLabel.font = UIFont.italicSystemFont(ofSize: 18)
|
|
||||||
|
|
||||||
authorLabel.text = review.author
|
|
||||||
authorLabel.font = UIFont.systemFont(ofSize: 18)
|
|
||||||
|
|
||||||
titleLabel.text = review.title
|
|
||||||
titleLabel.numberOfLines = 0
|
|
||||||
titleLabel.font = UIFont.boldSystemFont(ofSize: 22)
|
|
||||||
|
|
||||||
contentLabel.text = review.comment
|
|
||||||
contentLabel.numberOfLines = 0
|
|
||||||
|
|
||||||
ratingVersionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8).isActive = true
|
|
||||||
ratingVersionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8).isActive = true
|
|
||||||
ratingVersionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
|
|
||||||
ratingVersionLabel.heightAnchor.constraint(equalToConstant: 24).isActive = true
|
|
||||||
|
|
||||||
authorLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8).isActive = true
|
|
||||||
authorLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8).isActive = true
|
|
||||||
authorLabel.topAnchor.constraint(equalTo: ratingVersionLabel.bottomAnchor, constant: 8).isActive = true
|
|
||||||
authorLabel.heightAnchor.constraint(equalToConstant: 24).isActive = true
|
|
||||||
|
|
||||||
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8).isActive = true
|
|
||||||
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8).isActive = true
|
|
||||||
titleLabel.topAnchor.constraint(equalTo: authorLabel.bottomAnchor, constant: 8).isActive = true
|
|
||||||
titleLabel.heightAnchor.constraint(lessThanOrEqualToConstant: 72).isActive = true
|
|
||||||
|
|
||||||
contentLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8).isActive = true
|
|
||||||
contentLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8).isActive = true
|
|
||||||
contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8).isActive = true
|
|
||||||
contentLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 24).isActive = true
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,225 @@
|
|||||||
|
//
|
||||||
|
// FeedItemViewController.swift
|
||||||
|
// ReviewsFeed
|
||||||
|
//
|
||||||
|
// Created by Dmitrii Ivanov on 21/07/2020.
|
||||||
|
// Copyright © 2020 ING. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import ReviewsUIKit
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class FeedItemViewController: UIViewController {
|
||||||
|
|
||||||
|
// MARK: Constants
|
||||||
|
private let item: Review
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
private lazy var appVersionController = {
|
||||||
|
UIHostingController(rootView: FakeLabel(
|
||||||
|
systemIcon: .Icon.info,
|
||||||
|
title: item.rating.appVersion
|
||||||
|
))
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var authorController = {
|
||||||
|
UIHostingController(rootView: FakeLabel(
|
||||||
|
systemIcon: .Icon.person,
|
||||||
|
title: item.author
|
||||||
|
))
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var starRatingController = {
|
||||||
|
UIHostingController(rootView: StarRating(
|
||||||
|
item.rating.stars,
|
||||||
|
of: .Rating.total
|
||||||
|
))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: Outlets
|
||||||
|
private lazy var appVersionView = {
|
||||||
|
guard let view = appVersionController.view else {
|
||||||
|
fatalError("The StarRating component must be initialised")
|
||||||
|
}
|
||||||
|
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var authorView = {
|
||||||
|
guard let view = authorController.view else {
|
||||||
|
fatalError("The StarRating component must be initialised")
|
||||||
|
}
|
||||||
|
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var commentLabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
label.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
label.numberOfLines = 0
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
label.text = item.comment
|
||||||
|
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var ratingView = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.backgroundColor = .clear
|
||||||
|
stack.distribution = .fillProportionally
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
stack.addArrangedSubview(starRatingView)
|
||||||
|
stack.addArrangedSubview(appVersionView)
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var scrollView = {
|
||||||
|
let scroll = UIScrollView()
|
||||||
|
|
||||||
|
scroll.backgroundColor = .clear
|
||||||
|
scroll.showsVerticalScrollIndicator = true
|
||||||
|
scroll.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return scroll
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var stackView = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.alignment = .leading
|
||||||
|
stack.backgroundColor = .clear
|
||||||
|
stack.distribution = .fill
|
||||||
|
stack.spacing = 16
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var starRatingView = {
|
||||||
|
guard let view = starRatingController.view else {
|
||||||
|
fatalError("The StarRating component must be initialised")
|
||||||
|
}
|
||||||
|
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var titleLabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
label.font = UIFont.preferredFont(forTextStyle: .headline)
|
||||||
|
label.numberOfLines = 0
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
label.text = item.title
|
||||||
|
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
init(_ item: Review) {
|
||||||
|
self.item = item
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: UIViewController
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
addChild(appVersionController)
|
||||||
|
addChild(authorController)
|
||||||
|
addChild(starRatingController)
|
||||||
|
|
||||||
|
setView()
|
||||||
|
setNavigationBar()
|
||||||
|
setLayout()
|
||||||
|
|
||||||
|
appVersionController.didMove(toParent: self)
|
||||||
|
authorController.didMove(toParent: self)
|
||||||
|
starRatingController.didMove(toParent: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
private extension FeedItemViewController {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
func setLayout() {
|
||||||
|
let scrollContentGuide = scrollView.contentLayoutGuide
|
||||||
|
let scrollFrameGuide = scrollView.frameLayoutGuide
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: scrollContentGuide.bottomAnchor, constant: -16),
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: scrollContentGuide.leadingAnchor),
|
||||||
|
stackView.topAnchor.constraint(equalTo: scrollContentGuide.topAnchor, constant: 8),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: scrollContentGuide.trailingAnchor),
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: scrollFrameGuide.leadingAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: scrollFrameGuide.trailingAnchor),
|
||||||
|
|
||||||
|
authorView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 16),
|
||||||
|
|
||||||
|
ratingView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 16),
|
||||||
|
|
||||||
|
titleLabel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 16),
|
||||||
|
titleLabel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -16),
|
||||||
|
|
||||||
|
commentLabel.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 16),
|
||||||
|
commentLabel.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -16),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func setNavigationBar() {
|
||||||
|
navigationController?.navigationBar.prefersLargeTitles = true
|
||||||
|
navigationController?.navigationBar.isTranslucent = true
|
||||||
|
|
||||||
|
navigationItem.title = "# \(String(item.id))"
|
||||||
|
}
|
||||||
|
|
||||||
|
func setView() {
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
|
view.addSubview(scrollView)
|
||||||
|
|
||||||
|
scrollView.addSubview(stackView)
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(authorView)
|
||||||
|
stackView.addArrangedSubview(ratingView)
|
||||||
|
stackView.addArrangedSubview(titleLabel)
|
||||||
|
stackView.addArrangedSubview(commentLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
@available(iOS 17.0, *)
|
||||||
|
#Preview("Feed Item with a review") {
|
||||||
|
UINavigationController(rootViewController: FeedItemViewController(.init(
|
||||||
|
author: "Some author name here...",
|
||||||
|
comment: "Some long, explanatory review comment goes here...",
|
||||||
|
id: 1,
|
||||||
|
rating: .init(stars: 3, appVersion: "v1.0.0"),
|
||||||
|
title: "Some review title goes here..."
|
||||||
|
)))
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// AppDelegate.swift
|
// FeedListViewController.swift
|
||||||
// ReviewsFeed
|
// ReviewsFeed
|
||||||
//
|
//
|
||||||
// Created by Dmitrii Ivanov on 21/07/2020.
|
// Created by Dmitrii Ivanov on 21/07/2020.
|
||||||
@ -11,7 +11,7 @@ import ReviewsUIKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public class FeedViewController: UITableViewController {
|
public class FeedListViewController: UITableViewController {
|
||||||
|
|
||||||
// MARK: Constants
|
// MARK: Constants
|
||||||
private let viewModel: ViewModel
|
private let viewModel: ViewModel
|
||||||
@ -33,12 +33,9 @@ public class FeedViewController: UITableViewController {
|
|||||||
// MARK: UIViewController
|
// MARK: UIViewController
|
||||||
public override func viewDidLoad() {
|
public override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
tableView.register(
|
|
||||||
UITableViewCell.self,
|
|
||||||
forCellReuseIdentifier: .Cell.feedItem
|
|
||||||
)
|
|
||||||
|
|
||||||
|
setNavigationBar()
|
||||||
|
registerTableCells()
|
||||||
bindViewModel()
|
bindViewModel()
|
||||||
|
|
||||||
viewModel.fetch()
|
viewModel.fetch()
|
||||||
@ -63,11 +60,11 @@ public class FeedViewController: UITableViewController {
|
|||||||
cell.contentConfiguration = {
|
cell.contentConfiguration = {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
UIHostingConfiguration {
|
UIHostingConfiguration {
|
||||||
FeedItem(viewModel.items[indexPath.row])
|
FeedItemCell(viewModel.items[indexPath.row])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
HostingConfiguration {
|
HostingConfiguration {
|
||||||
FeedItem(viewModel.items[indexPath.row])
|
FeedItemCell(viewModel.items[indexPath.row])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -80,7 +77,7 @@ public class FeedViewController: UITableViewController {
|
|||||||
_ tableView: UITableView,
|
_ tableView: UITableView,
|
||||||
didSelectRowAt indexPath: IndexPath
|
didSelectRowAt indexPath: IndexPath
|
||||||
) {
|
) {
|
||||||
let details = DetailsViewController(review: viewModel.items[indexPath.row])
|
let details = FeedItemViewController(viewModel.items[indexPath.row])
|
||||||
|
|
||||||
tableView.deselectRow(
|
tableView.deselectRow(
|
||||||
at: indexPath,
|
at: indexPath,
|
||||||
@ -93,7 +90,7 @@ public class FeedViewController: UITableViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
private extension FeedViewController {
|
private extension FeedListViewController {
|
||||||
|
|
||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
func bindViewModel() {
|
func bindViewModel() {
|
||||||
@ -113,10 +110,21 @@ private extension FeedViewController {
|
|||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerTableCells() {
|
||||||
|
tableView.register(UITableViewCell.self, forCellReuseIdentifier: .Cell.feedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setNavigationBar() {
|
||||||
|
navigationController?.navigationBar.prefersLargeTitles = true
|
||||||
|
navigationController?.navigationBar.isTranslucent = true
|
||||||
|
|
||||||
|
navigationItem.title = "Latest reviews"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Configuration
|
// MARK: - Configuration
|
||||||
extension FeedViewController {
|
extension FeedListViewController {
|
||||||
public struct Configuration {
|
public struct Configuration {
|
||||||
|
|
||||||
// MARK: Constants
|
// MARK: Constants
|
||||||
@ -151,7 +159,16 @@ import ReviewsFoundationKit
|
|||||||
import ReviewsiTunesKit
|
import ReviewsiTunesKit
|
||||||
|
|
||||||
@available(iOS 17.0, *)
|
@available(iOS 17.0, *)
|
||||||
#Preview("Feed View Controller with few reviews") {
|
#Preview("Feed List loading reviews") {
|
||||||
|
MockURLProtocol.response = .init(statusCode: 200)
|
||||||
|
|
||||||
|
return UINavigationController(
|
||||||
|
rootViewController: FeedListViewController(configuration: .init(session: .mock))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 17.0, *)
|
||||||
|
#Preview("Feed List with few reviews") {
|
||||||
MockURLProtocol.response = .init(
|
MockURLProtocol.response = .init(
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
object: Feed(entries: [
|
object: Feed(entries: [
|
||||||
@ -182,19 +199,46 @@ import ReviewsiTunesKit
|
|||||||
version: "v1.0.0",
|
version: "v1.0.0",
|
||||||
updated: .init()
|
updated: .init()
|
||||||
),
|
),
|
||||||
|
.init(
|
||||||
|
id: 4,
|
||||||
|
author: "Some author name #4 here",
|
||||||
|
title: "Some review title #4 goes here...",
|
||||||
|
content: "Some long, explanatory review comment #4 goes here...",
|
||||||
|
rating: 4,
|
||||||
|
version: "v1.0.0",
|
||||||
|
updated: .init()
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
id: 5,
|
||||||
|
author: "Some author name #5 here",
|
||||||
|
title: "Some review title #5 goes here...",
|
||||||
|
content: "Some long, explanatory review comment #5 goes here...",
|
||||||
|
rating: 2,
|
||||||
|
version: "v1.0.0",
|
||||||
|
updated: .init()
|
||||||
|
),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
return FeedViewController(configuration: .init(session: .mock))
|
return UINavigationController(
|
||||||
|
rootViewController: FeedListViewController(configuration: .init(session: .mock))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 17.0, *)
|
@available(iOS 17.0, *)
|
||||||
#Preview("Feed View Controller with no reviews") {
|
#Preview("Feed List with no reviews") {
|
||||||
MockURLProtocol.response = .init(
|
MockURLProtocol.response = .init(
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
object: Feed(entries: [])
|
object: Feed(entries: [])
|
||||||
)
|
)
|
||||||
|
|
||||||
return FeedViewController(configuration: .init(session: .mock))
|
return UINavigationController(
|
||||||
|
rootViewController: FeedListViewController(configuration: .init(session: .mock))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 17.0, *)
|
||||||
|
#Preview("Feed List with live reviews") {
|
||||||
|
UINavigationController(rootViewController: FeedListViewController())
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
95
Libraries/UI/Kit/Sources/Components/FakeLabel.swift
Normal file
95
Libraries/UI/Kit/Sources/Components/FakeLabel.swift
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
//
|
||||||
|
// FakeLabel.swift
|
||||||
|
// ReviewsUIKit
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 20/03/2024.
|
||||||
|
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import ReviewsFoundationKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct FakeLabel: View {
|
||||||
|
|
||||||
|
// MARK: Constants
|
||||||
|
private let systemIcon: String
|
||||||
|
private let title: String
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
public init(
|
||||||
|
systemIcon: String,
|
||||||
|
title: String
|
||||||
|
) {
|
||||||
|
self.systemIcon = systemIcon
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Body
|
||||||
|
public var body: some View {
|
||||||
|
HStack(
|
||||||
|
alignment: .bottom,
|
||||||
|
spacing: 8
|
||||||
|
) {
|
||||||
|
iconOrQuestionMark
|
||||||
|
|
||||||
|
Text(titleOrDash)
|
||||||
|
.font(.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
private extension FakeLabel {
|
||||||
|
|
||||||
|
// MARK: Computed
|
||||||
|
var iconOrQuestionMark: Image {
|
||||||
|
.init(systemName: systemIcon.isEmpty
|
||||||
|
? .Icon.questionMark
|
||||||
|
: systemIcon
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleOrDash: String {
|
||||||
|
title.isEmpty
|
||||||
|
? .Title.none
|
||||||
|
: title
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String+Constants
|
||||||
|
private extension String {
|
||||||
|
enum Title {
|
||||||
|
static let none = "-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
#Preview("Fake Label with system icon and title") {
|
||||||
|
FakeLabel(
|
||||||
|
systemIcon: .Icon.person,
|
||||||
|
title: "Some title goes here..."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Fake Label with empty system icon and title") {
|
||||||
|
FakeLabel(
|
||||||
|
systemIcon: .empty,
|
||||||
|
title: "Some title goes here..."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Fake Label with system icon and empty title") {
|
||||||
|
FakeLabel(
|
||||||
|
systemIcon: .Icon.person,
|
||||||
|
title: .empty
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Fake Label with both empty system icon and title") {
|
||||||
|
FakeLabel(
|
||||||
|
systemIcon: .empty,
|
||||||
|
title: .empty
|
||||||
|
)
|
||||||
|
}
|
81
Libraries/UI/Kit/Sources/Components/StarRating.swift
Normal file
81
Libraries/UI/Kit/Sources/Components/StarRating.swift
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// StarRating.swift
|
||||||
|
// ReviewsUIKit
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 19/03/2024.
|
||||||
|
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct StarRating: View {
|
||||||
|
|
||||||
|
// MARK: Constants
|
||||||
|
private let rating: Int
|
||||||
|
private let total: Int
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
public init(
|
||||||
|
_ rating: Int,
|
||||||
|
of total: Int
|
||||||
|
) {
|
||||||
|
self.rating = rating
|
||||||
|
self.total = total
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Body
|
||||||
|
public var body: some View {
|
||||||
|
HStack {
|
||||||
|
ForEach(
|
||||||
|
1...total,
|
||||||
|
id: \.self
|
||||||
|
) { index in
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
Image.Icon.star
|
||||||
|
.symbolVariant(symbolVariant(for: index))
|
||||||
|
} else {
|
||||||
|
image(for: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
private extension StarRating {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
func image(for index: Int) -> Image {
|
||||||
|
index <= rating
|
||||||
|
? .Icon.starFill
|
||||||
|
: .Icon.star
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 15.0, *)
|
||||||
|
func symbolVariant(for index: Int) -> SymbolVariants {
|
||||||
|
index <= rating
|
||||||
|
? .fill
|
||||||
|
: .none
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Image+Constants
|
||||||
|
private extension Image {
|
||||||
|
enum Icon {
|
||||||
|
static let star: Image = .init(systemName: "star")
|
||||||
|
static let starFill: Image = .init(systemName: "star.fill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
#Preview("Star Rating") {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
StarRating(1, of: 5)
|
||||||
|
StarRating(2, of: 5)
|
||||||
|
StarRating(3, of: 5)
|
||||||
|
StarRating(4, of: 5)
|
||||||
|
StarRating(5, of: 5)
|
||||||
|
}
|
||||||
|
}
|
18
Libraries/UI/Kit/Sources/Extensions/String+Icons.swift
Normal file
18
Libraries/UI/Kit/Sources/Extensions/String+Icons.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// String+Icons.swift
|
||||||
|
// ReviewsUIKit
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 20/03/2024.
|
||||||
|
// Copyright © 2024 Röck+Cöde VoF. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
public extension String {
|
||||||
|
enum Icon {
|
||||||
|
|
||||||
|
// MARK: Constants
|
||||||
|
static let questionMark = "questionmark.circle.fill"
|
||||||
|
|
||||||
|
public static let info = "info.circle.fill"
|
||||||
|
public static let person = "person.crop.circle.fill"
|
||||||
|
}
|
||||||
|
}
|
@ -7,13 +7,14 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
0220ADA32BA90646001E6A9F /* FeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0220ADA22BA90646001E6A9F /* FeedItem.swift */; };
|
0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0220ADA22BA90646001E6A9F /* FeedItemCell.swift */; };
|
||||||
|
023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */; };
|
||||||
02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */; };
|
02620B8C2BA89C9A00DE7137 /* 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 /* FeedItemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13224C6EE64004E2EE1 /* FeedItemViewController.swift */; };
|
||||||
02DC7FAE2BA51B4C000EEEBE /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */; };
|
02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD12F24C6EE64004E2EE1 /* FeedListViewController.swift */; };
|
||||||
02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345AD13124C6EE64004E2EE1 /* Review.swift */; };
|
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 */; };
|
||||||
02DC7FB52BA52520000EEEBE /* ReviewsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 02DC7FB42BA52520000EEEBE /* ReviewsKit */; };
|
02DC7FB52BA52520000EEEBE /* ReviewsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 02DC7FB42BA52520000EEEBE /* ReviewsKit */; };
|
||||||
@ -47,7 +48,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>"; };
|
0220ADA22BA90646001E6A9F /* FeedItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemCell.swift; sourceTree = "<group>"; };
|
||||||
|
023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Constants.swift"; sourceTree = "<group>"; };
|
||||||
02620B8B2BA89C9A00DE7137 /* FeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewModel.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>"; };
|
||||||
@ -57,9 +59,9 @@
|
|||||||
345AD12424C6EDDC004E2EE1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
345AD12424C6EDDC004E2EE1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
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 /* FeedListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedListViewController.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 /* FeedItemViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedItemViewController.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -83,6 +85,22 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
0220ADA72BA98F8B001E6A9F /* Cells */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
0220ADA22BA90646001E6A9F /* FeedItemCell.swift */,
|
||||||
|
);
|
||||||
|
path = Cells;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
023AC7FA2BAA3EB60027D064 /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
02620B852BA89BF900DE7137 /* UI */ = {
|
02620B852BA89BF900DE7137 /* UI */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -95,6 +113,7 @@
|
|||||||
02620B862BA89C0000DE7137 /* Logic */ = {
|
02620B862BA89C0000DE7137 /* Logic */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
023AC7FA2BAA3EB60027D064 /* Extensions */,
|
||||||
02620B8A2BA89C3300DE7137 /* Models */,
|
02620B8A2BA89C3300DE7137 /* Models */,
|
||||||
02620B872BA89C0700DE7137 /* View Models */,
|
02620B872BA89C0700DE7137 /* View Models */,
|
||||||
);
|
);
|
||||||
@ -112,8 +131,8 @@
|
|||||||
02620B882BA89C1000DE7137 /* View Controllers */ = {
|
02620B882BA89C1000DE7137 /* View Controllers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
345AD13224C6EE64004E2EE1 /* DetailsViewController.swift */,
|
345AD13224C6EE64004E2EE1 /* FeedItemViewController.swift */,
|
||||||
345AD12F24C6EE64004E2EE1 /* FeedViewController.swift */,
|
345AD12F24C6EE64004E2EE1 /* FeedListViewController.swift */,
|
||||||
);
|
);
|
||||||
path = "View Controllers";
|
path = "View Controllers";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -121,7 +140,7 @@
|
|||||||
02620B892BA89C2400DE7137 /* Components */ = {
|
02620B892BA89C2400DE7137 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
0220ADA22BA90646001E6A9F /* FeedItem.swift */,
|
0220ADA72BA98F8B001E6A9F /* Cells */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -366,10 +385,11 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */,
|
02620B8C2BA89C9A00DE7137 /* FeedViewModel.swift in Sources */,
|
||||||
02DC7FAC2BA51B4C000EEEBE /* DetailsViewController.swift in Sources */,
|
023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */,
|
||||||
0220ADA32BA90646001E6A9F /* FeedItem.swift in Sources */,
|
02DC7FAC2BA51B4C000EEEBE /* FeedItemViewController.swift in Sources */,
|
||||||
|
0220ADA32BA90646001E6A9F /* FeedItemCell.swift in Sources */,
|
||||||
02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */,
|
02DC7FAF2BA51B4C000EEEBE /* Review.swift in Sources */,
|
||||||
02DC7FAE2BA51B4C000EEEBE /* FeedViewController.swift in Sources */,
|
02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user