-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from shinrenpan/develop
bugs / features
- Loading branch information
Showing
11 changed files
with
368 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// | ||
// EpisodeListModels.swift | ||
// | ||
// Created by Shinren Pan on 2024/6/4. | ||
// | ||
|
||
import UIKit | ||
|
||
protocol EpisodeListDelegate: UIViewController { | ||
func list(_ list: EpisodeListVC, selected episode: Comic.Episode) | ||
} | ||
|
||
enum EpisodeListModels {} | ||
|
||
// MARK: - Action | ||
|
||
extension EpisodeListModels { | ||
enum Action { | ||
case loadData | ||
} | ||
} | ||
|
||
// MARK: - State | ||
|
||
extension EpisodeListModels { | ||
enum State { | ||
case none | ||
case dataLoaded(episodes: [Comic.Episode], watchedId: String?) | ||
} | ||
} | ||
|
||
// MARK: - Other Model for DisplayModel | ||
|
||
extension EpisodeListModels { | ||
typealias DataSource = UITableViewDiffableDataSource<Section, Comic.Episode> | ||
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Comic.Episode> | ||
|
||
enum Section { | ||
case main | ||
} | ||
} | ||
|
||
// MARK: - Display Model for ViewModel | ||
|
||
extension EpisodeListModels { | ||
final class DisplayModel { | ||
let comic: Comic | ||
|
||
init(comic: Comic) { | ||
self.comic = comic | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// | ||
// EpisodeListRouter.swift | ||
// | ||
// Created by Shinren Pan on 2024/6/4. | ||
// | ||
|
||
import UIKit | ||
|
||
final class EpisodeListRouter { | ||
weak var vc: EpisodeListVC? | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
// | ||
// EpisodeListVC.swift | ||
// | ||
// Created by Shinren Pan on 2024/6/4. | ||
// | ||
|
||
import Combine | ||
import UIKit | ||
|
||
final class EpisodeListVC: UIViewController { | ||
let vo = EpisodeListVO() | ||
let vm: EpisodeListVM | ||
let router = EpisodeListRouter() | ||
var binding: Set<AnyCancellable> = .init() | ||
weak var delegate: EpisodeListDelegate? | ||
lazy var dataSource = makeDataSource() | ||
|
||
init(comic: Comic) { | ||
self.vm = .init(comic: comic) | ||
super.init(nibName: nil, bundle: nil) | ||
} | ||
|
||
@available(*, unavailable) | ||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
override func viewDidLoad() { | ||
super.viewDidLoad() | ||
setupSelf() | ||
setupBinding() | ||
setupVO() | ||
} | ||
|
||
override func viewIsAppearing(_ animated: Bool) { | ||
super.viewIsAppearing(animated) | ||
vm.doAction(.loadData) | ||
} | ||
} | ||
|
||
// MARK: - Private | ||
|
||
private extension EpisodeListVC { | ||
// MARK: Setup Something | ||
|
||
func setupSelf() { | ||
view.backgroundColor = vo.mainView.backgroundColor | ||
router.vc = self | ||
} | ||
|
||
func setupBinding() { | ||
vm.$state.receive(on: DispatchQueue.main).sink { [weak self] state in | ||
if self?.viewIfLoaded?.window == nil { return } | ||
|
||
switch state { | ||
case .none: | ||
self?.stateNone() | ||
case let .dataLoaded(episodes, watchId): | ||
self?.stateDataLoaded(episodes: episodes, watchId: watchId) | ||
} | ||
}.store(in: &binding) | ||
} | ||
|
||
func setupVO() { | ||
view.addSubview(vo.mainView) | ||
|
||
NSLayoutConstraint.activate([ | ||
vo.mainView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), | ||
vo.mainView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), | ||
vo.mainView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), | ||
vo.mainView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), | ||
]) | ||
|
||
vo.list.dataSource = dataSource | ||
vo.list.delegate = self | ||
} | ||
|
||
// MARK: - Handle State | ||
|
||
func stateNone() {} | ||
|
||
func stateDataLoaded(episodes: [Comic.Episode], watchId: String?) { | ||
let row = episodes.firstIndex(where: { $0.id == watchId }) | ||
var snapshot = EpisodeListModels.Snapshot() | ||
snapshot.appendSections([.main]) | ||
snapshot.appendItems(episodes, toSection: .main) | ||
snapshot.reloadSections([.main]) | ||
|
||
dataSource.apply(snapshot) { | ||
if let row { | ||
self.vo.list.scrollToRow(at: .init(row: row, section: 0), at: .top, animated: true) | ||
} | ||
} | ||
} | ||
|
||
func makeDataSource() -> EpisodeListModels.DataSource { | ||
.init(tableView: vo.list) { [weak self] tableView, indexPath, episode in | ||
let watched = self?.vm.model.comic.watchedId == episode.id | ||
var config = UIListContentConfiguration.cell() | ||
config.text = episode.title | ||
|
||
let cell = tableView.reuseCell(UITableViewCell.self, for: indexPath) | ||
cell.contentConfiguration = config | ||
cell.accessoryType = watched ? .checkmark : .none | ||
|
||
return cell | ||
} | ||
} | ||
} | ||
|
||
// MARK: - UITableViewDelegate | ||
|
||
extension EpisodeListVC: UITableViewDelegate { | ||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | ||
guard let epidose = dataSource.itemIdentifier(for: indexPath) else { | ||
return | ||
} | ||
|
||
delegate?.list(self, selected: epidose) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// | ||
// EpisodeListVM.swift | ||
// | ||
// Created by Shinren Pan on 2024/6/4. | ||
// | ||
|
||
import Combine | ||
import UIKit | ||
|
||
final class EpisodeListVM { | ||
@Published var state = EpisodeListModels.State.none | ||
let model: EpisodeListModels.DisplayModel | ||
|
||
init(comic: Comic) { | ||
self.model = .init(comic: comic) | ||
} | ||
} | ||
|
||
// MARK: - Public | ||
|
||
extension EpisodeListVM { | ||
func doAction(_ action: EpisodeListModels.Action) { | ||
switch action { | ||
case .loadData: | ||
actionLoadData() | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Private | ||
|
||
private extension EpisodeListVM { | ||
// MARK: Do Action | ||
|
||
func actionLoadData() { | ||
let episodes = model.comic.episodes?.sorted(by: { $0.index < $1.index }) ?? [] | ||
state = .dataLoaded(episodes: episodes, watchedId: model.comic.watchedId) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// | ||
// EpisodeListVO.swift | ||
// | ||
// Created by Shinren Pan on 2024/6/4. | ||
// | ||
|
||
import UIKit | ||
|
||
final class EpisodeListVO { | ||
let mainView = UIView(frame: .zero) | ||
.setup(\.translatesAutoresizingMaskIntoConstraints, value: false) | ||
.setup(\.backgroundColor, value: .white) | ||
|
||
let list = UITableView(frame: .zero, style: .plain) | ||
.setup(\.translatesAutoresizingMaskIntoConstraints, value: false) | ||
|
||
init() { | ||
addViews() | ||
} | ||
} | ||
|
||
// MARK: - Private | ||
|
||
private extension EpisodeListVO { | ||
// MARK: Add Something | ||
|
||
func addViews() { | ||
list.registerCell(UITableViewCell.self) | ||
mainView.addSubview(list) | ||
|
||
NSLayoutConstraint.activate([ | ||
list.topAnchor.constraint(equalTo: mainView.topAnchor), | ||
list.leadingAnchor.constraint(equalTo: mainView.leadingAnchor), | ||
list.trailingAnchor.constraint(equalTo: mainView.trailingAnchor), | ||
list.bottomAnchor.constraint(equalTo: mainView.bottomAnchor), | ||
]) | ||
} | ||
} |
Oops, something went wrong.