Edit the player interaction

dev
wenlei 1 year ago
parent 5d79917804
commit ee39a268be

@ -111,6 +111,11 @@
778B8AC12AF8ED280034AFD4 /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778B8AB82AF8ED280034AFD4 /* TableView.swift */; };
778B8AC22AF8ED280034AFD4 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778B8AB92AF8ED280034AFD4 /* TableViewController.swift */; };
778B8AC32AF8ED280034AFD4 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778B8ABA2AF8ED280034AFD4 /* TabViewController.swift */; };
77A659C92B4FC54300B408C3 /* PlayerInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A659C82B4FC54300B408C3 /* PlayerInteractor.swift */; };
77A659CB2B4FC5BC00B408C3 /* PlayerPresentAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A659CA2B4FC5BC00B408C3 /* PlayerPresentAnimator.swift */; };
77A659CD2B4FC5CE00B408C3 /* PlayerDismissAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A659CC2B4FC5CE00B408C3 /* PlayerDismissAnimator.swift */; };
77A659CF2B4FDC4100B408C3 /* PullToDismissable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A659CE2B4FDC4100B408C3 /* PullToDismissable.swift */; };
77A659D12B4FDC5200B408C3 /* PullToDismissTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A659D02B4FDC5200B408C3 /* PullToDismissTransition.swift */; };
77C9B9BE2B4AB4FA0006C83F /* TimingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9B9BD2B4AB4FA0006C83F /* TimingViewModel.swift */; };
77C9B9C02B4AB5B50006C83F /* PrivacyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9B9BF2B4AB5B50006C83F /* PrivacyViewModel.swift */; };
77C9B9C22B4AB6C10006C83F /* AboutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9B9C12B4AB6C10006C83F /* AboutViewModel.swift */; };
@ -308,6 +313,11 @@
778B8AB82AF8ED280034AFD4 /* TableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; };
778B8AB92AF8ED280034AFD4 /* TableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = "<group>"; };
778B8ABA2AF8ED280034AFD4 /* TabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewController.swift; sourceTree = "<group>"; };
77A659C82B4FC54300B408C3 /* PlayerInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerInteractor.swift; sourceTree = "<group>"; };
77A659CA2B4FC5BC00B408C3 /* PlayerPresentAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerPresentAnimator.swift; sourceTree = "<group>"; };
77A659CC2B4FC5CE00B408C3 /* PlayerDismissAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDismissAnimator.swift; sourceTree = "<group>"; };
77A659CE2B4FDC4100B408C3 /* PullToDismissable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToDismissable.swift; sourceTree = "<group>"; };
77A659D02B4FDC5200B408C3 /* PullToDismissTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToDismissTransition.swift; sourceTree = "<group>"; };
77C9B9BD2B4AB4FA0006C83F /* TimingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimingViewModel.swift; sourceTree = "<group>"; };
77C9B9BF2B4AB5B50006C83F /* PrivacyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyViewModel.swift; sourceTree = "<group>"; };
77C9B9C12B4AB6C10006C83F /* AboutViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewModel.swift; sourceTree = "<group>"; };
@ -521,6 +531,7 @@
children = (
778B8AB62AF8ED280034AFD4 /* NavigationController.swift */,
778B8AB32AF8ED270034AFD4 /* PresentationController.swift */,
77A659C82B4FC54300B408C3 /* PlayerInteractor.swift */,
778B8AB82AF8ED280034AFD4 /* TableView.swift */,
778B8AB92AF8ED280034AFD4 /* TableViewController.swift */,
778B8ABA2AF8ED280034AFD4 /* TabViewController.swift */,
@ -537,6 +548,10 @@
77C9B9C72B4B8E200006C83F /* AlertViewController.swift */,
77C9B9D02B4B99600006C83F /* BadgeButton.swift */,
77C9B9D22B4B9B180006C83F /* BorderLabel.swift */,
77A659CA2B4FC5BC00B408C3 /* PlayerPresentAnimator.swift */,
77A659CC2B4FC5CE00B408C3 /* PlayerDismissAnimator.swift */,
77A659CE2B4FDC4100B408C3 /* PullToDismissable.swift */,
77A659D02B4FDC5200B408C3 /* PullToDismissTransition.swift */,
);
path = Common;
sourceTree = "<group>";
@ -1070,6 +1085,7 @@
7751D3642B42BC2E00F1F2BD /* AccountViewModel.swift in Sources */,
778B8A5D2AF8EC610034AFD4 /* Application.swift in Sources */,
7751D3622B42BC0C00F1F2BD /* AccountViewController.swift in Sources */,
77A659CF2B4FDC4100B408C3 /* PullToDismissable.swift in Sources */,
7751D3542B42AE0E00F1F2BD /* SettingViewMdel.swift in Sources */,
77C9B9E12B4BDAEA0006C83F /* LikeListViewController.swift in Sources */,
774399A62AFE036A006F8EEA /* PlayerView.swift in Sources */,
@ -1096,6 +1112,7 @@
77FA0B2C2B0C480B00404C5E /* AudioMoreActionViewModel.swift in Sources */,
778B8AC22AF8ED280034AFD4 /* TableViewController.swift in Sources */,
77FA0B562B0F4ABF00404C5E /* MineSingleController.swift in Sources */,
77A659CD2B4FC5CE00B408C3 /* PlayerDismissAnimator.swift in Sources */,
778B8A9D2AF8ECFC0034AFD4 /* LibsManager.swift in Sources */,
77FA0B4C2B0EF8BB00404C5E /* LoginViewModel.swift in Sources */,
77C9B9C62B4B81890006C83F /* Timing.swift in Sources */,
@ -1149,6 +1166,7 @@
77C9B9EF2B4C2A910006C83F /* AudioTrackListViewController.swift in Sources */,
77FB7A782B4A4B6400B64030 /* MineJournalViewModel.swift in Sources */,
778B8A6D2AF8ECD30034AFD4 /* Observable+Operators.swift in Sources */,
77A659D12B4FDC5200B408C3 /* PullToDismissTransition.swift in Sources */,
77FA0B542B0F447400404C5E /* Mine.swift in Sources */,
77C9B9C02B4AB5B50006C83F /* PrivacyViewModel.swift in Sources */,
77FB7A7F2B4A630100B64030 /* ThanksViewModel.swift in Sources */,
@ -1190,6 +1208,7 @@
778B8AA92AF8ED0E0034AFD4 /* UIImage+IndieMusic.swift in Sources */,
77C9B9D12B4B99600006C83F /* BadgeButton.swift in Sources */,
778B8A7F2AF8ECE50034AFD4 /* MineViewModel.swift in Sources */,
77A659C92B4FC54300B408C3 /* PlayerInteractor.swift in Sources */,
778B8A9A2AF8ECFC0034AFD4 /* AudioManager.swift in Sources */,
774A180E2B07000C00F56DF1 /* JournalDetailView.swift in Sources */,
778B8A842AF8ECE50034AFD4 /* HomeTabBarController.swift in Sources */,
@ -1212,6 +1231,7 @@
77FA0B382B0C54C700404C5E /* FilterViewController.swift in Sources */,
77FA0B522B0F3BC700404C5E /* MineView.swift in Sources */,
778B8A6E2AF8ECD30034AFD4 /* RestApi.swift in Sources */,
77A659CB2B4FC5BC00B408C3 /* PlayerPresentAnimator.swift in Sources */,
77C9B9D52B4BBD6A0006C83F /* FollowingViewController.swift in Sources */,
778B8ABC2AF8ED280034AFD4 /* PresentationController.swift in Sources */,
77FA0B3E2B0C573600404C5E /* Filter.swift in Sources */,

