[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,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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user