Skip to content

Commit

Permalink
Merge pull request #14 from shinrenpan/develop
Browse files Browse the repository at this point in the history
bugs / features
  • Loading branch information
shinrenpan authored Jun 4, 2024
2 parents d59f7c1 + 12654f5 commit d43f576
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 4 deletions.
36 changes: 32 additions & 4 deletions Comic.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
0E299BE62C020AB50083E07C /* ReaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E299BE52C020AB50083E07C /* ReaderCell.swift */; };
0E299BE82C020DDB0083E07C /* UICollectionView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E299BE72C020DDB0083E07C /* UICollectionView+Extensions.swift */; };
0E299BEA2C0218630083E07C /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E299BE92C0218630083E07C /* Bundle+Extensions.swift */; };
0E58F7392C0F6803005936D7 /* EpisodeListVO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E58F7342C0F6803005936D7 /* EpisodeListVO.swift */; };
0E58F73A2C0F6803005936D7 /* EpisodeListRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E58F7352C0F6803005936D7 /* EpisodeListRouter.swift */; };
0E58F73B2C0F6803005936D7 /* EpisodeListVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E58F7362C0F6803005936D7 /* EpisodeListVM.swift */; };
0E58F73C2C0F6803005936D7 /* EpisodeListModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E58F7372C0F6803005936D7 /* EpisodeListModels.swift */; };
0E58F73D2C0F6803005936D7 /* EpisodeListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E58F7382C0F6803005936D7 /* EpisodeListVC.swift */; };
0E9E21A52C09FA4900E83164 /* ReaderCellIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9E21A42C09FA4900E83164 /* ReaderCellIndicator.swift */; };
0E9E21AC2C0C3D6000E83164 /* ParserConfiguration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9E21AB2C0C3D6000E83164 /* ParserConfiguration+Extensions.swift */; };
0E9E21AE2C0C3DD900E83164 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9E21AD2C0C3DD900E83164 /* String+Extensions.swift */; };
Expand Down Expand Up @@ -83,6 +88,11 @@
0E299BE52C020AB50083E07C /* ReaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderCell.swift; sourceTree = "<group>"; };
0E299BE72C020DDB0083E07C /* UICollectionView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extensions.swift"; sourceTree = "<group>"; };
0E299BE92C0218630083E07C /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = "<group>"; };
0E58F7342C0F6803005936D7 /* EpisodeListVO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeListVO.swift; sourceTree = "<group>"; };
0E58F7352C0F6803005936D7 /* EpisodeListRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeListRouter.swift; sourceTree = "<group>"; };
0E58F7362C0F6803005936D7 /* EpisodeListVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeListVM.swift; sourceTree = "<group>"; };
0E58F7372C0F6803005936D7 /* EpisodeListModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeListModels.swift; sourceTree = "<group>"; };
0E58F7382C0F6803005936D7 /* EpisodeListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeListVC.swift; sourceTree = "<group>"; };
0E9E21A42C09FA4900E83164 /* ReaderCellIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderCellIndicator.swift; sourceTree = "<group>"; };
0E9E21AB2C0C3D6000E83164 /* ParserConfiguration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParserConfiguration+Extensions.swift"; sourceTree = "<group>"; };
0E9E21AD2C0C3DD900E83164 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -183,6 +193,18 @@
path = Shared;
sourceTree = "<group>";
};
0E58F7332C0F67EA005936D7 /* EpisodeList */ = {
isa = PBXGroup;
children = (
0E58F7372C0F6803005936D7 /* EpisodeListModels.swift */,
0E58F7352C0F6803005936D7 /* EpisodeListRouter.swift */,
0E58F7382C0F6803005936D7 /* EpisodeListVC.swift */,
0E58F7362C0F6803005936D7 /* EpisodeListVM.swift */,
0E58F7342C0F6803005936D7 /* EpisodeListVO.swift */,
);
path = EpisodeList;
sourceTree = "<group>";
};
0EA88FA42BFCBB65002CAA75 = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -322,6 +344,7 @@
0EA890152BFE4777002CAA75 /* MVVVR */ = {
isa = PBXGroup;
children = (
0E58F7332C0F67EA005936D7 /* EpisodeList */,
0EA88FFE2BFD0E99002CAA75 /* Detail */,
0EA88FF02BFD01A9002CAA75 /* Favorite */,
0EE6365B2BFF93F8003F5694 /* History */,
Expand Down Expand Up @@ -439,6 +462,11 @@
buildActionMask = 2147483647;
files = (
0EA88FCB2BFCBCBA002CAA75 /* UpdateVO.swift in Sources */,
0E58F7392C0F6803005936D7 /* EpisodeListVO.swift in Sources */,
0E58F73A2C0F6803005936D7 /* EpisodeListRouter.swift in Sources */,
0E58F73B2C0F6803005936D7 /* EpisodeListVM.swift in Sources */,
0E58F73C2C0F6803005936D7 /* EpisodeListModels.swift in Sources */,
0E58F73D2C0F6803005936D7 /* EpisodeListVC.swift in Sources */,
0EA88FEA2BFCE59B002CAA75 /* DBWorker.swift in Sources */,
0EA88FE82BFCCF78002CAA75 /* UITableView+Extensions.swift in Sources */,
0E9E21A52C09FA4900E83164 /* ReaderCellIndicator.swift in Sources */,
Expand Down Expand Up @@ -629,7 +657,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20240604;
CURRENT_PROJECT_VERSION = 20240605;
DEVELOPMENT_TEAM = VZWPMD258L;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
Expand All @@ -640,7 +668,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.9.1;
MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = com.shinrenpan.Comic;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand All @@ -655,7 +683,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 20240604;
CURRENT_PROJECT_VERSION = 20240605;
DEVELOPMENT_TEAM = VZWPMD258L;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
Expand All @@ -666,7 +694,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.9.1;
MARKETING_VERSION = 0.9.2;
PRODUCT_BUNDLE_IDENTIFIER = com.shinrenpan.Comic;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down
19 changes: 19 additions & 0 deletions Sources/MVVVR/Detail/DetailVO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ extension DetailVO {
header.reloadUI(comic: model.comic)
list.refreshControl?.endRefreshing()
list.reloadData()

DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.updateWatchedUI(model: model)
}
}
}

Expand Down Expand Up @@ -61,4 +65,19 @@ private extension DetailVO {
list.bottomAnchor.constraint(equalTo: mainView.bottomAnchor),
])
}

// MARK: - Update Something

func updateWatchedUI(model: DetailModels.DisplayModel) {
guard let watchedId = model.comic.watchedId else {
return
}

guard let row = model.episodes.firstIndex(where: { $0.id == watchedId }) else {
return
}

let indexPath = IndexPath(row: row, section: 0)
list.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
53 changes: 53 additions & 0 deletions Sources/MVVVR/EpisodeList/EpisodeListModels.swift
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
}
}
}
11 changes: 11 additions & 0 deletions Sources/MVVVR/EpisodeList/EpisodeListRouter.swift
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?
}
121 changes: 121 additions & 0 deletions Sources/MVVVR/EpisodeList/EpisodeListVC.swift
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)
}
}
39 changes: 39 additions & 0 deletions Sources/MVVVR/EpisodeList/EpisodeListVM.swift
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)
}
}
38 changes: 38 additions & 0 deletions Sources/MVVVR/EpisodeList/EpisodeListVO.swift
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),
])
}
}
Loading

0 comments on commit d43f576

Please sign in to comment.