@ -57,6 +57,8 @@ class Navigator {
case login(viewModel: LoginViewModel)
case bindPhone(viewModel: BindPhoneViewModel)
case player(viewModel: PlayerViewModel)
case alert
case test
case safari(URL)
@ -161,6 +163,11 @@ class Navigator {
case .bindPhone(viewModel: let viewModel):
return BindPhoneViewController(viewModel: viewModel, navigator: self)
case .player(viewModel: let viewModel):
let player = PlayerViewController(viewModel: viewModel, navigator: self)
return player
case .alert:
return AlertViewController.init()
case .test:
@ -193,7 +200,12 @@ class Navigator {
}
func dismiss(sender: UIViewController?) {
sender?.navigationController?.dismiss(animated: true, completion: nil)
if let nav = sender?.navigationController {
sender?.navigationController?.dismiss(animated: true, completion: nil)
} else {
sender?.dismiss(animated: true, completion: nil)
}
}
// MARK: - invoke a single segue
@ -244,11 +256,17 @@ class Navigator {
DispatchQueue.main.async {
let nav = NavigationController(rootViewController: target)
nav.setNavigationBarHidden(true, animated: true)
nav.isPullToDismissEnabled = true
nav.modalPresentationStyle = .custom
nav.modalPresentationCapturesStatusBarAppearance = true
sender.present(nav, animated: true, completion: nil)
}
case .detail:
DispatchQueue.main.async {
let nav = NavigationController(rootViewController: target)
sender.showDetailViewController(nav, sender: nil)
}
case .alert:

@ -13,7 +13,8 @@ protocol DefaultNavigation {
}
class NavigationController: UINavigationController {
lazy var pullToDismissTransition: PullToDismissTransition = PullToDismissTransition(viewController: self)
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
modalPresentationStyle = .custom
@ -146,3 +147,31 @@ extension NavigationController: UIGestureRecognizerDelegate{
}
}
extension NavigationController: PullToDismissable, UIViewControllerTransitioningDelegate {
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
setupPullToDismiss()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupPullToDismiss()
}
func animationController(forDismissed dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
guard isPullToDismissEnabled else { return nil }
return pullToDismissTransition
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning)
-> UIViewControllerInteractiveTransitioning? {
guard isPullToDismissEnabled else { return nil }
return pullToDismissTransition
}
}

@ -0,0 +1,36 @@
//
// PlayerDismissAnimator.swift
// IndieMusic
//
// Created by WenLei on 2024/1/11.
//
import Foundation
class PlayerDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewController(forKey: .from),let fromView = transitionContext.view(forKey: .from) else {
transitionContext.completeTransition(true)
return
}
var fromFinalFrame = transitionContext.finalFrame(for: fromViewController)
let newFinalOrigin = CGPoint(x: fromFinalFrame.origin.x, y: fromFinalFrame.origin.y + fromFinalFrame.size.height)
fromFinalFrame.origin = newFinalOrigin
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions.curveLinear, animations: {
fromView.frame = fromFinalFrame
}) { (finished) in
let success = !transitionContext.transitionWasCancelled
transitionContext.completeTransition(success)
}
}
}

@ -0,0 +1,72 @@
//
// PlayerInteractor.swift
// IndieMusic
//
// Created by WenLei on 2024/1/11.
//
import Foundation
class PlayerInteractor: UIPercentDrivenInteractiveTransition {
private var viewController: UIViewController!
private var presenting: UIViewController? = nil
private var interactiveView: UIView!
private let thredhold: CGFloat = 0.4
init(_ viewController: UIViewController, _ interactiveView: UIView, _ presenting: UIViewController? = nil) {
super.init()
self.viewController = viewController
self.presenting = presenting
self.interactiveView = interactiveView
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
panGesture.maximumNumberOfTouches = 1
self.interactiveView.addGestureRecognizer(panGesture)
self.wantsInteractiveStart = false
}
@objc func handlePan(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
sender.setTranslation(.zero, in: interactiveView)
wantsInteractiveStart = true
if let presenting = presenting {
viewController.present(presenting, animated: true, completion: nil)
} else {
viewController.dismiss(animated: true, completion: nil)
}
case .changed:
let translation = sender.translation(in: interactiveView)
var y = translation.y
if presenting != nil {
y = y * -1
}
if y < 0 {
sender.setTranslation(.zero, in: interactiveView)
} else {
let percentage = abs(y / UIScreen.main.bounds.height)
print(percentage)
update(percentage)
}
case .ended:
if percentComplete >= thredhold {
finish()
} else {
wantsInteractiveStart = false
cancel()
}
case .cancelled, .failed:
wantsInteractiveStart = false
cancel()
default:
wantsInteractiveStart = false
return
}
}
}

@ -0,0 +1,43 @@
//
// PlayerPresentAnimator.swift
// IndieMusic
//
// Created by WenLei on 2024/1/11.
//
import Foundation
class PlayerPresentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to) else {
transitionContext.completeTransition(true)
return
}
let containerView = transitionContext.containerView
toView.frame.origin.y = UIScreen.main.bounds.size.height
toView.frame.size.height = UIScreen.main.bounds.size.height
let path = UIBezierPath(roundedRect: toView.bounds, byRoundingCorners: [.topLeft,.topRight], cornerRadii: CGSize(width: 10, height: 10))
let mask = CAShapeLayer()
mask.path = path.cgPath
toView.layer.mask = mask
containerView.addSubview(toView)
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions.curveLinear, animations: {
toView.frame.origin.y = UIScreen.main.bounds.size.height - (UIScreen.main.bounds.size.height)
}) { (finished) in
let success = !transitionContext.transitionWasCancelled
transitionContext.completeTransition(success)
}
}
}

