[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: rock-n-code/app-reviews#10 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #10.
This commit is contained in:
@@ -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 ReviewsiTunesKit
|
||||
|
||||
extension FeedViewController {
|
||||
extension FeedListViewController {
|
||||
final class ViewModel: ObservableObject {
|
||||
|
||||
// MARK: Type aliases
|
||||
typealias Configuration = FeedViewController.Configuration
|
||||
typealias Configuration = FeedListViewController.Configuration
|
||||
|
||||
// MARK: Constants
|
||||
private let configuration: Configuration
|
||||
|
||||
+24
-33
@@ -1,14 +1,15 @@
|
||||
//
|
||||
// FeedItem.swift
|
||||
// FeedItemCell.swift
|
||||
// ReviewsFeed
|
||||
//
|
||||
// Created by Javier Cicchelli on 19/03/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import ReviewsUIKit
|
||||
import SwiftUI
|
||||
|
||||
struct FeedItem: View {
|
||||
struct FeedItemCell: View {
|
||||
|
||||
// MARK: Constants
|
||||
private let item: Review
|
||||
@@ -24,15 +25,11 @@ struct FeedItem: View {
|
||||
alignment: .leading,
|
||||
spacing: 16
|
||||
) {
|
||||
HStack(
|
||||
alignment: .bottom,
|
||||
spacing: 8
|
||||
) {
|
||||
Image(systemName: "person.crop.circle")
|
||||
|
||||
Text(item.author)
|
||||
.font(.subheadline)
|
||||
}
|
||||
FakeLabel(
|
||||
systemIcon: .Icon.person,
|
||||
title: item.author
|
||||
)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(item.title)
|
||||
@@ -47,28 +44,21 @@ struct FeedItem: View {
|
||||
}
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
HStack(alignment: .bottom) {
|
||||
ForEach(1...5, id: \.self) { index in
|
||||
if #available(iOS 15.0, *) {
|
||||
Image(systemName: "star")
|
||||
.symbolVariant(index <= item.rating.stars ? .fill : .none)
|
||||
} else {
|
||||
Image(systemName: index <= item.rating.stars ? "star.fill" : "star")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
HStack(
|
||||
alignment: .center,
|
||||
spacing: 32
|
||||
) {
|
||||
StarRating(
|
||||
item.rating.stars,
|
||||
of: .Rating.total
|
||||
)
|
||||
|
||||
HStack(
|
||||
alignment: .bottom,
|
||||
spacing: 4
|
||||
) {
|
||||
Text(item.rating.appVersion)
|
||||
|
||||
Image(systemName: "iphone.gen3.circle")
|
||||
}
|
||||
FakeLabel(
|
||||
systemIcon: .Icon.info,
|
||||
title: item.rating.appVersion
|
||||
)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
@@ -76,8 +66,8 @@ struct FeedItem: View {
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
#Preview("Feed Item") {
|
||||
FeedItem(.init(
|
||||
#Preview("Feed Item Cell") {
|
||||
FeedItemCell(.init(
|
||||
author: "Some author name here...",
|
||||
comment: "Some review comment here...",
|
||||
id: 0,
|
||||
@@ -87,4 +77,5 @@ struct FeedItem: View {
|
||||
),
|
||||
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..."
|
||||
)))
|
||||
}
|
||||
+60
-16
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// FeedListViewController.swift
|
||||
// ReviewsFeed
|
||||
//
|
||||
// Created by Dmitrii Ivanov on 21/07/2020.
|
||||
@@ -11,7 +11,7 @@ import ReviewsUIKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
public class FeedViewController: UITableViewController {
|
||||
public class FeedListViewController: UITableViewController {
|
||||
|
||||
// MARK: Constants
|
||||
private let viewModel: ViewModel
|
||||
@@ -33,12 +33,9 @@ public class FeedViewController: UITableViewController {
|
||||
// MARK: UIViewController
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.register(
|
||||
UITableViewCell.self,
|
||||
forCellReuseIdentifier: .Cell.feedItem
|
||||
)
|
||||
|
||||
setNavigationBar()
|
||||
registerTableCells()
|
||||
bindViewModel()
|
||||
|
||||
viewModel.fetch()
|
||||
@@ -63,11 +60,11 @@ public class FeedViewController: UITableViewController {
|
||||
cell.contentConfiguration = {
|
||||
if #available(iOS 16.0, *) {
|
||||
UIHostingConfiguration {
|
||||
FeedItem(viewModel.items[indexPath.row])
|
||||
FeedItemCell(viewModel.items[indexPath.row])
|
||||
}
|
||||
} else {
|
||||
HostingConfiguration {
|
||||
FeedItem(viewModel.items[indexPath.row])
|
||||
FeedItemCell(viewModel.items[indexPath.row])
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -80,7 +77,7 @@ public class FeedViewController: UITableViewController {
|
||||
_ tableView: UITableView,
|
||||
didSelectRowAt indexPath: IndexPath
|
||||
) {
|
||||
let details = DetailsViewController(review: viewModel.items[indexPath.row])
|
||||
let details = FeedItemViewController(viewModel.items[indexPath.row])
|
||||
|
||||
tableView.deselectRow(
|
||||
at: indexPath,
|
||||
@@ -93,7 +90,7 @@ public class FeedViewController: UITableViewController {
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
private extension FeedViewController {
|
||||
private extension FeedListViewController {
|
||||
|
||||
// MARK: Functions
|
||||
func bindViewModel() {
|
||||
@@ -113,10 +110,21 @@ private extension FeedViewController {
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func registerTableCells() {
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: .Cell.feedItem)
|
||||
}
|
||||
|
||||
func setNavigationBar() {
|
||||
navigationController?.navigationBar.prefersLargeTitles = true
|
||||
navigationController?.navigationBar.isTranslucent = true
|
||||
|
||||
navigationItem.title = "Latest reviews"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Configuration
|
||||
extension FeedViewController {
|
||||
extension FeedListViewController {
|
||||
public struct Configuration {
|
||||
|
||||
// MARK: Constants
|
||||
@@ -151,7 +159,16 @@ import ReviewsFoundationKit
|
||||
import ReviewsiTunesKit
|
||||
|
||||
@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(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: [
|
||||
@@ -182,19 +199,46 @@ import ReviewsiTunesKit
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
.init(
|
||||
id: 4,
|
||||
author: "Some author name #4 here",
|
||||
title: "Some review title #4 goes here...",
|
||||
content: "Some long, explanatory review comment #4 goes here...",
|
||||
rating: 4,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
.init(
|
||||
id: 5,
|
||||
author: "Some author name #5 here",
|
||||
title: "Some review title #5 goes here...",
|
||||
content: "Some long, explanatory review comment #5 goes here...",
|
||||
rating: 2,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
])
|
||||
)
|
||||
|
||||
return FeedViewController(configuration: .init(session: .mock))
|
||||
return UINavigationController(
|
||||
rootViewController: FeedListViewController(configuration: .init(session: .mock))
|
||||
)
|
||||
}
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
#Preview("Feed View Controller with no reviews") {
|
||||
#Preview("Feed List with no reviews") {
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: [])
|
||||
)
|
||||
|
||||
return 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
|
||||
Reference in New Issue
Block a user