[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: rock-n-code/app-reviews#9 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 #9.
This commit is contained in:
@@ -15,6 +15,7 @@ let package = Package(
|
||||
.Target.filter.kit,
|
||||
.Target.foundation.kit,
|
||||
.Target.iTunes.kit,
|
||||
.Target.ui.kit,
|
||||
]
|
||||
),
|
||||
],
|
||||
@@ -45,6 +46,13 @@ let package = Package(
|
||||
],
|
||||
path: "iTunes/Kit"
|
||||
),
|
||||
.target(
|
||||
name: .Target.ui.kit,
|
||||
dependencies: [
|
||||
.byName(name: .Target.foundation.kit),
|
||||
],
|
||||
path: "UI/Kit"
|
||||
),
|
||||
.testTarget(
|
||||
name: .Target.feed.test,
|
||||
dependencies: [
|
||||
@@ -73,6 +81,13 @@ let package = Package(
|
||||
],
|
||||
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 foundation = "\(String.Product.name)Foundation"
|
||||
static let iTunes = "\(String.Product.name)iTunes"
|
||||
static let ui = "\(String.Product.name)UI"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
// MARK: Initialisers
|
||||
init(session: URLSessionConfiguration = .ephemeral) {
|
||||
public init(session: URLSessionConfiguration = .ephemeral) {
|
||||
self.init(
|
||||
host: .iTunes,
|
||||
session: session,
|
||||
|
||||
@@ -8,11 +8,16 @@
|
||||
|
||||
import ReviewsFeedKit
|
||||
|
||||
struct Feed {
|
||||
public struct Feed {
|
||||
|
||||
// MARK: Constants
|
||||
let entries: [Review]
|
||||
|
||||
// MARK: Initialisers
|
||||
public init(entries: [Review]) {
|
||||
self.entries = entries
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Decodable
|
||||
@@ -28,7 +33,7 @@ extension Feed: Decodable {
|
||||
}
|
||||
|
||||
// MARK: Initialisers
|
||||
init(from decoder: any Decoder) throws {
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let feed = try decoder.container(keyedBy: FeedKeys.self)
|
||||
let feedEntry = try feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed)
|
||||
|
||||
@@ -41,7 +46,7 @@ extension Feed: Decodable {
|
||||
extension Feed: Encodable {
|
||||
|
||||
// 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 feedEntry = feed.nestedContainer(keyedBy: EntryKeys.self, forKey: .feed)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user