@ -0,0 +1,493 @@
//
// PullToDismissTransition.swift
// IndieMusic
//
// Created by WenLei on 2024/1/11.
//
import UIKit
public protocol PullToDismissTransitionDelegate: NSObject {
func canBeginPullToDismiss(on dismissingViewController: UIViewController) -> Bool
func didBeginPullToDismissAttempt(on dismissingViewController: UIViewController)
func didCompletePullToDismissAttempt(on dismissingViewController: UIViewController, willDismiss: Bool)
func didFinishTransition(for dismissingViewController: UIViewController, didDismiss: Bool)
}
extension PullToDismissTransitionDelegate {
public func canBeginPullToDismiss(on dismissingViewController: UIViewController) -> Bool {
return true
}
public func didBeginPullToDismissAttempt(on dismissingViewController: UIViewController) {}
public func didCompletePullToDismissAttempt(on dismissingViewController: UIViewController, willDismiss: Bool) {}
public func didFinishTransition(for dismissingViewController: UIViewController, didDismiss: Bool) {}
}
public enum PullToDismissTransitionType {
case slideStatic
case slideDynamic
case scale
}
public class PullToDismissTransition: UIPercentDrivenInteractiveTransition {
private struct Const {
static let dimmingAlphaTransitionFinishDropDelay: TimeInterval = 0.24
static let dimmingPeakAlpha: CGFloat = 0.87
static let minimumTranslationYForDismiss: CGFloat = 87
static let translationThreshold: CGFloat = 0.35
static let scalingViewCornerRadius: CGFloat = 12
static let scalingViewCornerRadiusToggleDuration: TimeInterval = 0.15
static let scalingPeakScaleDivider: CGFloat = 5
static let touchStillWithoutPanEndDelay: TimeInterval = 0.15
static let transitionDurationDragSlide: TimeInterval = 0.87
static let transitionDurationDragScale: TimeInterval = 0.35
static let transitionReEnableTimeoutAfterScroll: TimeInterval = 0.72
static let velocityBeginThreshold: CGFloat = 10
static let velocityFinishThreshold: CGFloat = 1280
}
public let transitionType: PullToDismissTransitionType
public let animationOptions: UIView.AnimationOptions
private(set) weak var viewController: UIViewController?
private(set) weak var monitoredScrollView: UIScrollView?
public var permitWhenNotAtRootViewController = false
public weak var delegate: PullToDismissTransitionDelegate?
public var isDimmingEnabled = true
private weak var dimmingView: UIView?
private weak var scalingView: UIView?
private var transitionIsActiveFromTranslationPoint: CGPoint?
private var didRequestScrollViewBounceDisable = false
private var monitoredScrollViewDoesBounce = false
private var recentScrollIsBlockingTransition = false
private var scrollInitiateCount: Int = 0
private var scrollViewObservation: NSKeyValueObservation?
private var touchBeginOrPanIncrement: Int = 0
private var transitionHasEndedAndPanIsInactive = false
private var currentTouchIsStillAndActive = false
private var mostRecentActiveGestureTranslation: CGPoint?
@objc dynamic public private(set) var transitionProgress: CGFloat = 0
public var transitionDelegateObservation: NSKeyValueObservation?
deinit {
scrollViewObservation?.invalidate()
transitionDelegateObservation?.invalidate()
}
public init(
viewController: UIViewController,
animationOptions: UIView.AnimationOptions = [.curveLinear],
transitionType: PullToDismissTransitionType = .slideStatic
) {
self.animationOptions = animationOptions
self.transitionType = transitionType
self.viewController = viewController
super.init()
}
public func additionalGestureRecognizerForTrigger() -> UIGestureRecognizer {
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(didPan))
panGestureRecognizer.delegate = self
return panGestureRecognizer
}
private func updateBounceLockoutState() {
guard let monitoredScrollView = monitoredScrollView else { return }
guard monitoredScrollViewDoesBounce else { return }
var doesTranslateY = false
switch transitionType {
case .slideStatic, .slideDynamic:
doesTranslateY = true
case .scale:
doesTranslateY = false
}
let shouldScrollViewBounceBeDisabled =
currentTouchIsStillAndActive ||
((mostRecentActiveGestureTranslation?.y ?? 0) > 0) ||
(doesTranslateY && transitionHasEndedAndPanIsInactive && monitoredScrollView.contentOffset.y <= 0)
guard shouldScrollViewBounceBeDisabled != didRequestScrollViewBounceDisable else { return }
didRequestScrollViewBounceDisable = shouldScrollViewBounceBeDisabled
guard monitoredScrollView.bounces != !shouldScrollViewBounceBeDisabled else { return }
monitoredScrollView.bounces = !shouldScrollViewBounceBeDisabled
}
public func monitorActiveScrollView(scrollView: UIScrollView) {
if let monitoredScrollView = monitoredScrollView, monitoredScrollViewDoesBounce {
monitoredScrollView.bounces = true
}
scrollViewObservation?.invalidate()
monitoredScrollView = scrollView
didRequestScrollViewBounceDisable = false
monitoredScrollViewDoesBounce = scrollView.bounces
recentScrollIsBlockingTransition = false
scrollViewObservation = scrollView.observe(
\UIScrollView.contentOffset,
options: [.initial, .new]
) { [weak self] scrollView, _ in
self?.updateBounceLockoutState()
guard scrollView.contentOffset.y > scrollView.bounds.size.height else { return }
self?.recentScrollIsBlockingTransition = true
self?.scrollInitiateCount += 1
let localCopyOfScrollInitiateCount = self?.scrollInitiateCount
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Const.transitionReEnableTimeoutAfterScroll
) {
guard self?.monitoredScrollView === scrollView else { return }
guard self?.scrollInitiateCount == localCopyOfScrollInitiateCount else { return }
self?.recentScrollIsBlockingTransition = false
}
}
}
private func isAtRootViewController() -> Bool {
guard let navigationController = (viewController as? UINavigationController)
?? viewController?.navigationController else { return true }
return (navigationController.viewControllers.count <= 1)
}
private func canBeginPullToDismiss(
velocity: CGPoint = .zero,
on viewController: UIViewController
) -> Bool {
return !recentScrollIsBlockingTransition &&
velocity.y > Const.velocityBeginThreshold &&
velocity.y > abs(velocity.x) &&
(permitWhenNotAtRootViewController || isAtRootViewController()) &&
(monitoredScrollView?.contentOffset.y ?? 0) <= 0 &&
(delegate?.canBeginPullToDismiss(on: viewController) ?? true)
}
private func stopPullToDismiss(on viewController: UIViewController, finished: Bool) {
if finished {
finish()
} else {
cancel()
}
guard transitionIsActiveFromTranslationPoint != nil else { return }
transitionIsActiveFromTranslationPoint = nil
delegate?.didCompletePullToDismissAttempt(on: viewController, willDismiss: finished)
}
private func handlePan(from panGestureRecognizer: UIPanGestureRecognizer, on view: UIView) {
guard let viewController = viewController else { return }
let translation = panGestureRecognizer.translation(in: view)
let velocity = panGestureRecognizer.velocity(in: view)
currentTouchIsStillAndActive = false
touchBeginOrPanIncrement += 1
switch panGestureRecognizer.state {
case .began, .changed:
mostRecentActiveGestureTranslation = translation
transitionHasEndedAndPanIsInactive = false
if let transitionIsActiveFromTranslationPoint = transitionIsActiveFromTranslationPoint {
let progress = min(1, max(
0,
(translation.y - transitionIsActiveFromTranslationPoint.y) / max(1, view.bounds.size.height)
))
if progress == 0 {
stopPullToDismiss(on: viewController, finished: false)
break
}
transitionProgress = progress
update(progress)
} else if canBeginPullToDismiss(velocity: velocity, on: viewController) {
transitionIsActiveFromTranslationPoint = translation
if let monitoredScrollView = monitoredScrollView, monitoredScrollView.isScrollEnabled {
monitoredScrollView.contentOffset = CGPoint(
x: monitoredScrollView.contentOffset.x - monitoredScrollView.contentInset.left,
y: -monitoredScrollView.contentInset.top
)
}
viewController.dismiss(animated: true) { [weak self] in
let didDismiss = (viewController.presentingViewController == nil)
if didDismiss {
self?.transitionDelegateObservation?.invalidate()
}
self?.delegate?.didFinishTransition(for: viewController, didDismiss: didDismiss)
}
delegate?.didBeginPullToDismissAttempt(on: viewController)
}
case .cancelled, .ended:
if transitionIsActiveFromTranslationPoint != nil {
transitionHasEndedAndPanIsInactive = true
}
mostRecentActiveGestureTranslation = nil
stopPullToDismiss(on: viewController, finished: panGestureRecognizer.state != .cancelled && (
(percentComplete >= Const.translationThreshold && velocity.y >= 0) ||
(
velocity.y >= Const.velocityFinishThreshold &&
translation.y >= Const.minimumTranslationYForDismiss
)
))
default:
break
}
updateBounceLockoutState()
}
@objc private func didPan(sender: Any?) {
guard let panGestureRecognizer = sender as? UIPanGestureRecognizer else { return }
guard let view = panGestureRecognizer.view else { return }
handlePan(from: panGestureRecognizer, on: view)
}
}
extension PullToDismissTransition: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
touchBeginOrPanIncrement += 1
currentTouchIsStillAndActive = true
let localCopyOfTouchBeginOrPanIncrement = touchBeginOrPanIncrement
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Const.touchStillWithoutPanEndDelay) {
guard self.touchBeginOrPanIncrement == localCopyOfTouchBeginOrPanIncrement else { return }
self.currentTouchIsStillAndActive = false
self.updateBounceLockoutState()
}
updateBounceLockoutState()
return true
}
public func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
return true
}
}
extension PullToDismissTransition: UIViewControllerAnimatedTransitioning {
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
switch transitionType {
case .slideStatic, .slideDynamic:
return Const.transitionDurationDragSlide
case .scale:
return Const.transitionDurationDragScale
}
}
private func setupTransitionViewsIfNecessary(
using transitionContext: UIViewControllerContextTransitioning,
in viewController: UIViewController
) {
if isDimmingEnabled && dimmingView == nil {
let dimmingView = UIView()
dimmingView.alpha = Const.dimmingPeakAlpha
dimmingView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
dimmingView.frame = transitionContext.containerView.bounds
let color: UIColor
switch transitionType {
case .slideStatic, .slideDynamic:
color = .black
case .scale:
color = .white
}
dimmingView.backgroundColor = color
self.dimmingView = dimmingView
transitionContext.containerView.insertSubview(dimmingView, belowSubview: viewController.view)
}
if transitionType != .slideDynamic && scalingView == nil,
let scalingView = viewController.view.resizableSnapshotView(
from: viewController.view.bounds,
afterScreenUpdates: true,
withCapInsets: .zero
) {
scalingView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
scalingView.frame = transitionContext.containerView.bounds
scalingView.transform = .identity
self.scalingView = scalingView
transitionContext.containerView.insertSubview(scalingView, aboveSubview: viewController.view)
viewController.view.isHidden = true
var shouldRoundCorners = false
switch transitionType {
case .slideStatic, .slideDynamic:
break
case .scale:
shouldRoundCorners = true
}
if #available(iOS 10.0, *) {
if shouldRoundCorners {
scalingView.layer.masksToBounds = true
UIViewPropertyAnimator(
duration: Const.scalingViewCornerRadiusToggleDuration,
curve: .easeIn
) {
scalingView.layer.cornerRadius = Const.scalingViewCornerRadius
}.startAnimation()
}
}
}
}
private func tearDownTransitionViewsAsNecessary(
using transitionContext: UIViewControllerContextTransitioning,
for viewController: UIViewController,
completionHandler: (() -> Void)? = nil
) {
if transitionContext.transitionWasCancelled, let scalingView = scalingView {
if #available(iOS 10.0, *) {
if scalingView.layer.cornerRadius > 0 {
viewController.view.layer.cornerRadius = scalingView.layer.cornerRadius
viewController.view.layer.masksToBounds = true
UIViewPropertyAnimator(
duration: Const.scalingViewCornerRadiusToggleDuration,
curve: .easeIn
) {
viewController.view.layer.cornerRadius = 0
}.startAnimation()
}
}
viewController.view.isHidden = false
scalingView.removeFromSuperview()
self.scalingView = nil
}
let completeBlock: (Bool) -> Void = { [weak self] finished -> Void in
if finished {
self?.dimmingView?.removeFromSuperview()
self?.dimmingView = nil
}
completionHandler?()
}
if dimmingView != nil {
var holdDimmingView = false
switch transitionType {
case .slideStatic, .slideDynamic:
holdDimmingView = false
case .scale:
holdDimmingView = !transitionContext.transitionWasCancelled
}
if holdDimmingView {
UIView.animate(
withDuration: Const.dimmingAlphaTransitionFinishDropDelay,
animations: { [weak self] in
self?.dimmingView?.alpha = transitionContext.transitionWasCancelled ? 1 : 0
},
completion: completeBlock
)
return
}
}
completeBlock(true)
}
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVc = transitionContext.viewController(
forKey: UITransitionContextViewControllerKey.from
) else { return }
setupTransitionViewsIfNecessary(using: transitionContext, in: fromVc)
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: animationOptions,
animations: { [weak self] in
guard let strongSelf = self else { return }
switch strongSelf.transitionType {
case .slideStatic, .slideDynamic:
guard let slideView = (
strongSelf.transitionType == .slideDynamic
?
fromVc.view
:
strongSelf.scalingView
) else { break }
slideView.frame = slideView.frame.offsetBy(
dx: 0,
dy: slideView.window?.bounds.height ?? 0
)
strongSelf.dimmingView?.alpha = 0
case .scale:
guard let scalingView = strongSelf.scalingView else { break }
scalingView.alpha = 0
scalingView.frame = scalingView.frame.insetBy(
dx: scalingView.frame.width / Const.scalingPeakScaleDivider,
dy: scalingView.frame.height / Const.scalingPeakScaleDivider
)
}
},
completion: { [weak self] _ in
self?.tearDownTransitionViewsAsNecessary(
using: transitionContext,
for: fromVc,
completionHandler: {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
)
}
)
}
}

