[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:
2024-03-20 01:42:21 +00:00
committed by javier
parent 09df006ab9
commit c9f4b9a677
12 changed files with 554 additions and 143 deletions
@@ -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
)
}
@@ -1,100 +0,0 @@
//
// 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)
}
}
@@ -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)
}
}