我的页面开发,网络状态监控

This commit is contained in:
2025-04-21 09:50:26 +08:00
parent 97c9f1f365
commit b52e1b4a21
40 changed files with 988 additions and 30 deletions

View File

@ -31,6 +31,8 @@ target 'ShortPlay' do
pod 'KTVHTTPCache' #视频缓存
pod 'HWPanModal' #底部弹出控制器
pod 'Kingfisher' #图片加载
pod 'EmptyStateKit' #空数据页面
pod 'ReachabilitySwift' #网络状态监控
target 'ShortPlayTests' do

View File

@ -1,6 +1,7 @@
PODS:
- Alamofire (5.10.2)
- CocoaAsyncSocket (7.6.5)
- EmptyStateKit (1.1.0)
- HWPanModal (0.9.9)
- Kingfisher (8.3.1)
- KTVHTTPCache (3.0.2):
@ -10,6 +11,7 @@ PODS:
- Moya/Core (= 15.0.0)
- Moya/Core (15.0.0):
- Alamofire (~> 5.0)
- ReachabilitySwift (5.2.4)
- SmartCodable (4.3.2)
- SnapKit (5.7.1)
- Toast (4.1.1)
@ -21,11 +23,13 @@ PODS:
- ZFPlayer/Core (4.1.4)
DEPENDENCIES:
- EmptyStateKit
- HWPanModal
- Kingfisher
- KTVHTTPCache
- MJRefresh
- Moya
- ReachabilitySwift
- SmartCodable
- SnapKit
- Toast
@ -36,11 +40,13 @@ SPEC REPOS:
trunk:
- Alamofire
- CocoaAsyncSocket
- EmptyStateKit
- HWPanModal
- Kingfisher
- KTVHTTPCache
- MJRefresh
- Moya
- ReachabilitySwift
- SmartCodable
- SnapKit
- Toast
@ -50,17 +56,19 @@ SPEC REPOS:
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
EmptyStateKit: dc41e9ce5c6089f67a49d063bce73ade9f2ba73f
HWPanModal: b57a6717d3cdcd666bff44f9dd2a5be9f4d6f5d2
Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3
KTVHTTPCache: 5711692cdf9a5ecfe829b1e16577deb3ffe3dc86
MJRefresh: ff9e531227924c84ce459338414550a05d2aea78
Moya: 138f0573e53411fb3dc17016add0b748dfbd78ee
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
SmartCodable: 88fbf3d65207c2376fdbce4b080a3d578cb51be8
SnapKit: d612e99e678a2d3b95bf60b0705ed0a35c03484a
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
YYKit: 7cda43304a8dc3696c449041e2cb3107b4e236e7
ZFPlayer: 5cf39e8d9f0c2394a014b0db4767b5b5a6bffe13
PODFILE CHECKSUM: 13500f038833f93358c53b1941ce4a7f311776dd
PODFILE CHECKSUM: 3842e01f3a774298d51e08a9caf0e72ea42cd7bc
COCOAPODS: 1.16.2

View File

@ -448,7 +448,7 @@
INFOPLIST_KEY_UIMainStoryboardFile = "";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -485,7 +485,7 @@
INFOPLIST_KEY_UIMainStoryboardFile = "";
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1DBC40582DA4EDFC0093FCB0"
BuildableName = "ShortPlay.app"
BlueprintName = "ShortPlay"
ReferencedContainer = "container:ShortPlay.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1DBC406E2DA4EE010093FCB0"
BuildableName = "ShortPlayTests.xctest"
BlueprintName = "ShortPlayTests"
ReferencedContainer = "container:ShortPlay.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1DBC40782DA4EE010093FCB0"
BuildableName = "ShortPlayUITests.xctest"
BlueprintName = "ShortPlayUITests"
ReferencedContainer = "container:ShortPlay.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1DBC40582DA4EDFC0093FCB0"
BuildableName = "ShortPlay.app"
BlueprintName = "ShortPlay"
ReferencedContainer = "container:ShortPlay.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1DBC40582DA4EDFC0093FCB0"
BuildableName = "ShortPlay.app"
BlueprintName = "ShortPlay"
ReferencedContainer = "container:ShortPlay.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -33,7 +33,7 @@ extension AppDelegate {
let backgroundImage = UIImage(color: .clear)
let backgroundColor = UIColor.clear
let backgroundColor = UIColor.black
let shadowImage = UIImage()
let shadowColor = UIColor.clear