@ -0,0 +1,106 @@
//
// PullToDismissable.swift
// IndieMusic
//
// Created by WenLei on 2024/1/11.
//
import UIKit
public protocol PullToDismissable where Self: UIViewController {
var eligibleViewControllersForPullToDismiss: [UIViewController] { get }
var isPullToDismissEnabled: Bool { get set }
var pullToDismissTransition: PullToDismissTransition { get }
func setupPullToDismiss()
func setPullToDismissEnabled(_ isEnabled: Bool)
}
extension PullToDismissable where Self: UIViewController, Self: UIViewControllerTransitioningDelegate {
public var eligibleViewControllersForPullToDismiss: [UIViewController] {
var viewControllers = [UIViewController]()
if let navigationController = navigationController {
viewControllers.append(navigationController)
}
viewControllers.append(self)
return viewControllers
}
public var isPullToDismissEnabled: Bool {
get {
return isPullToDismissEnabled(on: eligibleViewControllersForPullToDismiss)
}
set {
guard newValue != isPullToDismissEnabled else { return }
setPullToDismissEnabled(newValue)
}
}
private func isPullToDismissEnabled(on eligibleViewControllers: [UIViewController]) -> Bool {
return eligibleViewControllers.contains(where: { (viewController) -> Bool in
viewController.transitioningDelegate === self
})
}
private func tearDownPullToDismiss(on viewController: UIViewController) {
if viewController.transitioningDelegate === self {
viewController.transitioningDelegate = nil
}
guard viewController.isViewLoaded else { return }
viewController.view.gestureRecognizers?.forEach {
guard $0.delegate === pullToDismissTransition else { return }
viewController.view.removeGestureRecognizer($0)
}
}
private func propagateChanges(to eligibleViewControllers: [UIViewController], isEnabled: Bool) {
pullToDismissTransition.transitionDelegateObservation?.invalidate()
eligibleViewControllers.reversed().forEach { viewController in
if !isEnabled || eligibleViewControllers.first !== viewController {
tearDownPullToDismiss(on: viewController)
} else {
viewController.transitioningDelegate = self
pullToDismissTransition.transitionDelegateObservation = viewController.observe(
\UIViewController.transitioningDelegate,
options: [.new]
) { [weak self] _, _ in
self?.setupPullToDismiss()
}
guard viewController.isViewLoaded else { return }
guard !(viewController.view.gestureRecognizers?.contains(
where: { (gestureRecognizer) -> Bool in
gestureRecognizer.delegate === pullToDismissTransition
}
) ?? false) else { return }
viewController.view.addGestureRecognizer(
pullToDismissTransition.additionalGestureRecognizerForTrigger()
)
}
}
}
public func setupPullToDismiss() {
let eligibleViewControllers = eligibleViewControllersForPullToDismiss
propagateChanges(
to: eligibleViewControllers,
isEnabled: isPullToDismissEnabled(on: eligibleViewControllers)
)
}
public func setPullToDismissEnabled(_ isEnabled: Bool) {
propagateChanges(to: eligibleViewControllersForPullToDismiss, isEnabled: isEnabled)
}
}

