[Framework] Feed Item coordinator #17

Merged
javier merged 10 commits from framework/feed/coordinators into main 2024-03-21 22:56:39 +00:00
13 changed files with 287 additions and 63 deletions

View File

@ -37,6 +37,7 @@ extension AppDelegate: UIApplicationDelegate {
coordinator.present(animated: false)
coordinator.present(
child: FeedListCoordinator(
configuration: .init(session: .ephemeral),
router: StackRouter(coordinator.navigationController)
),
animated: false

View File

@ -0,0 +1,16 @@
//
// FeedListCoordination.swift
// ReviewsFeed
//
// Created by Javier Cicchelli on 21/03/2024.
// Copyright © 2024 Röck+Cöde. All rights reserved.
//
import Foundation
protocol FeedListCoordination: AnyObject {
// MARK: Functions
func open(_ item: Review)
}

View File

@ -0,0 +1,42 @@
//
// FeedItemCoordinator.swift
// ReviewsFeed
//
// Created by Javier Cicchelli on 21/03/2024.
// Copyright © 2024 Röck+Cöde. All rights reserved.
//
import Foundation
import ReviewsCoordinationKit
public final class FeedItemCoordinator: Coordinator {
// MARK: Constants
public let router: any Router
private let item: Review
// MARK: Properties
public var children: [any Coordinator] = []
public init(
item: Review,
router: any Router
) {
self.item = item
self.router = router
}
// MARK: Functions
public func present(
animated: Bool,
onDismiss: Router.OnDismissClosure? = nil
) {
router.present(
FeedItemViewController(item),
animated: animated,
onDismiss: onDismiss
)
}
}

View File

@ -10,22 +10,29 @@ import Foundation
import ReviewsCoordinationKit
public final class FeedListCoordinator: Coordinator {
// MARK: Constants
public let router: any Router
private let sessionConfiguration: URLSessionConfiguration
private let configuration: FeedListConfiguration
// MARK: Properties
public var children: [any Coordinator] = []
lazy var viewController = {
FeedListViewController(.init(
configuration: configuration,
coordination: self
))
}()
// MARK: Initialisers
public init(
router: any Router,
sessionConfiguration: URLSessionConfiguration = .ephemeral
configuration: FeedListConfiguration,
router: any Router
) {
self.configuration = configuration
self.router = router
self.sessionConfiguration = sessionConfiguration
}
// MARK: Functions
@ -34,12 +41,26 @@ public final class FeedListCoordinator: Coordinator {
onDismiss: Router.OnDismissClosure? = nil
) {
router.present(
FeedListViewController(configuration: .init(
session: sessionConfiguration
)),
viewController,
animated: animated,
onDismiss: onDismiss
)
}
}
// MARK: - FeedListCoordination
extension FeedListCoordinator: FeedListCoordination {
// MARK: Functions
func open(_ item: Review) {
present(
child: FeedItemCoordinator(
item: item,
router: SheetRouter(parentViewController: viewController)
),
animated: true
)
}
}

View File

@ -8,7 +8,7 @@
import Foundation
struct Review {
public struct Review {
// MARK: Constants
let author: String

View File

@ -0,0 +1,29 @@
//
// FeedListConfiguration.swift
// ReviewsFeed
//
// Created by Javier Cicchelli on 21/03/2024.
// Copyright © 2024 Röck+Cöde. All rights reserved.
//
import Foundation
public struct FeedListConfiguration {
// MARK: Constants
let appID: String
let countryCode: String
let session: URLSessionConfiguration
// MARK: Initialisers
public init(
appID: String = "474495017",
countryCode: String = "nl",
session: URLSessionConfiguration = .ephemeral
) {
self.appID = appID
self.countryCode = countryCode
self.session = session
}
}

View File

@ -13,11 +13,8 @@ import ReviewsiTunesKit
extension FeedListViewController {
final class ViewModel: ObservableObject {
// MARK: Type aliases
typealias Configuration = FeedListViewController.Configuration
// MARK: Constants
private let configuration: Configuration
private let configuration: FeedListConfiguration
private let filterWords: FilterWordsUseCase = .init()
private let topWords: TopWordsUseCase = .init()
@ -30,18 +27,24 @@ extension FeedListViewController {
var items: [Review] = []
var words: [TopWord] = []
private var reviewsAll: [Review] = []
private var reviewsFiltered: FilteredReviews = [:]
private var reviewsTopWords: TopWordsReviews = [:]
private weak var coordination: FeedListCoordination?
lazy private var iTunesService: iTunesService = {
.init(configuration: .init(session: configuration.session))
}()
// MARK: Initialisers
init(configuration: Configuration = .init()) {
init(
configuration: FeedListConfiguration = .init(),
coordination: FeedListCoordination? = nil
) {
self.configuration = configuration
self.coordination = coordination
}
// MARK: Computed
@ -114,6 +117,12 @@ extension FeedListViewController {
? items[index - 1]
: items[index]
}
func openItem(at index: Int) {
guard let item = item(for: index) else { return }
coordination?.open(item)
}
}
}

View File

@ -13,7 +13,7 @@ import ReviewsUIKit
import SwiftUI
import UIKit
public class FeedListViewController: UITableViewController {
final class FeedListViewController: UITableViewController {
// MARK: Constants
private let viewModel: ViewModel
@ -79,8 +79,8 @@ public class FeedListViewController: UITableViewController {
}()
// MARK: Initialisers
public init(configuration: Configuration = .init()) {
self.viewModel = .init(configuration: configuration)
init(_ viewModel: ViewModel) {
self.viewModel = viewModel
super.init(style: .plain)
}
@ -95,7 +95,7 @@ public class FeedListViewController: UITableViewController {
}
// MARK: UIViewController
public override func viewDidLoad() {
override func viewDidLoad() {
super.viewDidLoad()
setNavigationBar()
@ -108,14 +108,14 @@ public class FeedListViewController: UITableViewController {
}
// MARK: UITableViewDataSource
public override func tableView(
override func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int
) -> Int {
viewModel.itemsCount
}
public override func tableView(
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
@ -127,21 +127,16 @@ public class FeedListViewController: UITableViewController {
}
// MARK: UITableViewDelegate
public override func tableView(
override func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath
) {
guard let item = viewModel.item(for: indexPath.row) else { return }
viewModel.openItem(at: indexPath.row)
tableView.deselectRow(
at: indexPath,
animated: true
)
navigationController?.pushViewController(
FeedItemViewController(item),
animated: true
)
}
}
@ -292,29 +287,6 @@ private extension FeedListViewController {
}
// MARK: - Configuration
extension FeedListViewController {
public struct Configuration {
// MARK: Constants
let appID: String
let countryCode: String
let session: URLSessionConfiguration
// MARK: Initialisers
public init(
appID: String = "474495017",
countryCode: String = "nl",
session: URLSessionConfiguration = .ephemeral
) {
self.appID = appID
self.countryCode = countryCode
self.session = session
}
}
}
// MARK: - String+Constants
private extension String {
enum Cell {
@ -363,9 +335,9 @@ import ReviewsiTunesKit
#Preview("Feed List loading reviews") {
MockURLProtocol.response = .init(statusCode: 200)
return UINavigationController(
rootViewController: FeedListViewController(configuration: .init(session: .mock))
)
return UINavigationController(rootViewController: FeedListViewController(.init(
configuration: .init(session: .mock)
)))
}
@available(iOS 17.0, *)
@ -421,9 +393,9 @@ import ReviewsiTunesKit
])
)
return UINavigationController(
rootViewController: FeedListViewController(configuration: .init(session: .mock))
)
return UINavigationController(rootViewController: FeedListViewController(.init(
configuration: .init(session: .mock)
)))
}
@available(iOS 17.0, *)
@ -433,13 +405,13 @@ import ReviewsiTunesKit
object: Feed(entries: [])
)
return UINavigationController(
rootViewController: FeedListViewController(configuration: .init(session: .mock))
)
return UINavigationController(rootViewController: FeedListViewController(.init(
configuration: .init(session: .mock)
)))
}
@available(iOS 17.0, *)
#Preview("Feed List with live reviews") {
UINavigationController(rootViewController: FeedListViewController())
return UINavigationController(rootViewController: FeedListViewController(.init()))
}
#endif

View File

@ -0,0 +1,103 @@
//
// SheetRouter.swift
// ReviewsCoordinationKit
//
// Created by Javier Cicchelli on 21/03/2024.
// Copyright © 2024 Röck+Cöde. All rights reserved.
//
import ReviewsUIKit
import UIKit
public class SheetRouter: BaseNavigationRouter {
// MARK: Properties
public unowned let parentViewController: UIViewController
// MARK: Initialisers
public init(parentViewController: UIViewController) {
self.parentViewController = parentViewController
super.init(navigationController: .init())
}
}
// MARK: - Router
extension SheetRouter: Router {
// MARK: Functions
public func present(
_ viewController: UIViewController,
animated: Bool,
onDismiss: Router.OnDismissClosure?
) {
onDismissForViewController[viewController] = onDismiss
if navigationController.viewControllers.isEmpty {
presentModally(
viewController,
animated: animated
)
} else {
navigationController.pushViewController(
viewController,
animated: animated
)
}
}
public func dismiss(animated: Bool) {
guard let firstViewController = navigationController.viewControllers.first else {
return
}
performOnDismiss(for: firstViewController)
parentViewController.dismiss(animated: animated)
}
}
// MARK: - Helpers
private extension SheetRouter {
// MARK: Actions
@objc func onCancelPressed() {
guard let firstViewController = navigationController.viewControllers.first else {
return
}
dismiss(animated: true)
performOnDismiss(for: firstViewController)
}
// MARK: Functions
func presentModally(
_ viewController: UIViewController,
animated: Bool
) {
viewController.navigationItem.rightBarButtonItem = UIBarButtonItem(
image: .Icon.close,
style: .plain,
target: self,
action: #selector(onCancelPressed)
)
if #available(iOS 15.0, *) {
navigationController.sheetPresentationController?.detents = [.medium(), .large()]
}
navigationController.setViewControllers(
[viewController],
animated: false
)
parentViewController.present(
navigationController,
animated: animated
)
}
}

View File

@ -25,6 +25,7 @@ let package = Package(
name: .Target.coordination.kit,
dependencies: [
.byName(name: .Target.foundation.kit),
.byName(name: .Target.ui.kit),
],
path: "Coordination/Kit"
),

View File

@ -19,6 +19,7 @@ public extension String {
public static let star4 = "4.circle"
public static let star5 = "5.circle"
static let close = "xmark.circle.fill"
static let filter = "camera.filters"
static let questionMark = "questionmark.circle.fill"
static let star = "star"

View File

@ -1,5 +1,5 @@
//
// UIImage+ICons.swift
// UIImage+Icons.swift
// ReviewsUIKit
//
// Created by Javier Cicchelli on 20/03/2024.
@ -12,6 +12,7 @@ public extension UIImage {
enum Icon {
// MARK: Constants
public static let close = UIImage(systemName: .Icon.close)
public static let filter = UIImage(systemName: .Icon.filter)
public static let star = UIImage(systemName: .Icon.star)

View File

@ -10,6 +10,9 @@
0220ADA32BA90646001E6A9F /* FeedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0220ADA22BA90646001E6A9F /* FeedItemView.swift */; };
023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */; };
02620B8C2BA89C9A00DE7137 /* FeedListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02620B8B2BA89C9A00DE7137 /* FeedListViewModel.swift */; };
028134712BACC8CC0074AB4B /* FeedListConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028134702BACC8CC0074AB4B /* FeedListConfiguration.swift */; };
028134822BACCC780074AB4B /* FeedListCoordination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028134812BACCC770074AB4B /* FeedListCoordination.swift */; };
028134842BACD0B20074AB4B /* FeedItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028134832BACD0B20074AB4B /* FeedItemCoordinator.swift */; };
02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E782BAB6B0200710E14 /* FilterOption.swift */; };
02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */; };
02909E7D2BAB7FFE00710E14 /* Review+DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */; };
@ -62,6 +65,9 @@
0220ADA22BA90646001E6A9F /* FeedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemView.swift; sourceTree = "<group>"; };
023AC7FB2BAA3EC10027D064 /* Int+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Constants.swift"; sourceTree = "<group>"; };
02620B8B2BA89C9A00DE7137 /* FeedListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListViewModel.swift; sourceTree = "<group>"; };
028134702BACC8CC0074AB4B /* FeedListConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListConfiguration.swift; sourceTree = "<group>"; };
028134812BACCC770074AB4B /* FeedListCoordination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordination.swift; sourceTree = "<group>"; };
028134832BACD0B20074AB4B /* FeedItemCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemCoordinator.swift; sourceTree = "<group>"; };
02909E782BAB6B0200710E14 /* FilterOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterOption.swift; sourceTree = "<group>"; };
02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Constants.swift"; sourceTree = "<group>"; };
02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review+DTOs.swift"; sourceTree = "<group>"; };
@ -134,6 +140,7 @@
02909E772BAB6AD500710E14 /* Enumerations */,
023AC7FA2BAA3EB60027D064 /* Extensions */,
02620B8A2BA89C3300DE7137 /* Models */,
0281346F2BACC8B00074AB4B /* Structs */,
02620B872BA89C0700DE7137 /* View Models */,
);
path = Logic;
@ -174,6 +181,22 @@
path = Models;
sourceTree = "<group>";
};
0281346F2BACC8B00074AB4B /* Structs */ = {
isa = PBXGroup;
children = (
028134702BACC8CC0074AB4B /* FeedListConfiguration.swift */,
);
path = Structs;
sourceTree = "<group>";
};
028134802BACCC630074AB4B /* Coordination */ = {
isa = PBXGroup;
children = (
028134812BACCC770074AB4B /* FeedListCoordination.swift */,
);
path = Coordination;
sourceTree = "<group>";
};
02909E772BAB6AD500710E14 /* Enumerations */ = {
isa = PBXGroup;
children = (
@ -202,6 +225,8 @@
02C1B1952BAC9BE7001781DE /* Coordinators */ = {
isa = PBXGroup;
children = (
028134802BACCC630074AB4B /* Coordination */,
028134832BACD0B20074AB4B /* FeedItemCoordinator.swift */,
02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */,
);
path = Coordinators;
@ -473,6 +498,7 @@
02DC7FAC2BA51B4C000EEEBE /* FeedItemViewController.swift in Sources */,
02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */,
02EACF2E2BABA34600FF8ECD /* FeedItemCell.swift in Sources */,
028134842BACD0B20074AB4B /* FeedItemCoordinator.swift in Sources */,
02909E7D2BAB7FFE00710E14 /* Review+DTOs.swift in Sources */,
0220ADA32BA90646001E6A9F /* FeedItemView.swift in Sources */,
02EACF362BABB2F200FF8ECD /* TopWord+DTOs.swift in Sources */,
@ -480,6 +506,8 @@
02C1B1972BAC9BFE001781DE /* FeedListCoordinator.swift in Sources */,
02DC7FAE2BA51B4C000EEEBE /* FeedListViewController.swift in Sources */,
02EACF322BABB23A00FF8ECD /* TopWordsView.swift in Sources */,
028134712BACC8CC0074AB4B /* FeedListConfiguration.swift in Sources */,
028134822BACCC780074AB4B /* FeedListCoordination.swift in Sources */,
02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;