View File

@ -17,8 +17,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
self.appConfig()
SPLoginManager.manager.requestVisitorLogin(completer: nil)
///
// SPNetworkReachabilityManager.manager.startMonitoring()
NotificationCenter.default.addObserver(self, selector: #selector(reachabilityDidChangeNotification), name: SPNetworkReachabilityManager.reachabilityDidChangeNotification, object: nil)
NetworkObserver.share.startMonitoring()
return true
@ -39,5 +44,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
@objc private func reachabilityDidChangeNotification() {
// if SPNetworkReachabilityManager.manager.isReachable {
// SPLoginManager.manager.requestVisitorLogin(completer: nil)
// }
}
}

View File

@ -43,7 +43,7 @@ class SPVideoAPI: NSObject {
}
///
static func requestCollectShort(isCollect: Bool, shortPlayId: String, videoId: String, success: (() -> Void)?) {
static func requestCollectShort(isCollect: Bool, shortPlayId: String, videoId: String?, success: (() -> Void)?, failure: (() -> Void)? = nil) {
let path: String
if isCollect {
path = "/collect"
@ -51,12 +51,17 @@ class SPVideoAPI: NSObject {
path = "/cancelCollect"
}
var parameters: [String : Any] = [
"short_play_id" : shortPlayId,
]
if let videoId = videoId {
parameters["video_id"] = videoId
}
var param = SPNetworkParameters(path: path)
param.isLoding = true
param.parameters = [
"short_play_id" : shortPlayId,
"video_id" : videoId
]
param.parameters = parameters
SPNetwork.request(parameters: param) { (response: SPNetworkResponse<String>) in
if response.code == SPNetworkCodeSucceed {
@ -65,6 +70,8 @@ class SPVideoAPI: NSObject {
"state" : isCollect,
"id" : shortPlayId,
])
} else {
failure?()
}
}
}
@ -83,6 +90,20 @@ class SPVideoAPI: NSObject {
}
}
///
static func requestPlayHistoryList(page: Int, completer: ((_ listModel: SPListModel<SPShortModel>?) -> Void)?) {
var param = SPNetworkParameters(path: "/myHistorys")
param.method = .get
param.parameters = [
"current_page" : page,
"page_size" : 20
]
SPNetwork.request(parameters: param) { (response: SPNetworkResponse<SPListModel<SPShortModel>>) in
completer?(response.data)
}
}
}
extension SPVideoAPI {

View File

@ -0,0 +1,60 @@
//
// SPNetworkReachabilityManager.swift
// ShortPlay
//
// Created by Overseas on 2025/4/19.
//
import UIKit
import Network
import Combine
import Alamofire
class SPNetworkReachabilityManager {
// enum Status {
// case notReachable
// case reachableViaWiFi
// case reachableViaWWAN
// case ethernet
// }
static let manager: SPNetworkReachabilityManager = SPNetworkReachabilityManager()
private let reachabilityManager = NetworkReachabilityManager()
///
// @objc var isReachable: Bool {
// switch currentReachabilityStatus {
// case .notReachable:
// return false
// case .reachableViaWiFi, .reachableViaWWAN:
// return true
//
// default:
// return false
// }
// }
func startMonitoring() {
reachabilityManager?.startListening(onUpdatePerforming: { status in
switch status {
case .notReachable:
print("网络不可用")
case .unknown:
print("网络状态未知")
case .reachable(.cellular):
print("蜂窝网络连接")
case .reachable(.ethernetOrWiFi):
print("WiFi 或有线网络连接")
}
})
}
}
extension SPNetworkReachabilityManager {
///
@objc static let reachabilityDidChangeNotification = NSNotification.Name(rawValue: "reachabilityDidChangeNotification")
}

View File

@ -11,6 +11,9 @@ class SPHomePageController: SPViewController {
private var topModel: SPHomeTopModel?
///
private var isRequesting = false
private lazy var categoryArr: [SPHomeCategoryModel] = {
let arr = [
SPHomeCategoryModel(category_name: "Hot Picks".localized, category_id: nil, viewController: SPHomeViewController()),
@ -55,6 +58,7 @@ class SPHomePageController: SPViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(reachabilityDidChangeNotification), name: SPNetworkReachabilityManager.reachabilityDidChangeNotification, object: nil)
sp_setupUI()
@ -97,6 +101,9 @@ extension SPHomePageController {
self.navigationController?.pushViewController(vc, animated: true)
}
@objc private func reachabilityDidChangeNotification() {
requestData()
}
}
@ -130,9 +137,9 @@ extension SPHomePageController: JYPageControllerDelegate, JYPageControllerDataSo
extension SPHomePageController {
private func requestData() {
if self.topModel != nil { return }
if self.topModel != nil || isRequesting { return }
isRequesting = true
SPHomeAPI.requestHomeTopData { [weak self] model in
guard let self = self else { return }
if let model = model {
@ -142,6 +149,7 @@ extension SPHomePageController {
}
self.pageView.reload()
}
self.isRequesting = false
}
}

View File

@ -29,6 +29,8 @@ class SPHomeViewController: SPHomeChildController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(reachabilityDidChangeNotification), name: SPNetworkReachabilityManager.reachabilityDidChangeNotification, object: nil)
// view.backgroundColor = .clear
requestModuleData()
_setupUI()
@ -53,6 +55,12 @@ extension SPHomeViewController {
}
}
extension SPHomeViewController {
@objc private func reachabilityDidChangeNotification() {
}
}
//MARK: -------------- UICollectionViewDelegate & UICollectionViewDataSource --------------
extension SPHomeViewController: UICollectionViewDelegate, UICollectionViewDataSource {

View File

@ -0,0 +1,18 @@
//
// SPAboutUsViewController.swift
// ShortPlay
//
// Created by Overseas on 2025/4/19.
//
import UIKit
class SPAboutUsViewController: SPViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}

View File

@ -21,6 +21,11 @@ class SPMineViewController: SPViewController {
return arr
}()
private lazy var headerView: SPMineHeaderView = {
let view = SPMineHeaderView(frame: CGRect(x: 0, y: 0, width: kSPScreenWidth, height: 200))
return view
}()
private lazy var tableView: SPTableView = {
let tableView = SPTableView(frame: .zero, style: .insetGrouped)
tableView.delegate = self
@ -49,6 +54,8 @@ class SPMineViewController: SPViewController {
extension SPMineViewController {
private func _setupUI() {
tableView.tableHeaderView = self.headerView
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
@ -84,6 +91,10 @@ extension SPMineViewController: UITableViewDelegate, UITableViewDataSource {
vc.urlStr = SPUserAgreementWebUrl
self.navigationController?.pushViewController(vc, animated: true)
case .aboutUs:
let vc = SPAboutUsViewController()
self.navigationController?.pushViewController(vc, animated: true)
default:
break
}

View File

@ -0,0 +1,14 @@
//
// SPMineHeaderView.swift
// ShortPlay
//
// Created by Overseas on 2025/4/19.
//
import UIKit
class SPMineHeaderView: UIView {
}

View File

@ -7,12 +7,14 @@
import UIKit
class SPCollectListViewController: SPViewController {
class SPCollectListViewController: SPMyListChildViewController {
private lazy var dataArr: [SPShortModel] = []
private var page: Int?
private lazy var deleteGorup = DispatchGroup()
//MARK: UI
private lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let itemWidth = floor((kSPScreenWidth - 30 - 9 * 2) / 3)
@ -67,6 +69,47 @@ class SPCollectListViewController: SPViewController {
self?.collectionView.sp_endFooterRefreshing()
}
}
override var sp_isEditing: Bool {
didSet {
self.collectionView.reloadData()
}
}
override var isAllSelected: Bool {
var result = true
for model in dataArr {
if model.sp_isSelected != true {
result = false
break
}
}
return result
}
override var selectedCount: Int {
var result = 0
dataArr.forEach {
if $0.sp_isSelected ?? false {
result += 1
}
}
return result
}
override func setAllSelectedState(isSelected: Bool) {
dataArr.forEach {
$0.sp_isSelected = isSelected
}
self.collectionView.reloadData()
allSelectedStateDidChange?(isSelected)
self.updateDeleteButtonState()
}
override func handelDeleteButton() {
requestDelete()
}
}
extension SPCollectListViewController {
@ -87,16 +130,38 @@ extension SPCollectListViewController: UICollectionViewDelegate, UICollectionVie
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = SPCollectListCell.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath)
cell.sp_isEditing = self.sp_isEditing
cell.model = dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr.count
let count = self.dataArr.count
if count == 0 {
let parameters = SPEmptyParameters(image: UIImage(named: "empty_image_01"))
let emptyState = SPEmptyState.normail(parameters: parameters)
self.collectionView.emptyState.format = emptyState.format
self.collectionView.emptyState.show(emptyState)
} else {
self.collectionView.emptyState.hide()
}
return count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = dataArr[indexPath.row]
if sp_isEditing {
model.sp_isSelected = !(model.sp_isSelected ?? false)
self.collectionView.reloadData()
allSelectedStateDidChange?(isAllSelected)
self.updateDeleteButtonState()
} else {
let vc = SPPlayerDetailViewController()
vc.shortPlayId = model.short_play_id
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
@ -117,13 +182,33 @@ extension SPCollectListViewController {
self.page = page
self.collectionView.reloadData()
if self.sp_isEditing {
self.allSelectedStateDidChange?(self.isAllSelected)
}
}
completer?()
}
}
private func requestDelete() {
dataArr.forEach {
if $0.sp_isSelected == true, let shortPlayId = $0.short_play_id {
self.deleteGorup.enter()
SPVideoAPI.requestCollectShort(isCollect: false, shortPlayId: shortPlayId, videoId: nil) { [weak self] in
self?.deleteGorup.leave()
} failure: { [weak self] in
self?.deleteGorup.leave()
}
}
}
self.deleteGorup.notify(queue: DispatchQueue.main) { [weak self] in
self?.requestDataList(page: 1, completer: nil)
}
}
}

View File

@ -0,0 +1,87 @@
//
// SPMyListChildViewController.swift
// ShortPlay
//
// Created by Overseas on 2025/4/19.
//
import UIKit
class SPMyListChildViewController: SPViewController {
var sp_isEditing = false {
didSet {
deleteButton.isHidden = !sp_isEditing
if sp_isEditing {
self.view.bringSubviewToFront(deleteButton)
}
}
}
///
var isAllSelected: Bool {
return false
}
///
var selectedCount: Int {
return 0
}
///
var allSelectedStateDidChange: ((_ isAllSelected: Bool) -> Void)?
private(set) lazy var deleteButton: UIButton = {
let button = JXButton(type: .custom)
button.setTitle("0", for: .normal)
button.setTitleColor(.color9D9D9D(), for: .disabled)
button.setTitleColor(.colorF564B6(), for: .normal)
button.setImage(UIImage(named: "delete_icon_01"), for: .disabled)
button.setImage(UIImage(named: "delete_icon_02"), for: .normal)
button.jx_font = .fontRegular(ofSize: 14)
button.jx_setBorderColor(.color9D9D9D(), for: .disabled)
button.jx_setBorderColor(.colorF564B6(), for: .normal)
button.space = 7
button.layer.cornerRadius = 24
button.layer.masksToBounds = true
button.layer.borderWidth = 0.7
button.isHidden = true
button.addTarget(self, action: #selector(handelDeleteButton), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
updateDeleteButtonState()
view.addSubview(deleteButton)
deleteButton.snp.makeConstraints { make in
make.left.equalToSuperview().offset(20)
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-20)
make.height.equalTo(48)
}
}
///
func setAllSelectedState(isSelected: Bool) { }
///
func updateDeleteButtonState() {
let count = self.selectedCount
deleteButton.isEnabled = count > 0
let text = String(format: "Delet (%@)".localized, "\(selectedCount)")
deleteButton.setTitle(text, for: .normal)
}
@objc func handelDeleteButton() {
}
}

View File

@ -12,12 +12,40 @@ class SPMyListViewController: SPViewController {
private lazy var titles = ["Follow List".localized, "Play List".localized]
private lazy var viewControllers: [SPViewController] = {
private lazy var viewControllers: [SPMyListChildViewController] = {
let vc1 = SPCollectListViewController()
vc1.allSelectedStateDidChange = { [weak self] isAllSelected in
self?.allSelectedButton.isSelected = isAllSelected
}
let vc2 = SPPlayHistoryViewController()
vc2.allSelectedStateDidChange = { [weak self] isAllSelected in
self?.allSelectedButton.isSelected = isAllSelected
}
return [vc1, vc2]
}()
private lazy var sp_isEditing = false {
didSet {
editButton.isHidden = sp_isEditing
pageView.segmentedView.isHidden = sp_isEditing
cancelButton.isHidden = !sp_isEditing
allSelectedButton.isHidden = !sp_isEditing
pageView.pageContentScrollView.isScrollEnabled = !sp_isEditing
let vc = viewControllers[pageView.selectedIndex]
vc.sp_isEditing = sp_isEditing
if !sp_isEditing {
vc.setAllSelectedState(isSelected: false)
allSelectedButton.isSelected = false
}
}
}
//MARK: UI
private lazy var pageView: JYPageController = {
let pageView = JYPageController()
pageView.delegate = self
@ -37,6 +65,39 @@ class SPMyListViewController: SPViewController {
return pageView
}()
private lazy var editButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "delete_icon_01"), for: .normal)
button.addTarget(self, action: #selector(handleEditButton), for: .touchUpInside)
return button
}()
private lazy var cancelButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle("Cancel".localized, for: .normal)
button.setTitleColor(.colorFFFFFF(alpha: 0.9), for: .normal)
button.titleLabel?.font = .fontRegular(ofSize: 15)
button.addTarget(self, action: #selector(handleCancelButton), for: .touchUpInside)
button.isHidden = true
return button
}()
///
private lazy var allSelectedButton: UIButton = {
let button = JXButton(type: .custom)
button.setTitle("Select All".localized, for: .normal)
button.setTitleColor(.colorFFFFFF(alpha: 0.9), for: .normal)
button.setImage(UIImage(named: "check_icon_01"), for: .normal)
button.setImage(UIImage(named: "check_icon_01_selected"), for: .selected)
button.setImage(UIImage(named: "check_icon_01_selected"), for: [.selected, .highlighted])
button.jx_font = .fontRegular(ofSize: 15)
button.titleDirection = .right
button.space = 6
button.isHidden = true
button.addTarget(self, action: #selector(handleAllSelectedButton), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
@ -49,18 +110,57 @@ class SPMyListViewController: SPViewController {
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.sp_isEditing = false
}
@objc private func handleEditButton() {
self.sp_isEditing = true
}
@objc private func handleCancelButton() {
self.sp_isEditing = false
}
@objc private func handleAllSelectedButton() {
let vc = viewControllers[pageView.selectedIndex]
if self.allSelectedButton.isSelected {
vc.setAllSelectedState(isSelected: false)
} else {
vc.setAllSelectedState(isSelected: true)
}
}
}
extension SPMyListViewController {
private func _setupUI() {
addChild(pageView)
view.addSubview(pageView.view)
view.addSubview(editButton)
view.addSubview(cancelButton)
view.addSubview(allSelectedButton)
pageView.view.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
editButton.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-5)
make.top.equalToSuperview().offset(kSPStatusbarHeight + 10)
make.height.equalTo(35)
make.width.equalTo(40)
}
cancelButton.snp.makeConstraints { make in
make.top.bottom.equalTo(editButton)
make.right.equalTo(editButton)
}
allSelectedButton.snp.makeConstraints { make in
make.left.equalToSuperview().offset(15)
make.top.bottom.equalTo(editButton)
}
}
}
@ -88,4 +188,12 @@ extension SPMyListViewController: JYPageControllerDelegate, JYPageControllerData
return self.viewControllers[index]
}
func pageController(_ pageController: JYPageController, didEnterControllerAt index: Int) {
if index == 0 {
self.editButton.isHidden = false
} else {
self.editButton.isHidden = true
}
}
}

View File

@ -7,11 +7,13 @@
import UIKit
class SPPlayHistoryViewController: SPViewController {
class SPPlayHistoryViewController: SPMyListChildViewController {
private lazy var dataArr: [SPShortModel] = []
private var page: Int?
private lazy var deleteGorup = DispatchGroup()
//MARK: UI
private lazy var collectionViewLayout: UICollectionViewFlowLayout = {
let itemWidth = floor((kSPScreenWidth - 30 - 9 * 2) / 3)
@ -21,7 +23,7 @@ class SPPlayHistoryViewController: SPViewController {
layout.itemSize = .init(width: itemWidth, height: itemHeight)
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 9
layout.sectionInset = .init(top: 10, left: 15, bottom: 0, right: 15)
layout.sectionInset = .init(top: 0, left: 15, bottom: 0, right: 15)
return layout
}()
@ -30,6 +32,13 @@ class SPPlayHistoryViewController: SPViewController {
let collectionView = SPCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.contentInset = .init(top: 10, left: 0, bottom: 0, right: 0)
collectionView.sp_addRefreshHeader(insetTop: collectionView.contentInset.top) { [weak self] in
self?.handleHeaderRefresh(nil)
}
collectionView.sp_addRefreshBackFooter { [weak self] in
self?.handleFooterRefresh(nil)
}
SPCollectListCell.registerCell(collectionView: collectionView)
return collectionView
}()
@ -46,6 +55,60 @@ class SPPlayHistoryViewController: SPViewController {
override func setBgImageView() { }
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
requestDataList(page: 1) { [weak self] in
self?.collectionView.sp_endHeaderRefreshing()
}
}
override func handleFooterRefresh(_ completer: (() -> Void)?) {
guard let page = self.page else { return }
requestDataList(page: page + 1) { [weak self] in
self?.collectionView.sp_endFooterRefreshing()
}
}
override var sp_isEditing: Bool {
didSet {
self.collectionView.reloadData()
}
}
override var isAllSelected: Bool {
var result = true
for model in dataArr {
if model.sp_isSelected != true {
result = false
break
}
}
return result
}
override var selectedCount: Int {
var result = 0
dataArr.forEach {
if $0.sp_isSelected ?? false {
result += 1
}
}
return result
}
override func setAllSelectedState(isSelected: Bool) {
dataArr.forEach {
$0.sp_isSelected = isSelected
}
self.collectionView.reloadData()
allSelectedStateDidChange?(isSelected)
self.updateDeleteButtonState()
}
override func handelDeleteButton() {
requestDelete()
}
}
@ -68,12 +131,38 @@ extension SPPlayHistoryViewController: UICollectionViewDelegate, UICollectionVie
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = SPCollectListCell.dequeueReusableCell(collectionView: collectionView, indexPath: indexPath)
cell.sp_isEditing = self.sp_isEditing
cell.model = dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dataArr.count
let count = self.dataArr.count
if count == 0 {
let parameters = SPEmptyParameters(image: UIImage(named: "empty_image_01"))
let emptyState = SPEmptyState.normail(parameters: parameters)
self.collectionView.emptyState.format = emptyState.format
self.collectionView.emptyState.show(emptyState)
} else {
self.collectionView.emptyState.hide()
}
return count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = dataArr[indexPath.row]
if sp_isEditing {
model.sp_isSelected = !(model.sp_isSelected ?? false)
self.collectionView.reloadData()
allSelectedStateDidChange?(isAllSelected)
self.updateDeleteButtonState()
} else {
let vc = SPPlayerDetailViewController()
vc.shortPlayId = model.short_play_id
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
@ -82,7 +171,7 @@ extension SPPlayHistoryViewController {
private func requestDataList(page: Int, completer: (() -> Void)?) {
SPVideoAPI.requestCollectList(page: page) { [weak self] listModel in
SPVideoAPI.requestPlayHistoryList(page: page) { [weak self] listModel in
guard let self = self else { return }
if let listModel = listModel, let list = listModel.list {
@ -94,12 +183,31 @@ extension SPPlayHistoryViewController {
self.page = page
self.collectionView.reloadData()
if self.sp_isEditing {
self.allSelectedStateDidChange?(self.isAllSelected)
}
}
completer?()
}
}
}
private func requestDelete() {
dataArr.forEach {
if $0.sp_isSelected == true, let shortPlayId = $0.short_play_id {
self.deleteGorup.enter()
SPVideoAPI.requestCollectShort(isCollect: false, shortPlayId: shortPlayId, videoId: nil) { [weak self] in
self?.deleteGorup.leave()
} failure: { [weak self] in
self?.deleteGorup.leave()
}
}
}
self.deleteGorup.notify(queue: DispatchQueue.main) { [weak self] in
self?.requestDataList(page: 1, completer: nil)
}
}
}

View File

@ -13,6 +13,14 @@ class SPCollectListCell: SPCollectionViewCell {
didSet {
coverImageView.sp_setImage(url: model?.image_url)
titleLabel.text = model?.name
selectedButton.isSelected = model?.sp_isSelected ?? false
}
}
var sp_isEditing: Bool = false {
didSet {
selectedButton.isHidden = !sp_isEditing
}
}
@ -31,6 +39,15 @@ class SPCollectListCell: SPCollectionViewCell {
return label
}()
private lazy var selectedButton: UIButton = {
let button = UIButton(type: .custom)
button.isHidden = true
button.isUserInteractionEnabled = false
button.setImage(UIImage(named: "check_icon_01"), for: .normal)
button.setImage(UIImage(named: "check_icon_01_selected"), for: .selected)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
@ -49,6 +66,7 @@ extension SPCollectListCell {
private func _setupUI() {
contentView.addSubview(coverImageView)
contentView.addSubview(titleLabel)
coverImageView.addSubview(selectedButton)
coverImageView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
@ -61,6 +79,11 @@ extension SPCollectListCell {
make.right.lessThanOrEqualToSuperview()
}
selectedButton.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-5)
make.top.equalToSuperview().offset(5)
}
}
}

View File

@ -63,7 +63,7 @@ class SPPlayerDetailViewController: SPPlayerListViewController {
override func play() {
super.play()
if let _ = self.viewModel.currentPlayer?.model as? SPVideoDetailModel,
if let _ = self.detailModel,
let videoInfo = self.viewModel.currentPlayer?.videoInfo
{
SPVideoAPI.requestRequestVideoPlayHistory(videoId: videoInfo.short_play_video_id ?? "", shortPlayId: videoInfo.short_play_id ?? "")

View File

@ -30,6 +30,8 @@ class SPShortModel: SPModel, SmartCodable {
@IgnoredKey
var titleAttributedString: NSAttributedString?
@IgnoredKey
var sp_isSelected: Bool?
static func mappingForKey() -> [SmartKeyTransformer]? {

View File

@ -0,0 +1,91 @@
//
// SPEmptyState.swift
// ShortPlay
//
// Created by Overseas on 2025/4/19.
//
import UIKit
import EmptyStateKit
struct SPEmptyParameters {
// var title: String = ""
// var titleFont: UIFont = UIFont.text_md
// var titleColor: UIColor = UIColor.system_text_secondary_300
var image: UIImage? = UIImage(named: "empty_image_01")
// var buttonTitle: String?
}
enum SPEmptyState {
case normail(parameters: SPEmptyParameters)
}
extension SPEmptyState: CustomState {
var image: UIImage? {
switch self {
case .normail(let parameters):
return parameters.image
}
}
var title: String? {
switch self {
case .normail(let parameters):
return nil
}
}
// var titleButton: String? {
// switch self {
// case .normail(let parameters):
// return parameters.buttonTitle
// }
}
extension SPEmptyState {
var format: EmptyStateFormat {
var format = EmptyStateFormat()
format.backgroundColor = .clear
format.imageSize = self.image?.size ?? .zero
// format.verticalMargin = -10
//
// format.buttonWidth = 107
// format.buttonTopMargin = 10
// format.buttonColor = .system_fill_primary_100
// format.buttonAttributes = [
// .font: UIFont.text_md,
// .foregroundColor: UIColor.system_text_secondary_500
// ]
//
// switch self {
// case .normail(let p):
// format.titleAttributes = [
// .font: p.titleFont,
// .foregroundColor: p.titleColor
// ]
//
//
// case .login(let p):
// format.titleAttributes = [
// .font: p.titleFont,
// .foregroundColor: p.titleColor
// ]
//
//
// }
return format
}
}

View File

@ -0,0 +1,72 @@
//
// NetworkObserver.swift
// ShortPlay
//
// Created by Overseas on 2025/4/21.
//
import UIKit
import Network
import Reachability
class NetworkObserver {
static let share = NetworkObserver()
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitorQueue")
func startMonitoring() {
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
print("++++++Network is available")
} else {
print("++++++No network connection")
}
if path.usesInterfaceType(.wifi) {
print("++++++Using Wi-Fi")
}
if path.usesInterfaceType(.cellular) {
print("++++++Using Cellular")
}
}
monitor.start(queue: queue)
}
func stopMonitoring() {
monitor.cancel()
}
/*
private let reachability = try! Reachability()
func startMonitoring() {
reachability.whenReachable = { reachability in
if reachability.connection == .wifi {
print("++++++Network reachable via Wi-Fi")
} else if reachability.connection == .cellular {
print("++++++Network reachable via Cellular")
}
}
reachability.whenUnreachable = { _ in
print("++++++Network not reachable")
}
do {
try reachability.startNotifier()
} catch {
print("++++++Unable to start notifier")
}
}
func stopMonitoring() {
reachability.stopNotifier()
}
*/
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "组件 67 2@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "组件 67 2@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "组件 67 9@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "组件 67 9@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "delet-选中@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "delet-选中@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "delete@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "delete@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "空空如也@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "空空如也@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -30,6 +30,9 @@
"My list" = "My list";
"Follow List" = "Follow List";
"Play List" = "Play List";
"Cancel" = "Cancel";
"Select All" = "Select All";
"Delet (%@)" = "Delet (%@)";
///视频详情标题
"kPlayerDetailTitleString" = "Episode %@ / %@";

View File

@ -211,6 +211,12 @@ class JXButton: UIButton {
updateStatus()
}
}
override var isEnabled: Bool {
didSet {
updateStatus()
}
}
}

View File

@ -343,7 +343,7 @@ open class JYPageController: UIViewController {
return segment
}()
private lazy var pageContentScrollView : UIScrollView = {
private(set) lazy var pageContentScrollView : UIScrollView = {
let scrollView = UIScrollView()
// scrollView.backgroundColor = .white
scrollView.showsHorizontalScrollIndicator = false