@ -53,3 +53,30 @@ extension MineSingleSection: SectionModelType {
self.items = items
}
}
struct MineDownload {
let cover: String
let title: String
let detail: String
var isEditing: Bool
var isSelected: Bool
var isDownloaded: Bool
}
struct MineDownloadSection {
var items: [MineDownload]
}
extension MineDownloadSection: SectionModelType {
typealias Item = MineDownload
init(original: MineDownloadSection, items: [Item]) {
self = original
self.items = items
}
}

@ -66,6 +66,13 @@ enum HomeTabBarItem: Int {
class HomeTabBarController: UITabBarController, Navigatable {
// private var playerPresentInteractor:PlayerInteractor!
// private var playerDismissInteractor:PlayerInteractor!
// var playerVC: PlayerViewController!
var viewModel: HomeTabBarViewModel?
var navigator: Navigator!
@ -105,6 +112,22 @@ class HomeTabBarController: UITabBarController, Navigatable {
// Do any additional setup after loading the view.
makeUI()
bindViewModel()
guard let viewModel = viewModel else { return }
playerTabBar.addTarget(self, action: #selector(playerTabBarClick), for: .touchUpInside)
// let playerViewModel = PlayerViewModel.init(provider: viewModel.provider)
// self.playerVC = PlayerViewController.init(viewModel: playerViewModel, navigator: self.navigator)
// playerVC.modalPresentationStyle = .custom
// playerVC.transitioningDelegate = self
//
// playerPresentInteractor = PlayerInteractor(self, self.playerTabBar, playerVC)
// playerPresentInteractor.wantsInteractiveStart = false
// playerDismissInteractor = PlayerInteractor(playerVC, playerVC.view, nil)
// playerDismissInteractor.wantsInteractiveStart = false
}
func makeUI() {
@ -170,6 +193,76 @@ class HomeTabBarController: UITabBarController, Navigatable {
}
func showAllBar(_ isShowing: Bool, animated: Bool) {
if isAnimating { return }
isAnimating = true
if isShowing {
customeTabBar.isHidden = false
playerTabBar.isHidden = false
customeTabBar.snp.remakeConstraints { make in
make.left.equalTo(view)
make.right.equalTo(view)
make.bottom.equalTo(view)
make.height.equalTo(BaseDimensions.tabBarHeight)
}
playerTabBar.snp.remakeConstraints { make in
make.left.equalTo(view)
make.right.equalTo(view)
make.bottom.equalTo(customeTabBar.snp.top)
}
} else {
customeTabBar.snp.remakeConstraints { make in
make.left.equalTo(view)
make.right.equalTo(view)
make.bottom.equalTo(view).offset(self.tabBar.bounds.height)
make.height.equalTo(BaseDimensions.tabBarHeight)
}
playerTabBar.snp.remakeConstraints { make in
make.left.equalTo(view)
make.right.equalTo(view)
make.bottom.equalTo(customeTabBar.snp.top).offset(BaseDimensions.tabBarHeight)
}
}
var animationDuration = 0.3
if animated == false {
animationDuration = 0
}
UIView.animate(withDuration: animationDuration,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.25,
options: .curveEaseInOut,
animations: {
if isShowing {
self.customeTabBar.alpha = 1
self.playerTabBar.alpha = 1
} else {
self.customeTabBar.alpha = 0
self.playerTabBar.alpha = 0
}
self.view.layoutIfNeeded()
}) { (completed) in
self.isTabBarShowing = isShowing
self.isPlayerBarShowing = isShowing
self.customeTabBar.isHidden = !isShowing
self.playerTabBar.isHidden = !isShowing
self.isAnimating = false
}
}
open func showTabBar(_ isShowing: Bool, animated: Bool) {
if isAnimating { return }
@ -271,5 +364,41 @@ class HomeTabBarController: UITabBarController, Navigatable {
}
@objc func playerTabBarClick() {
guard let viewModel = viewModel else { return }
let playerViewModel = PlayerViewModel.init(provider: viewModel.provider)
self.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal)
// let player = PlayerViewController.init(viewModel: playerViewModel, navigator: self.navigator)
// player.modalPresentationStyle = .custom
// player.isPullToDismissEnabled = true
// player.modalPresentationCapturesStatusBarAppearance = true
//
// self.present(player, animated: true)
}
}
//extension HomeTabBarController: UIViewControllerTransitioningDelegate {
//
// func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// return PlayerPresentAnimator()
// }
//
// func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
// return playerPresentInteractor
// }
//
// func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// return PlayerDismissAnimator()
// }
//
// func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
// return playerDismissInteractor
// }
//
//}
//
//

@ -160,6 +160,16 @@ class HomeViewCell: UITableViewCell {
return commentCountButton
}()
var homeJournal: HomeJournal? {
didSet {
titleLabel.text = homeJournal?.title
subTitleLabel.text = homeJournal?.subTitle
commentLabel.text = homeJournal?.comment
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)

@ -50,7 +50,7 @@ class HomeViewController: TableViewController {
// self.navigationController?.pushViewController(vc, animated: true)
if let tabbar = self.tabBarController as? HomeTabBarController {
tabbar.showTabBar(true, animated: true)
tabbar.showAllBar(true, animated: true)
}
guard let viewModel = viewModel as? HomeViewModel else { return }
@ -126,7 +126,7 @@ class HomeViewController: TableViewController {
make.bottom.equalTo(view).offset( -BaseDimensions.tabBarHeight)
make.left.equalTo(view)
make.right.equalTo(view)
make.height.equalTo(86)
make.height.equalTo(100)
}
@ -180,7 +180,10 @@ extension HomeViewController {
case .journalDetil(let model):
let cell: HomeViewCell = tableView.dequeueReusableCell(withIdentifier: "HomeViewCell", for: indexPath) as! HomeViewCell
cell.homeJournal = model
return cell
default: UITableViewCell()
}
}
}
@ -192,8 +195,13 @@ extension HomeViewController {
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = HomeSectionView.init()
header.theFMButtonClosures = {
header.theFMButtonClosures = {[weak self] in
guard let viewModel = self?.viewModel else { return }
let playerViewModel = PlayerViewModel.init(provider: viewModel.provider)
self?.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal)
}

@ -8,15 +8,6 @@
import UIKit
class SongViewCell: UITableViewCell {
lazy var selectButton: UIButton = {
let selectButton = UIButton.init()
selectButton.setImage(UIImage.init(named: ""), for: .normal)
selectButton.setImage(UIImage.init(named: ""), for: .selected)
return selectButton
}()
lazy var coverView: UIImageView = {
let coverView = UIImageView.init()
@ -57,39 +48,7 @@ class SongViewCell: UITableViewCell {
var buttonTapCallback: ((AudioTrack) -> ())?
override var isEditing: Bool {
didSet {
selectButton.isHidden = !isEditing
if isEditing {
selectButton.snp.remakeConstraints { make in
make.left.equalTo(contentView).offset(18)
make.top.equalTo(contentView).offset(29)
}
coverView.snp.remakeConstraints { make in
make.left.equalTo(selectButton.snp.right).offset(10)
make.top.equalTo(contentView).offset(15)
make.size.equalTo(CGSize.init(width: 64, height: 64))
make.bottom.equalTo(contentView).offset(-23)
}
} else {
coverView.snp.remakeConstraints { make in
make.left.equalTo(contentView).offset(18)
make.top.equalTo(contentView).offset(15)
make.size.equalTo(CGSize.init(width: 64, height: 64))
make.bottom.equalTo(contentView).offset(-23)
}
}
}
}
var audioTrack: AudioTrack? {
didSet {
@ -112,7 +71,6 @@ class SongViewCell: UITableViewCell {
}
func makeUI () {
contentView.addSubview(selectButton)
contentView.addSubview(coverView)
contentView.addSubview(titleLabel)
contentView.addSubview(detailLabel)

@ -115,7 +115,7 @@ class JournalDetailController: ViewController, UIScrollViewDelegate {
if let tabbar = self.tabBarController as? HomeTabBarController {
tabbar.showTabBar(false, animated: true)
tabbar.showAllBar(false, animated: true)
}
}

@ -59,7 +59,7 @@ class BindPhoneViewController: ViewController {
if let tabbar = self.tabBarController as? HomeTabBarController {
tabbar.showTabBar(false, animated: true)
tabbar.showAllBar(false, animated: true)
}
}

@ -71,7 +71,7 @@ class LoginViewController: ViewController {
if let tabbar = self.tabBarController as? HomeTabBarController {
tabbar.showTabBar(false, animated: true)
tabbar.showAllBar(false, animated: true)
}
}

@ -6,15 +6,400 @@
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
class MineDownloadViewController: ViewController {
let mineSingleNoDataView: MineSingleNoDataView = {
let mineSingleNoDataView = MineSingleNoDataView.init()
mineSingleNoDataView.isHidden = true
return mineSingleNoDataView
}()
let headerView: MineSingleHeaderView = {
let headerView = MineSingleHeaderView.init()
headerView.editButton.isHidden = false
return headerView
}()
let tableView: UITableView = {
let tableView = UITableView.init()
tableView.register(MineDownloadViewCell.self, forCellReuseIdentifier: "MineDownloadViewCell")
return tableView
}()
let mineDownloadBottomView: MineDownloadBottomView = {
let mineDownloadBottomView = MineDownloadBottomView.init()
mineDownloadBottomView.isHidden = true
return mineDownloadBottomView
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func makeUI() {
super.makeUI()
self.navigationItem.title = "已下载"
view.backgroundColor = .white
tableView.register(SongViewCell.self
, forCellReuseIdentifier: "SongViewCell")
view.addSubview(mineSingleNoDataView)
view.addSubview(headerView)
view.addSubview(tableView)
view.addSubview(mineDownloadBottomView)
}
override func bindViewModel() {
super.bindViewModel()
guard let viewModel = viewModel as? MineDownloadViewModel else { return }
let input = MineDownloadViewModel.Input.init(viewWillAppear: rx.viewWillAppear,
selection: tableView.rx.itemSelected.asDriver(),
editButtonTrigger: self.headerView.editButton.rx.tap.asDriver())
let output = viewModel.transform(input: input)
let dataSource = MineDownloadViewController.dataSource { [weak self] cell, mineDownload in
let audioMoreActionViewModel = AudioMoreActionViewModel.init(provider: viewModel.provider)
self?.navigator.show(segue: .audioMore(viewModel: audioMoreActionViewModel), sender: self, transition: .navigationPresent(type: .audioMore))
}
viewModel.isEditing
.bind(to: self.headerView.editButton.rx.isSelected)
.disposed(by: rx.disposeBag)
viewModel.isEditing.subscribe { [weak self] isEditing in
self?.headerView.editButton.isSelected = isEditing
self?.mineDownloadBottomView.isHidden = !isEditing
guard let view = self?.view,
let headerView = self?.headerView,
let mineDownloadBottomView = self?.mineDownloadBottomView else { return }
// self?.mineDownloadBottomView.snp.updateConstraints { make in
// make.height.equalTo(!isEditing ? 0 : 30)
// }
self?.tableView.snp.remakeConstraints({ make in
make.left.equalTo(view)
make.right.equalTo(view)
make.top.equalTo(headerView.snp.bottom)
make.bottom.equalTo(!isEditing ? view.snp.bottom : mineDownloadBottomView.snp.top)
})
self?.tableView.setNeedsLayout()
self?.tableView.layoutIfNeeded()
} .disposed(by: rx.disposeBag)
output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag)
output.itemSelected.subscribe { [weak self] sectionItem in
}.disposed(by: rx.disposeBag)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
headerView.snp.makeConstraints { make in
make.left.equalTo(view)
make.right.equalTo(view)
make.top.equalTo(view)
make.height.equalTo(64)
}
mineDownloadBottomView.snp.makeConstraints { make in
make.left.equalTo(view)
make.right.equalTo(view)
make.height.equalTo(30)
make.bottom.equalTo(view).offset(-BaseDimensions.bottomHeight - 25)
}
tableView.snp.makeConstraints { make in
make.left.equalTo(view)
make.right.equalTo(view)
make.top.equalTo(headerView.snp.bottom)
make.bottom.equalTo(view)
}
mineSingleNoDataView.snp.makeConstraints { make in
make.left.equalTo(view)
make.right.equalTo(view)
make.top.equalTo(view)
make.height.equalTo(200)
}
}
}
extension MineDownloadViewController {
static func dataSource(_ buttonTapHandler: @escaping (UITableViewCell, MineDownload) -> Void) -> RxTableViewSectionedReloadDataSource<MineDownloadSection> {
return RxTableViewSectionedReloadDataSource<MineDownloadSection>(
configureCell: { dataSource, tableView, indexPath, item in
let cell: MineDownloadViewCell = tableView.dequeueReusableCell(withIdentifier: "MineDownloadViewCell", for: indexPath) as! MineDownloadViewCell
cell.mineDownload = item
cell.buttonTapCallback = {mineDownload in
buttonTapHandler(cell, item)
}
return cell
}
)
}
}
class MineDownloadViewCell: UITableViewCell {
lazy var selectButton: UIButton = {
let selectButton = UIButton.init()
selectButton.setImage(UIImage.init(named: "mine_select_off"), for: .normal)
selectButton.setImage(UIImage.init(named: "mine_select_on"), for: .selected)
return selectButton
}()
lazy var coverView: UIImageView = {
let coverView = UIImageView.init()
coverView.layer.cornerRadius = 3
coverView.layer.masksToBounds = true
coverView.backgroundColor = .gray
return coverView
}()
lazy var titleLabel: UILabel = {
let titleLabel = UILabel.init()
titleLabel.font = UIFont.systemFont(ofSize: 15, weight: .medium)
return titleLabel
}()
lazy var detailLabel: UILabel = {
let detailLabel = UILabel.init()
detailLabel.font = UIFont.systemFont(ofSize: 12)
detailLabel.textColor = .init(hex: 0x000000, alpha: 0.6)
return detailLabel
}()
lazy var moreButtin: UIButton = {
let menuButtin = UIButton.init()
menuButtin.setImage(UIImage.init(named: "audio_more_btn"), for: .normal)
menuButtin.setContentHuggingPriority(.required, for: .horizontal)
menuButtin.setContentCompressionResistancePriority(.required, for: .horizontal)
menuButtin.addTarget(self, action: #selector(menuButtinClick), for: .touchUpInside)
return menuButtin
}()
var buttonTapCallback: ((MineDownload) -> ())?
override var isSelected: Bool {
didSet {
}
}
override var isEditing: Bool {
didSet {
selectButton.isHidden = !isEditing
if isEditing {
coverView.snp.remakeConstraints { make in
make.left.equalTo(contentView.snp.left).offset(48)
make.top.equalTo(contentView).offset(15)
make.size.equalTo(CGSize.init(width: 64, height: 64))
make.bottom.equalTo(contentView).offset(-23)
}
selectButton.snp.remakeConstraints { make in
make.left.equalTo(contentView).offset(18)
make.centerY.equalTo(coverView)
make.size.equalTo(CGSize.init(width: 20, height: 20))
}
} else {
coverView.snp.remakeConstraints { make in
make.left.equalTo(contentView).offset(18)
make.top.equalTo(contentView).offset(15)
make.size.equalTo(CGSize.init(width: 64, height: 64))
make.bottom.equalTo(contentView).offset(-23)
}
}
}
}
var mineDownload: MineDownload? {
didSet {
guard let mineDownload = mineDownload else { return }
titleLabel.text = mineDownload.title
detailLabel.text = mineDownload.detail
isEditing = mineDownload.isEditing
selectButton.isSelected = mineDownload.isSelected
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
makeUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func makeUI () {
contentView.addSubview(selectButton)
contentView.addSubview(coverView)
contentView.addSubview(titleLabel)
contentView.addSubview(detailLabel)
contentView.addSubview(moreButtin)
coverView.snp.makeConstraints { make in
make.left.equalTo(contentView).offset(18)
make.top.equalTo(contentView).offset(15)
make.size.equalTo(CGSize.init(width: 64, height: 64))
make.bottom.equalTo(contentView).offset(-23)
}
moreButtin.snp.makeConstraints { make in
make.right.equalTo(contentView).offset(-18)
make.centerY.equalTo(coverView)
}
titleLabel.snp.makeConstraints { make in
make.left.equalTo(coverView.snp.right).offset(15)
make.top.equalTo(coverView).offset(10)
make.right.equalTo(moreButtin.snp.left).offset(-10)
}
detailLabel.snp.makeConstraints { make in
make.left.equalTo(coverView.snp.right).offset(15)
make.top.equalTo(titleLabel.snp.bottom).offset(10)
make.right.equalTo(moreButtin.snp.left).offset(-6)
}
}
@objc func menuButtinClick() {
if let buttonTapCallback = self.buttonTapCallback,
let mineDownload = self.mineDownload {
buttonTapCallback(mineDownload)
}
}
}
class MineDownloadBottomView: UIView {
let allSelectButton: UIButton = {
let allSelectButton = UIButton.init()
allSelectButton.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .medium)
allSelectButton.setTitle("全选", for: .normal)
allSelectButton.setTitleColor(.primaryText(), for: .normal)
return allSelectButton
}()
let deleteButton: UIButton = {
let deleteButton = UIButton.init()
deleteButton.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .medium)
deleteButton.setTitle("删除", for: .normal)
deleteButton.setTitleColor(.primary(), for: .normal)
return deleteButton
}()
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func makeUI() {
backgroundColor = .white
addSubview(allSelectButton)
addSubview(deleteButton)
}
override func layoutSubviews() {
super.layoutSubviews()
allSelectButton.snp.makeConstraints { make in
make.left.equalTo(self).offset(18)
make.bottom.equalTo(self)
make.top.equalTo(self)
}
deleteButton.snp.makeConstraints { make in
make.left.equalTo(allSelectButton.snp.right).offset(22)
make.bottom.equalTo(self)
make.top.equalTo(self)
}
}
}

@ -14,18 +14,21 @@ class MineDownloadViewModel: ViewModel, ViewModelType {
struct Input {
let viewWillAppear: ControlEvent<Bool>
let selection: Driver<IndexPath>
let editButtonTrigger: Driver<Void>
}
struct Output {
let items: BehaviorRelay<[MineSingleSection]>
let items: BehaviorRelay<[MineDownloadSection]>
let selection: Driver<IndexPath>
let itemSelected: PublishSubject<MineSingle>
let itemSelected: PublishSubject<MineDownload>
}
let itemSelected = PublishSubject<MineSingle>()
let items = BehaviorRelay<[MineSingleSection]>.init(value: [])
let itemSelected = PublishSubject<MineDownload>()
let items = BehaviorRelay<[MineDownloadSection]>.init(value: [])
let isEditing = BehaviorRelay<Bool>.init(value: false)
func transform(input: Input) -> Output {
@ -33,14 +36,47 @@ class MineDownloadViewModel: ViewModel, ViewModelType {
}.disposed(by: rx.disposeBag)
let mine = MineSingle.init(cover: "", title: "", detail: "")
let mine = MineDownload.init(cover: "", title: "123", detail: "212", isEditing: false, isSelected: false, isDownloaded: false)
items.accept([MineDownloadSection.init(items: [mine, mine, mine, mine, mine, mine, mine, mine, mine])])
items.accept([MineSingleSection.init(items: [mine, mine, mine])])
input.editButtonTrigger.drive { _ in
self.isEditing.accept(!self.isEditing.value)
}.disposed(by: rx.disposeBag)
isEditing.subscribe { isEditing in
guard var currentItems = self.items.value.first?.items else { return }
currentItems = currentItems.map { item in
var modifiedItem = item
modifiedItem.isEditing = isEditing
return modifiedItem
}
self.items.accept([MineDownloadSection.init(items: currentItems)])
input.selection.drive { indexPath in
guard let sectionItem = self.items.value.first?.items[indexPath.row] else { return }
self.itemSelected.onNext(sectionItem)
}.disposed(by: rx.disposeBag)
input.selection.drive { [weak self] indexPath in
guard var currentItems = self?.items.value else { return }
var sectionItem = currentItems[indexPath.section].items[indexPath.row]
if self?.isEditing.value == true {
sectionItem.isSelected.toggle()
self?.items.accept(currentItems)
} else {
self?.itemSelected.onNext(sectionItem)
}
}.disposed(by: rx.disposeBag)

@ -198,6 +198,19 @@ class MineSingleHeaderView: UIView {
return playAllButton
}()
let editButton: UIButton = {
let editButton = UIButton.init()
editButton.titleLabel?.font = UIFont.systemFont(ofSize: 15)
editButton.setTitleColor(.primaryText(), for: .normal)
editButton.setTitle("选择", for: .normal)
editButton.setTitle("取消", for: .selected)
editButton.isHidden = true
return editButton
}()
override init(frame: CGRect) {
super.init(frame: frame)
@ -207,24 +220,24 @@ class MineSingleHeaderView: UIView {
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// override init(frame: CGRect) {
// super.init(frame: frame)
//
// makeUI()
// }
func makeUI() {
backgroundColor = .white
addSubview(playAllButton)
addSubview(editButton)
}
override func layoutSubviews() {
super.layoutSubviews()
editButton.snp.makeConstraints { make in
make.right.equalTo(self).offset(-28)
make.centerY.equalTo(self)
}
playAllButton.snp.makeConstraints { make in
make.left.equalTo(self).offset(18)
make.centerY.equalTo(self)

@ -32,7 +32,7 @@ class MineViewController: TableViewController {
super.viewWillDisappear(animated)
if let tabbar = self.tabBarController as? HomeTabBarController {
tabbar.showTabBar(false, animated: true)
tabbar.showAllBar(false, animated: true)
}
}
@ -42,7 +42,7 @@ class MineViewController: TableViewController {
super.viewWillAppear(animated)
if let tabbar = self.tabBarController as? HomeTabBarController {
tabbar.showTabBar(true, animated: true)
tabbar.showAllBar(true, animated: true)
}
}

@ -7,7 +7,7 @@
import UIKit
class PlayerTabBar: UIView {
class PlayerTabBar: UIControl {
private var coverView: UIImageView = {
var coverView = UIImageView.init()
coverView.backgroundColor = .red
@ -38,7 +38,7 @@ class PlayerTabBar: UIView {
return playButton
}()
private var listButton: UIButton = {
var listButton: UIButton = {
var listButton = UIButton.init()
listButton.setImage(UIImage.init(named: "playerBar_list_btn"), for: .normal)
return listButton
@ -133,5 +133,15 @@ class PlayerTabBar: UIView {
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
super.hitTest(point, with: event)
if !self.isHidden, self.bounds.contains(point) {
return self
}
return super.hitTest(point, with: event)
}
}

@ -11,10 +11,11 @@ import RxCocoa
import RxDataSources
class PlayerViewController: ViewController {
var blurEffectView: BlurEffectView = {
let blurEffectView = BlurEffectView.init()
// blurEffectView.isHidden = true
return blurEffectView
}()
@ -22,13 +23,14 @@ class PlayerViewController: ViewController {
let playerViewTopBar = PlayerViewTopBar.init()
playerViewTopBar.setContentHuggingPriority(.required, for: .vertical)
playerViewTopBar.setContentCompressionResistancePriority(.required, for: .vertical)
// playerViewTopBar.isHidden = true
return playerViewTopBar
}()
var playerScrollView: PlayerScrollView = {
let playerScrollView = PlayerScrollView.init()
// playerScrollView.isHidden = true
return playerScrollView
}()
@ -38,6 +40,7 @@ class PlayerViewController: ViewController {
let playerControlView = PlayerControlView.init()
playerControlView.setContentHuggingPriority(.required, for: .vertical)
playerControlView.setContentCompressionResistancePriority(.required, for: .vertical)
// playerControlView.isHidden = true
return playerControlView
}()
@ -48,7 +51,9 @@ class PlayerViewController: ViewController {
super.viewDidLoad()
// let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleDismissGesture(_:)))
// view.addGestureRecognizer(panGesture)
// playerControlView.addGestureRecognizer(panGesture)
}
override func viewWillAppear(_ animated: Bool) {
@ -56,6 +61,8 @@ class PlayerViewController: ViewController {
}
override func makeUI() {
super.makeUI()
@ -88,6 +95,14 @@ class PlayerViewController: ViewController {
output.items.bind(to: playerScrollView.playerLyricsView.tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag)
self.playerViewTopBar.dropButton.rx.tap.subscribe { [weak self] _ in
// self?.navigator.dismiss(sender: self)
self?.navigationController?.dismiss(animated: true)
}.disposed(by: rx.disposeBag)
}
@ -125,6 +140,29 @@ class PlayerViewController: ViewController {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.dismiss(animated: true)
}
@objc func handleDismissGesture(_ gesture: UIPanGestureRecognizer) {
// let translation = gesture.translation(in: view)
// let progress = translation.y / view.bounds.height
//
// switch gesture.state {
// case .changed:
// //
// break
// case .ended:
// if progress > 0.5 {
// //
// self.navigationController?.dismiss(animated: true, completion: nil)
// } else {
// //
// }
// default:
// break
// }
}
}
extension PlayerViewController {
@ -154,3 +192,4 @@ extension PlayerViewController: PlayerScrollViewDelegate {
playerViewTopBar.segmentControl.move(to: index, animated: true)
}
}

@ -97,7 +97,7 @@ class MusicStyleViewController: ViewController {
if let tabbar = self.tabBarController as? HomeTabBarController {
tabbar.showTabBar(false, animated: true)
tabbar.showAllBar(false, animated: true)
}
}

@ -38,10 +38,20 @@ class SearchViewController: ViewController, UIScrollViewDelegate {
super.viewWillAppear(animated)
if let tabbar = self.tabBarController as? HomeTabBarController {
tabbar.showTabBar(true, animated: true)
tabbar.showPlayerBar(true, animated: true)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// if let tabbar = self.tabBarController as? HomeTabBarController {
// tabbar.showPlayerBar(false, animated: true)
// }
}
override func viewDidLoad() {
super.viewDidLoad()

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

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" rx="10" fill="white"/>
<rect x="0.5" y="0.5" width="19" height="19" rx="9.5" stroke="black" stroke-opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 247 B

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

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" rx="10" fill="black" fill-opacity="0.95"/>
<path d="M6 10L9 13L14 7.5" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 262 B

Loading…
Cancel
Save