From 106f56637eb704792d4fff3f4ea1e52ad4482459 Mon Sep 17 00:00:00 2001 From: wenlei Date: Mon, 19 Feb 2024 09:21:16 +0800 Subject: [PATCH] Comment on details of interactive optimization --- .../IndieMusic.xcodeproj/project.pbxproj | 4 + .../IndieMusic/Application/Navigator.swift | 18 +- .../Application/SceneDelegate.swift | 4 +- .../Common/NavigationController.swift | 28 +- .../Common/PresentAndDismissTransition.swift | 111 ++++++++ .../Common/PullToDismissTransition.swift | 4 +- .../IndieMusic/Common/ViewController.swift | 6 +- .../IndieMusic/Managers/AudioManager.swift | 162 ++++++++--- IndieMusic/IndieMusic/Models/Feedback.swift | 2 +- .../Modules/Home/HomeTabBarController.swift | 143 +++++----- .../Modules/Home/HomeTabBarViewModel.swift | 5 + .../Modules/Home/HomeViewController.swift | 24 +- .../CommentDetailViewController.swift | 256 +++++++++++++++--- .../CommentDetailViewModel.swift | 121 +++++++-- .../Modules/JournalDetail/CommentView.swift | 37 +++ .../JournalDetail/CommentViewController.swift | 80 +++++- .../JournalDetail/CommentViewModel.swift | 52 ++-- .../JournalDetailController.swift | 3 +- .../Modules/Login/LoginViewController.swift | 6 +- .../Message/MessageViewController.swift | 18 +- .../Modules/Message/MessageViewModel.swift | 32 +-- .../Modules/Mine/MineSingleController.swift | 23 ++ .../Modules/Mine/MineSingleViewModel.swift | 30 +- .../IndieMusic/Modules/Mine/MineView.swift | 9 + .../Modules/Mine/MineViewController.swift | 82 ++++-- .../Modules/Mine/MineViewModel.swift | 29 +- .../Personal/FollowersViewController.swift | 3 - .../Personal/FollowingViewController.swift | 19 +- .../Modules/Personal/FollowingViewModel.swift | 9 +- .../Personal/PersonalViewController.swift | 5 +- .../Modules/Personal/PersonalViewModel.swift | 8 +- .../Modules/Search/SearchViewController.swift | 15 +- .../EditInfo/EditInfoViewController.swift | 26 +- .../Setting/EditInfo/EditNameController.swift | 24 +- .../EditInfo/EditSexViewController.swift | 2 +- .../EditSignatureViewController.swift | 8 + .../Setting/FeedbackViewController.swift | 143 +++++++--- .../Modules/Setting/FeedbackViewModel.swift | 116 +++++++- IndieMusic/IndieMusic/Networking/Api.swift | 2 + .../Networking/Rest/APIConfig.swift | 37 ++- .../Networking/Rest/Networking.swift | 1 - .../IndieMusic/Networking/Rest/RestApi.swift | 21 +- 42 files changed, 1339 insertions(+), 389 deletions(-) create mode 100644 IndieMusic/IndieMusic/Common/PresentAndDismissTransition.swift diff --git a/IndieMusic/IndieMusic.xcodeproj/project.pbxproj b/IndieMusic/IndieMusic.xcodeproj/project.pbxproj index fad14a1..901bb58 100644 --- a/IndieMusic/IndieMusic.xcodeproj/project.pbxproj +++ b/IndieMusic/IndieMusic.xcodeproj/project.pbxproj @@ -185,6 +185,7 @@ 77C9B9ED2B4BEA610006C83F /* MyCommentListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9B9EC2B4BEA610006C83F /* MyCommentListViewModel.swift */; }; 77C9B9EF2B4C2A910006C83F /* AudioTrackListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9B9EE2B4C2A910006C83F /* AudioTrackListViewController.swift */; }; 77C9B9F12B4C2B3A0006C83F /* AudioTrackListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9B9F02B4C2B3A0006C83F /* AudioTrackListViewModel.swift */; }; + 77CEFEFC2B81EC600071B671 /* PresentAndDismissTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CEFEFB2B81EC600071B671 /* PresentAndDismissTransition.swift */; }; 77DFA9C52B4E8388005B8B13 /* MineDownloadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77DFA9C42B4E8388005B8B13 /* MineDownloadViewController.swift */; }; 77DFA9C72B4E83B0005B8B13 /* MineDownloadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77DFA9C62B4E83B0005B8B13 /* MineDownloadViewModel.swift */; }; 77FA0B282B0B3E1E00404C5E /* Journal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FA0B272B0B3E1E00404C5E /* Journal.swift */; }; @@ -434,6 +435,7 @@ 77C9B9EC2B4BEA610006C83F /* MyCommentListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCommentListViewModel.swift; sourceTree = ""; }; 77C9B9EE2B4C2A910006C83F /* AudioTrackListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioTrackListViewController.swift; sourceTree = ""; }; 77C9B9F02B4C2B3A0006C83F /* AudioTrackListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioTrackListViewModel.swift; sourceTree = ""; }; + 77CEFEFB2B81EC600071B671 /* PresentAndDismissTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAndDismissTransition.swift; sourceTree = ""; }; 77DFA9C42B4E8388005B8B13 /* MineDownloadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MineDownloadViewController.swift; sourceTree = ""; }; 77DFA9C62B4E83B0005B8B13 /* MineDownloadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MineDownloadViewModel.swift; sourceTree = ""; }; 77FA0B272B0B3E1E00404C5E /* Journal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Journal.swift; sourceTree = ""; }; @@ -727,6 +729,7 @@ 77C9B9D22B4B9B180006C83F /* BorderLabel.swift */, 77A659CE2B4FDC4100B408C3 /* PullToDismissable.swift */, 77A659D02B4FDC5200B408C3 /* PullToDismissTransition.swift */, + 77CEFEFB2B81EC600071B671 /* PresentAndDismissTransition.swift */, 770228F02B57AD2C00E07F7A /* RefreshLoadingView.swift */, 775D075D2B5E5BCA009270D3 /* GradientLayerLabel.swift */, ); @@ -1314,6 +1317,7 @@ 7751D3702B43A4FC00F1F2BD /* CacheViewController.swift in Sources */, 7751D3642B42BC2E00F1F2BD /* AccountViewModel.swift in Sources */, 778B8A5D2AF8EC610034AFD4 /* Application.swift in Sources */, + 77CEFEFC2B81EC600071B671 /* PresentAndDismissTransition.swift in Sources */, 7751D3622B42BC0C00F1F2BD /* AccountViewController.swift in Sources */, 77A659CF2B4FDC4100B408C3 /* PullToDismissable.swift in Sources */, 77620D9A2B69DA1A00798861 /* EditSignatureViewController.swift in Sources */, diff --git a/IndieMusic/IndieMusic/Application/Navigator.swift b/IndieMusic/IndieMusic/Application/Navigator.swift index 44a7d4b..6574a1b 100644 --- a/IndieMusic/IndieMusic/Application/Navigator.swift +++ b/IndieMusic/IndieMusic/Application/Navigator.swift @@ -86,19 +86,11 @@ class Navigator { func get(segue: Scene) -> UIViewController? { switch segue { case .tabs(let viewModel): - let rootVC = HomeTabBarController(viewModel: viewModel, navigator: self) -// let rootVC = IndieMusicViewController(viewModel: viewModel, navigator: self) - - - let home = HomeViewController(viewModel: viewModel, navigator: self) - let search = SearchViewController(viewModel: viewModel, navigator: self) - let mine = MineViewController(viewModel: viewModel, navigator: self) + let tabVC = HomeTabBarController(viewModel: viewModel, navigator: self) -// rootVC.setViewController([home, search, mine]) - - rootVC.viewControllers = [home, search, mine] - return rootVC + let nav = tabVC + return nav case .search(let viewModel): return SearchViewController(viewModel: viewModel, navigator: self) case .journalDetail(viewModel: let viewModel): return JournalDetailController.init(viewModel: viewModel, navigator: self) @@ -276,8 +268,8 @@ class Navigator { case .modal: DispatchQueue.main.async { let nav = NavigationController(rootViewController: target) - nav.setNavigationBarHidden(true, animated: true) -// nav.isPullToDismissEnabled = true + nav.setNavigationBarHidden(true, animated: false) + nav.isPullToDismissEnabled = true nav.modalPresentationStyle = .custom nav.modalPresentationCapturesStatusBarAppearance = true diff --git a/IndieMusic/IndieMusic/Application/SceneDelegate.swift b/IndieMusic/IndieMusic/Application/SceneDelegate.swift index c7a2a1f..7b3b323 100644 --- a/IndieMusic/IndieMusic/Application/SceneDelegate.swift +++ b/IndieMusic/IndieMusic/Application/SceneDelegate.swift @@ -24,8 +24,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, WXApiDelegate { let libsManager = LibsManager.shared libsManager.setupLibs(with: window) - IQKeyboardManager.shared.enable = true - IQKeyboardManager.shared.enableAutoToolbar = true +// IQKeyboardManager.shared.enable = true +// IQKeyboardManager.shared.enableAutoToolbar = true Application.shared.presentInitialScreen(in: window!) diff --git a/IndieMusic/IndieMusic/Common/NavigationController.swift b/IndieMusic/IndieMusic/Common/NavigationController.swift index 85479e9..aab1039 100644 --- a/IndieMusic/IndieMusic/Common/NavigationController.swift +++ b/IndieMusic/IndieMusic/Common/NavigationController.swift @@ -101,11 +101,17 @@ class NavigationController: UINavigationController { extension NavigationController { override func pushViewController(_ viewController: UIViewController, animated: Bool) { + + if !children.isEmpty { + +// viewController.hidesBottomBarWhenPushed = true +// if children.count > 1 { +// viewController.hidesBottomBarWhenPushed = false +// } if let tabbar = self.tabBarController as? HomeTabBarController { tabbar.showAllBar(false, animated: true) } - // viewController.hidesBottomBarWhenPushed = true // let leftButton = UIButton.init() @@ -146,6 +152,14 @@ extension NavigationController { return super.popViewController(animated: animated) } + override func popToRootViewController(animated: Bool) -> [UIViewController]? { + if let tabbar = self.tabBarController as? HomeTabBarController { + tabbar.refreshTabBarItems() + } + + return super.popToRootViewController(animated: animated) + } + @objc func backAction() { self.popViewController(animated: true) } @@ -188,11 +202,21 @@ extension NavigationController: PullToDismissable, UIViewControllerTransitioning setupPullToDismiss() } + func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + guard isPullToDismissEnabled else { return nil } + + if self.children.first is CommentDetailViewController { + return PresentAndDismissTransition(false) + } else { + return nil + } + } + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { guard isPullToDismissEnabled else { return nil } - return pullToDismissTransition + return PresentAndDismissTransition(true) } func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) diff --git a/IndieMusic/IndieMusic/Common/PresentAndDismissTransition.swift b/IndieMusic/IndieMusic/Common/PresentAndDismissTransition.swift new file mode 100644 index 0000000..663e12e --- /dev/null +++ b/IndieMusic/IndieMusic/Common/PresentAndDismissTransition.swift @@ -0,0 +1,111 @@ +// +// PresentAndDismissTransition.swift +// IndieMusic +// +// Created by WenLei on 2024/2/18. +// + +import Foundation + +class DimmingView:UIView { + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = UIColor.black + self.alpha = 0 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class PresentAndDismissTransition: NSObject, UIViewControllerAnimatedTransitioning { + + private var isDismiss:Bool! + + convenience init(_ isDismiss:Bool) { + self.init() + self.isDismiss = isDismiss + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.4 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + + guard let toViewController = transitionContext.viewController(forKey: .to),let fromViewController = transitionContext.viewController(forKey: .from) else { + return + } + + if !self.isDismiss { + //Present + + toViewController.view.frame.size.height -= BaseDimensions.topHeight + 22 + toViewController.view.frame.origin.y = UIScreen.main.bounds.size.height + transitionContext.containerView.addSubview(toViewController.view) + + let toViewpath = UIBezierPath(roundedRect: toViewController.view.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: 6, height: 6)) + let toViewmask = CAShapeLayer() + toViewmask.path = toViewpath.cgPath + toViewController.view.layer.mask = toViewmask + + let fromViewpath = UIBezierPath(roundedRect: fromViewController.view.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: 6, height: 6)) + let fromViewmask = CAShapeLayer() + fromViewmask.path = fromViewpath.cgPath + fromViewController.view.layer.mask = fromViewmask + + + let dimmingView = DimmingView(frame: fromViewController.view.frame) + transitionContext.containerView.insertSubview(dimmingView, belowSubview: toViewController.view) + + fromViewController.view.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) + + UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseOut], animations: { + dimmingView.alpha = 0.7 +// fromViewController.view.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + toViewController.view.frame.origin.y = BaseDimensions.topHeight + 22 + }) { (_) in +// fromViewController.view.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + } + } else { + //Dismiss + + let dimmingView = transitionContext.containerView.subviews.first(where: { (view) -> Bool in + return view is DimmingView + }) + +// fromViewController.view.frame.origin.y = BaseDimensions.topHeight + 22 //or use finalFrame + + let fromViewSnpaShot = fromViewController.view.snapshotView(afterScreenUpdates: false) + + if let fromViewSnpaShot = fromViewSnpaShot { + fromViewController.view.isHidden = true + fromViewSnpaShot.frame = fromViewController.view.frame + transitionContext.containerView.addSubview(fromViewSnpaShot) + } + + dimmingView?.alpha = 0.7 +// toViewController.view.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + + + UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveLinear], animations: { + dimmingView?.alpha = 0 + fromViewSnpaShot?.frame.origin.y = UIScreen.main.bounds.size.height +// toViewController.view.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) + }) { (_) in + if (!transitionContext.transitionWasCancelled) { + toViewController.view.transform = .identity + dimmingView?.removeFromSuperview() + toViewController.view.layer.mask = nil + } + fromViewSnpaShot?.removeFromSuperview() + fromViewController.view.isHidden = false + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + } + } + + } + +} diff --git a/IndieMusic/IndieMusic/Common/PullToDismissTransition.swift b/IndieMusic/IndieMusic/Common/PullToDismissTransition.swift index 6a19254..2798771 100644 --- a/IndieMusic/IndieMusic/Common/PullToDismissTransition.swift +++ b/IndieMusic/IndieMusic/Common/PullToDismissTransition.swift @@ -407,8 +407,8 @@ extension PullToDismissTransition: UIViewControllerAnimatedTransitioning { let completeBlock: (Bool) -> Void = { [weak self] finished -> Void in if finished { - self?.dimmingView?.removeFromSuperview() - self?.dimmingView = nil +// self?.dimmingView?.removeFromSuperview() +// self?.dimmingView = nil } completionHandler?() diff --git a/IndieMusic/IndieMusic/Common/ViewController.swift b/IndieMusic/IndieMusic/Common/ViewController.swift index b1122f7..ce68e22 100644 --- a/IndieMusic/IndieMusic/Common/ViewController.swift +++ b/IndieMusic/IndieMusic/Common/ViewController.swift @@ -202,9 +202,9 @@ class ViewController: UIViewController, Navigatable { self.navigationItem.leftBarButtonItem = nil } - if let tabbar = self.tabBarController as? HomeTabBarController, self.navigationController?.viewControllers.count == 1 { - tabbar.showAllBar(true, animated: true) - } +// if let tabbar = self.tabBarController as? HomeTabBarController, self.navigationController?.viewControllers.count == 1 { +// tabbar.showAllBar(true, animated: true) +// } } @objc func closeAction(sender: AnyObject) { diff --git a/IndieMusic/IndieMusic/Managers/AudioManager.swift b/IndieMusic/IndieMusic/Managers/AudioManager.swift index 665bc9e..73e42f9 100644 --- a/IndieMusic/IndieMusic/Managers/AudioManager.swift +++ b/IndieMusic/IndieMusic/Managers/AudioManager.swift @@ -8,6 +8,11 @@ import Foundation import AVFoundation import MediaPlayer +import Kingfisher +import RxSwift +import RxCocoa +import RxViewController + class AudioManager { @@ -31,15 +36,9 @@ class AudioManager { } - commandCenter.playCommand.addTarget { [weak self] event in - // 处理播放逻辑 - return .success - } - - commandCenter.pauseCommand.addTarget { [weak self] event in - // 处理暂停逻辑 - return .success - } + setupRemoteTransportControls() + + } @objc func playerDidFinishPlaying(note: NSNotification) { @@ -64,7 +63,9 @@ class AudioManager { var isSeekInProgress = false var chasingTime = CMTime.zero - + var isPlayingState = BehaviorSubject(value: false) + var disposeBag = DisposeBag() + func setPlaylist(list: [AudioTrack]) { self.playlist = list @@ -90,17 +91,22 @@ class AudioManager { self.currentTrack = track NotificationCenter.default.post(name: .notiPlayAudioTrack, object: track) - setLockScreenDisplay(track: track) +// updateLockScreenDisplay(track: track) + + player?.rx.observe(AVPlayer.TimeControlStatus.self, "timeControlStatus") + .map { $0 == .playing } + .bind(to: isPlayingState) + .disposed(by: disposeBag) + + + playerItem.rx.observe(AVPlayerItem.Status.self, "status") + .filter { $0 == .readyToPlay } + .subscribe(onNext: { [weak self] _ in + self?.updateLockScreenDisplay(track: track) + }) + .disposed(by: disposeBag) + -// var nowPlayingInfo = [String: Any]() -// nowPlayingInfo[MPMediaItemPropertyTitle] = "歌曲标题" -// nowPlayingInfo[MPMediaItemPropertyArtist] = "艺术家名称" -// nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = 100 -// nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = 200 -// nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1 -// -// MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo -// } catch { @@ -158,6 +164,9 @@ class AudioManager { public func pause() { if let player = player { player.pause() +// if let currentTrack = currentTrack { +// updateLockScreenDisplay(track: currentTrack) +// } NotificationCenter.default.post(name: .notiPlayPause, object: nil) } } @@ -165,6 +174,9 @@ class AudioManager { public func resume() { if let player = player { player.play() +// if let currentTrack = currentTrack { +// updateLockScreenDisplay(track: currentTrack) +// } NotificationCenter.default.post(name: .notiPlayResume, object: nil) } } @@ -185,6 +197,9 @@ class AudioManager { } else { self.trySeekToChaseTime() } + if let currentTrack = self.currentTrack { + self.updateLockScreenDisplay(track: currentTrack) + } } } @@ -203,16 +218,101 @@ class AudioManager { } - func setLockScreenDisplay(track: AudioTrack) { - var info = Dictionary() - info[MPMediaItemPropertyTitle] = track.title//歌名 - info[MPMediaItemPropertyArtist] = track.artist//作者 - // [info setObject:self.model.filename forKey:MPMediaItemPropertyAlbumTitle];//专辑名 - info[MPMediaItemPropertyAlbumArtist] = track.artist//专辑作者 -// info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: UIImage.init(named: track.pic!))//显示的图片 - info[MPMediaItemPropertyPlaybackDuration] = 200 - info[MPNowPlayingInfoPropertyPlaybackRate] = 1.0//播放速率 - MPNowPlayingInfoCenter.default().nowPlayingInfo = info - } + func updateLockScreenDisplay(track: AudioTrack) { + guard let player = player, let currentItem = player.currentItem, currentItem.status == .readyToPlay else { return } + + var info = [String: Any]() + + // 基础信息 + info[MPMediaItemPropertyTitle] = track.title + info[MPMediaItemPropertyArtist] = track.artist + // 添加更多信息... + + // 计算并更新播放信息 + let duration = CMTimeGetSeconds(currentItem.duration) + let currentTime = CMTimeGetSeconds(player.currentTime()) + info[MPMediaItemPropertyPlaybackDuration] = duration + info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime + info[MPNowPlayingInfoPropertyPlaybackRate] = player.rate + + // 更新封面 + updateAlbumArtwork(urlString: track.pic) { artwork in + info[MPMediaItemPropertyArtwork] = artwork + DispatchQueue.main.async { + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + } + } + } + + func updateAlbumArtwork(urlString: String?, completion: @escaping (MPMediaItemArtwork) -> Void) { + guard let urlString = urlString, let url = URL(string: urlString) else { return } + + KingfisherManager.shared.retrieveImage(with: url) { result in + switch result { + case .success(let value): + let image = value.image + let artwork = MPMediaItemArtwork(boundsSize: image.size) { _ in return image } + DispatchQueue.main.async { + completion(artwork) + } + case .failure(let error): + print("Error fetching image: \(error)") + // 处理错误情况 + } + } + } + + func setupRemoteTransportControls() { + + commandCenter.playCommand.isEnabled = true + commandCenter.playCommand.addTarget { [unowned self] event in + if let currentTrack = currentTrack, !self.isPlaying() { + self.player?.play() +// updateLockScreenDisplay(track: currentTrack) + return .success + } + return .commandFailed + } + + commandCenter.pauseCommand.isEnabled = true + commandCenter.pauseCommand.addTarget { [unowned self] event in + if let currentTrack = currentTrack, self.isPlaying() { + self.player?.pause() +// updateLockScreenDisplay(track: currentTrack) + return .success + } + return .commandFailed + } + + commandCenter.changePlaybackPositionCommand.addTarget { event in + + guard let event = event as? MPChangePlaybackPositionCommandEvent else { + return .commandFailed + } + + let time = CMTime.init(seconds: event.positionTime, preferredTimescale: 1) + + self.seekActually(time: time) + + + // 更新锁屏信息 + return .success + } + + + commandCenter.nextTrackCommand.addTarget { event in + + self.nextTrack() + return .success + } + + + commandCenter.previousTrackCommand.addTarget { event in + + self.previousTrack() + return .success + } + + } } diff --git a/IndieMusic/IndieMusic/Models/Feedback.swift b/IndieMusic/IndieMusic/Models/Feedback.swift index 229e124..9afb52c 100644 --- a/IndieMusic/IndieMusic/Models/Feedback.swift +++ b/IndieMusic/IndieMusic/Models/Feedback.swift @@ -16,7 +16,7 @@ import RxDataSources enum FeedbackPhotoType { case add - case photo(String) + case photo(UIImage) } struct FeedbackSection { diff --git a/IndieMusic/IndieMusic/Modules/Home/HomeTabBarController.swift b/IndieMusic/IndieMusic/Modules/Home/HomeTabBarController.swift index c1f414f..15bf67e 100644 --- a/IndieMusic/IndieMusic/Modules/Home/HomeTabBarController.swift +++ b/IndieMusic/IndieMusic/Modules/Home/HomeTabBarController.swift @@ -21,7 +21,8 @@ enum HomeTabBarItem: Int { return NavigationController(rootViewController: vc) case .mine: let vc = MineViewController(viewModel: viewModel, navigator: navigator) - return NavigationController(rootViewController: vc) + let nav = NavigationController(rootViewController: vc) + return nav } } @@ -109,33 +110,21 @@ class HomeTabBarController: UITabBarController, Navigatable { override func viewDidLoad() { super.viewDidLoad() - // 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 - +// customeTabBar.delegate = self } func makeUI() { tabBar.isHidden = true + tabBar.backgroundColor = .blue + view.addSubview(playerTabBar) view.addSubview(customeTabBar) - customeTabBar.snp.makeConstraints { make in make.left.equalTo(view) @@ -149,12 +138,15 @@ class HomeTabBarController: UITabBarController, Navigatable { make.right.equalTo(view) make.bottom.equalTo(customeTabBar.snp.top).offset(BaseDimensions.tabBarHeight) } + } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - +// self.tabBar.isHidden = true + + self.delegate = self } func bindViewModel() { @@ -169,8 +161,9 @@ class HomeTabBarController: UITabBarController, Navigatable { output.tabBarItems.delay(.milliseconds(50)).drive(onNext: { [weak self] (tabBarItems) in if let strongSelf = self { let controllers = tabBarItems.map { $0.getController(with: viewModel.viewModel(for: $0), navigator: strongSelf.navigator) } -// strongSelf.showAllBar(true, animated: false) strongSelf.setViewControllers(controllers, animated: false) + self?.selectedIndex = 0 + } }).disposed(by: rx.disposeBag) @@ -235,24 +228,66 @@ class HomeTabBarController: UITabBarController, Navigatable { var tabItems = [UITabBarItem]() for (index, viewController) in viewControllers.enumerated() { var tabItem: UITabBarItem = viewController.tabBarItem - if tabItem.title == nil && tabItem.image == nil && tabItem.selectedImage == nil { - tabItem = UITabBarItem(title: "", image: nil, tag: index) + tabItem = UITabBarItem(title: nil, image: nil, tag: index) } tabItems.append(tabItem) } customeTabBar.setItems(tabItems, animated: false) - customeTabBar.selectedItem = customeTabBar.items?.first + customeTabBar.selectedItem = customeTabBar.items?[self.selectedIndex] +// tabBar.frame = CGRect.zero +// tabBar.isHidden = true + + } + + func refreshTabBarItems() { + guard let viewModel = self.viewModel, let navigator = self.navigator else { return } + + + let tabBarItems: [HomeTabBarItem] = [.home, .search, .mine] + let viewControllers = tabBarItems.map { item -> UIViewController in + return item.getController(with: viewModel.viewModel(for: item), navigator: navigator) + } + + + + + self.setViewControllers(viewControllers, animated: false) + + tabBar.isHidden = true + tabBar.frame = CGRect.zero + } + + override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { + super.tabBar(tabBar, didSelect: item) + + + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + } + func showAllBar(_ isShowing: Bool, animated: Bool) { + if isAnimating { return } isAnimating = true + + if isShowing { customeTabBar.isHidden = false @@ -321,17 +356,6 @@ class HomeTabBarController: UITabBarController, Navigatable { self.isAnimating = false } - - -// UIView.animate(withDuration: animationDuration, -// delay: 0, -// usingSpringWithDamping: 0.7, -// initialSpringVelocity: 0, -// options: .curveEaseInOut, -// animations: { -// -// }) { (completed) in -// } } open func showTabBar(_ isShowing: Bool, animated: Bool) { @@ -435,59 +459,14 @@ class HomeTabBarController: UITabBarController, Navigatable { } - @objc func playerTabBarClick() { - - -// let player = PlayerViewController.init(viewModel: playerViewModel, navigator: self.navigator) -// player.modalPresentationStyle = .custom -// player.isPullToDismissEnabled = true -// player.modalPresentationCapturesStatusBarAppearance = true -// -// self.present(player, animated: true) - - -// let player = TestViewController.init() -// player.modalPresentationStyle = .custom -// player.isPullToDismissEnabled = true -// player.modalPresentationCapturesStatusBarAppearance = true +} -//// -// let nav = NavigationController.init(rootViewController: player) -// -// nav.modalPresentationStyle = .custom -// nav.isPullToDismissEnabled = true -// nav.modalPresentationCapturesStatusBarAppearance = true -// -// self.present(nav, animated: true) +extension HomeTabBarController: UITabBarControllerDelegate { - - - - + func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { + return viewController != tabBarController.selectedViewController } + } - - -//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 -// } -// -//} -// -// diff --git a/IndieMusic/IndieMusic/Modules/Home/HomeTabBarViewModel.swift b/IndieMusic/IndieMusic/Modules/Home/HomeTabBarViewModel.swift index f8fa2b6..b78ae3b 100644 --- a/IndieMusic/IndieMusic/Modules/Home/HomeTabBarViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Home/HomeTabBarViewModel.swift @@ -58,11 +58,16 @@ class HomeTabBarViewModel: ViewModel, ViewModelType { checkVersion() input.notiPlayAudioTrack.subscribe { [weak self] noti in + guard let track = noti.element?.object as? AudioTrack else { return } + AudioManager.sharedInstance.player?.addProgressObserver { currentTime, duration, progress in self?.progress.accept(Float(progress)) self?.duration.accept(Float(duration)) self?.time.accept(Float(currentTime)) print("viewmodel addProgressObserver : \(progress)") + +// AudioManager.sharedInstance.updateNowPlayingInfo(track: track, progress: progress, duration: duration) + } }.disposed(by: rx.disposeBag) diff --git a/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift b/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift index 5fc8569..eee6e15 100644 --- a/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Home/HomeViewController.swift @@ -45,7 +45,7 @@ class HomeViewController: TableViewController { self.navigationItem.leftBarButtonItem = nil - if let tabbar = self.tabBarController as? HomeTabBarController { + if let tabbar = self.tabBarController as? HomeTabBarController, tabbar.customeTabBar.isHidden == true { tabbar.showAllBar(true, animated: true) } @@ -53,6 +53,13 @@ class HomeViewController: TableViewController { navigationBar.passthroughView = homePagerView } + if AudioManager.sharedInstance.playlist?.count ?? 0 > 0 { + tableView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight + 66, right: 0) + } else { + tableView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight, right: 0) + } + + } override func viewWillDisappear(_ animated: Bool) { @@ -61,6 +68,7 @@ class HomeViewController: TableViewController { if let navigationBar = self.navigationController?.navigationBar as? NavigationBar { navigationBar.passthroughView = nil } + } override func makeUI() { @@ -76,6 +84,11 @@ class HomeViewController: TableViewController { tableView.tableHeaderView = headerView tableView.separatorColor = .clear tableView.register(HomeViewCell.self, forCellReuseIdentifier: "HomeViewCell") + + + + + } @@ -194,12 +207,9 @@ class HomeViewController: TableViewController { make.left.equalTo(view) make.right.equalTo(view) make.top.equalTo(view).offset(-BaseDimensions.statusBarHeight) - - if AudioManager.sharedInstance.playlist?.count ?? 0 > 0 { - make.bottom.equalTo(view).offset(-66) - } else { - make.bottom.equalTo(view) - } + make.bottom.equalTo(view) + + } } diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift index f1b6ae1..918cae3 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewController.swift @@ -11,55 +11,203 @@ import RxCocoa import RxDataSources class CommentDetailViewController: TableViewController { - + let commentDetailHeaderView: CommentDetailHeaderView = { + let commentDetailHeaderView = CommentDetailHeaderView.init() + + return commentDetailHeaderView + }() + + var commentToolView: CommentToolView = { + let commentToolView = CommentToolView.init(isShowButton: false, frame: CGRect.zero) + + return commentToolView + }() override func viewDidLoad() { super.viewDidLoad() - tableView.register(CommentViewCell.self, forCellReuseIdentifier: "CommentViewCell") } - +// +// override func viewWillAppear(_ animated: Bool) { +// super.viewWillAppear(animated) +// +// self.navigationController?.setNavigationBarHidden(true, animated: false) +// } +// +// +// override func viewWillDisappear(_ animated: Bool) { +// super.viewWillDisappear(animated) +// self.navigationController?.setNavigationBarHidden(false, animated: false) +// } override func makeUI() { super.makeUI() + view.backgroundColor = .clear view.addSubview(tableView) + self.navigationController?.view.backgroundColor = UIColor.clear +// UIColor.black.withAlphaComponent(0.5) + + tableView.register(CommentViewCell.self, forCellReuseIdentifier: "CommentViewCell") + tableView.tableHeaderView = commentDetailHeaderView + tableView.separatorColor = .clear + tableView.backgroundColor = .white + tableView.layer.cornerRadius = 8 + tableView.layer.masksToBounds = true + tableView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + + view.addSubview(commentToolView) } override func bindViewModel() { super.bindViewModel() - guard let viewModel = viewModel as? CommentViewModel else { return } + guard let viewModel = viewModel as? CommentDetailViewModel else { return } -// let input = CommentViewModel.Input.init(viewWillAppear: rx.viewWillAppear, -// selection: tableView.rx.itemSelected.asDriver(), -// currentCommentListType: current) -// let output = viewModel.transform(input: input) -// -// let dataSource = CommentDetailViewController.dataSource() -// -// output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) -// -// -// -// -// output.itemSelected.subscribe { [weak self] sectionItem in -// -// }.disposed(by: rx.disposeBag) -// - + let input = CommentDetailViewModel.Input.init(viewWillAppear: rx.viewWillAppear, + modelSelected: tableView.rx.modelSelected(CommentType.self).asDriver(), + footerRefresh: footerRefreshTrigger, + commentText: commentToolView.textField.rx.text.asDriver(), + sendCommentTrigger: commentToolView.textField.rx.controlEvent(.editingDidEndOnExit).asDriver()) + + let output = viewModel.transform(input: input) + + let dataSource = CommentDetailViewController.dataSource() + + output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) + + + + + output.modelSelected.drive { [weak self] sectionItem in + + }.disposed(by: rx.disposeBag) + + output.comment.drive { comment in + self.commentDetailHeaderView.comment = comment + + + self.updateHeaderViewFrame() + }.disposed(by: rx.disposeBag) + + // 监听键盘弹出事件 + NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) + .subscribe(onNext: { notification in + if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { + let keyboardHeight = keyboardSize.height + + self.commentToolView.snp.remakeConstraints { make in + make.left.equalTo(self.view) + make.right.equalTo(self.view) + make.bottom.equalTo(self.view).offset(-keyboardHeight) + make.height.equalTo(BaseDimensions.bottomHeight + 48) + + } + + UIView.animate(withDuration: 0.25) { + self.view.layoutIfNeeded() + } + + } + }) + .disposed(by: rx.disposeBag) + + // 监听键盘隐藏事件 + NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification) + .subscribe(onNext: { notification in + + self.commentToolView.snp.remakeConstraints { make in + make.left.equalTo(self.view) + make.right.equalTo(self.view) + make.bottom.equalTo(self.view) + make.height.equalTo(BaseDimensions.bottomHeight + 48) + } + + UIView.animate(withDuration: 0.25) { + self.view.layoutIfNeeded() + } + + }) + .disposed(by: rx.disposeBag) + + + + let tapGesture = UITapGestureRecognizer() + tableView.addGestureRecognizer(tapGesture) + + tapGesture.rx.event + .bind(onNext: { [weak self] _ in + self?.view.endEditing(true) + + self?.navigator.dismiss(sender: self) + + }) + .disposed(by: rx.disposeBag) + + + tableView.mj_header = nil + + } + + func updateHeaderViewFrame() { + guard let headerView = tableView.tableHeaderView else { + return + } + + // 设置headerView的宽度和布局 + headerView.layoutIfNeeded() + let height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height + var frame = headerView.frame + frame.size.height = height + headerView.frame = frame + + // 重要: 更新tableView的tableHeaderView以应用新的frame + tableView.tableHeaderView = headerView } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() + + commentToolView.snp.makeConstraints { make in + make.left.equalTo(view) + make.right.equalTo(view) + make.bottom.equalTo(view) + make.height.equalTo(BaseDimensions.bottomHeight + 48) + } + + tableView.snp.remakeConstraints { make in + make.left.equalTo(view) + make.right.equalTo(view) + make.top.equalTo(view) + make.bottom.equalTo(view).offset(-BaseDimensions.bottomHeight - 48) + } + } } +extension CommentDetailViewController { + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 60 + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let header = CommentDetailSectionView.init() + header.titleLabel.text = "全部回复" + + return header + } + +} + + extension CommentDetailViewController { static func dataSource() -> RxTableViewSectionedReloadDataSource { return RxTableViewSectionedReloadDataSource( @@ -69,23 +217,17 @@ extension CommentDetailViewController { case .comment(let comment): let cell: CommentViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentViewCell", for: indexPath) as! CommentViewCell -// cell.setting = item.setting - + cell.comment = comment return cell case .quote(let commentQuote): let cell: CommentQuoteViewCell = tableView.dequeueReusableCell(withIdentifier: "CommentQuoteViewCell", for: indexPath) as! CommentQuoteViewCell - -// cell.setting = item.setting + cell.comment = commentQuote return cell } - - - - } - ) + }) } } @@ -94,7 +236,10 @@ extension CommentDetailViewController { class CommentDetailHeaderView: UIView { let avatarView: UIImageView = { let avatarView = UIImageView.init() - + avatarView.layer.cornerRadius = 20 + avatarView.layer.masksToBounds = true + avatarView.backgroundColor = .init(hex: 0xf5f5f5) + return avatarView }() @@ -107,6 +252,7 @@ class CommentDetailHeaderView: UIView { let commentLabel: UILabel = { let commentLabel = UILabel.init() + commentLabel.numberOfLines = 0 commentLabel.textColor = .primaryText() commentLabel.font = UIFont.systemFont(ofSize: 14) @@ -121,6 +267,18 @@ class CommentDetailHeaderView: UIView { }() + var comment: Comment? { + didSet { + guard let comment = comment else { return } + avatarView.kf.setImage(with: URL.init(string: comment.avatar ?? "")) + + nameLabel.text = comment.nickName + + commentLabel.text = comment.content + + } + } + override init(frame: CGRect) { super.init(frame: frame) @@ -142,7 +300,9 @@ class CommentDetailHeaderView: UIView { avatarView.snp.makeConstraints { make in make.left.equalTo(self).offset(18) make.size.equalTo(CGSize.init(width: 40, height: 40)) - make.centerY.equalTo(self) +// make.centerY.equalTo(self) + make.top.equalTo(self).offset(24) +// make.bottom.equalTo(self).offset(-21) } nameLabel.snp.makeConstraints { make in @@ -167,3 +327,35 @@ class CommentDetailHeaderView: UIView { } } + + +class CommentDetailSectionView: UIView { + var titleLabel: UILabel = { + let titleLabel = UILabel.init() + titleLabel.font = UIFont.systemFont(ofSize: 17) + + return titleLabel + }() + + + override init(frame: CGRect) { + super.init(frame: frame) + + makeUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func makeUI() { + addSubview(titleLabel) + + titleLabel.snp.makeConstraints { make in + make.top.equalTo(self).offset(18) + make.left.equalTo(self).offset(20) + } + } + + +} diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift index 0a411c9..6d6ec00 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentDetailViewModel.swift @@ -8,59 +8,138 @@ import Foundation import RxSwift import RxCocoa - +import SVProgressHUD class CommentDetailViewModel: ViewModel, ViewModelType { struct Input { let viewWillAppear: ControlEvent - let selection: Driver + let modelSelected: Driver + let footerRefresh: Observable + let commentText: Driver + let sendCommentTrigger: Driver + } struct Output { + let comment: Driver let items: BehaviorRelay<[CommentSection]> - let itemSelected: PublishSubject + let modelSelected: Driver } let items = BehaviorRelay<[CommentSection]>.init(value: []) let itemSelected = PublishSubject() - let commentID: String? + let comment: Comment - init(commentID: String, provider: IndieMusicAPI) { - self.commentID = commentID + init(comment: Comment, provider: IndieMusicAPI) { + self.comment = comment super.init(provider: provider) } func transform(input: Input) -> Output { + input.viewWillAppear.subscribe { _ in + self.requestSubCommentData(parentId: self.comment.id, page: self.page, size: 10) + .subscribe { commentArray in + let newArray = commentArray.map { comment in + return CommentType.comment(comment) + } + + self.items.accept([CommentSection.init(items: newArray)]) + + } onError: { error in + + }.disposed(by: self.rx.disposeBag) + + }.disposed(by: rx.disposeBag) -// let commentQuote = CommentQuote.init(avatar: "", name: "1233", comment: "dfdsadsdads", haveMore: false) - - - -// let comment = Comment.init(avatar: "", name: "nike", date: 10000, isLike: false, comment: "test", commentQuotes: [commentQuote, commentQuote]) -// -// -// let journalSection = CommentSection.init(items: [.comment(comment)]) -// -// items.accept([journalSection]) + input.footerRefresh.flatMapLatest({ [weak self] () -> Observable<[Comment]> in + guard let self = self else { return Observable.just([]) } + self.page += 1 + return self.requestSubCommentData(parentId: self.comment.id, page: self.page, size: 10) + .trackActivity(self.footerLoading) + }) + .subscribe(onNext: { (commentArray) in + let newArray = commentArray.map { comment in + return CommentType.comment(comment) + } + + + guard let new = self.items.value.first else { return } + + self.items.accept([CommentSection.init(items: new.items + newArray)]) + + }).disposed(by: rx.disposeBag) + + - input.selection.drive { indexPath in - guard let sectionItem = self.items.value.first?.items[indexPath.row] else { return } + + input.modelSelected.drive { commentType in -// self.itemSelected.onNext(sectionItem) }.disposed(by: rx.disposeBag) + + let sendComment = input.sendCommentTrigger + .withLatestFrom(input.commentText) + .drive(onNext: { comment in + if let comment = comment, !comment.isEmpty { + + self.sendCommentData(content: comment, journalId: self.comment.journalId, parentId: self.comment.id, journalImage: self.comment.journalImage) + .subscribe { comment in + + //TODO 更新cell的点赞状态 + SVProgressHUD.showText(withStatus: "发送成功") + } onError: { error in + + }.disposed(by: self.rx.disposeBag) + } else { + print("评论为空,不发送") + } + }) + + + - return Output.init(items: items, - itemSelected: itemSelected) + return Output.init(comment: Driver.just(comment), + items: items, + modelSelected: input.modelSelected) + } + + + func requestSubCommentData(parentId: String, page: Int, size: Int) -> Observable<[Comment]> { + return self.provider.subCommentList(parentId: parentId, page: page, size: size) + .trackActivity(loading) + .trackError(error) + + } + + + + func requestCommentLike(commentID: String) -> Observable { + return self.provider.commentLike(commentId: commentID) + .trackError(error) + } + + func insertComment(in index: Int, with newCommentType: CommentType) { + var firstSection = items.value.first + firstSection?.items.insert(newCommentType, at: index + 1) + if let updatedSection = firstSection { + items.accept([updatedSection]) + } } + + func sendCommentData(content: String, journalId: String?, parentId: String?, journalImage: String?) -> Observable { + return self.provider.sendComment(content: content, journalId: journalId, parentId: parentId, journalImage: journalImage) + .trackActivity(loading) + .trackError(error) + + } } diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift index 53c964a..ebaf1c1 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentView.swift @@ -138,6 +138,16 @@ class CommentViewCell: UITableViewCell { return commentLabel }() + let moreButton: UIButton = { + let moreButton = UIButton.init() + moreButton.setTitle("更多回复", for: .normal) + moreButton.titleLabel?.font = UIFont.systemFont(ofSize: 14) + moreButton.setTitleColor(.primary(), for: .normal) + moreButton.isHidden = true + + return moreButton + }() + var comment: Comment? { didSet { guard let comment = comment else { return } @@ -154,6 +164,31 @@ class CommentViewCell: UITableViewCell { likeButton.isSelected = comment.haveThumbup == true ? true : false + + + if let commentCount = comment.commentCount, commentCount > 0 { + moreButton.setTitle("展开\(commentCount)条回复", for: .normal) + moreButton.isHidden = false + commentLabel.snp.remakeConstraints { make in + make.left.equalTo(avatarView.snp.right).offset(14) + make.right.equalTo(contentView).offset(-18) + make.top.equalTo(dateLabel.snp.bottom).offset(10) + } + + moreButton.snp.remakeConstraints { make in + make.left.equalTo(avatarView.snp.right).offset(14) + make.top.equalTo(commentLabel.snp.bottom).offset(12) + make.bottom.equalTo(contentView).offset(-12) + } + } else { + moreButton.isHidden = true + commentLabel.snp.remakeConstraints { make in + make.left.equalTo(avatarView.snp.right).offset(14) + make.top.equalTo(dateLabel.snp.bottom).offset(10) + make.bottom.equalTo(contentView).offset(-12) + } + } + } } @@ -189,6 +224,7 @@ class CommentViewCell: UITableViewCell { contentView.addSubview(dateLabel) contentView.addSubview(likeButton) contentView.addSubview(commentLabel) + contentView.addSubview(moreButton) avatarView.snp.makeConstraints { make in make.left.equalTo(contentView).offset(18) @@ -219,6 +255,7 @@ class CommentViewCell: UITableViewCell { make.bottom.equalTo(contentView).offset(-12) } + } diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift index 4f42caa..58096bc 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewController.swift @@ -24,6 +24,7 @@ class CommentViewController: ViewController { tableView.separatorColor = .clear tableView.register(CommentViewCell.self, forCellReuseIdentifier: "CommentViewCell") tableView.register(CommentQuoteViewCell.self, forCellReuseIdentifier: "CommentQuoteViewCell") + tableView.keyboardDismissMode = .onDrag return tableView }() @@ -64,6 +65,16 @@ class CommentViewController: ViewController { } } + + let tapGesture = UITapGestureRecognizer() + tableView.addGestureRecognizer(tapGesture) + + tapGesture.rx.event + .bind(onNext: { [weak self] _ in + self?.view.endEditing(true) + }) + .disposed(by: rx.disposeBag) + } @@ -85,7 +96,12 @@ class CommentViewController: ViewController { selection: tableView.rx.itemSelected.asDriver(), currentCommentListType: currentCommentListType, commentText: commentToolView.textField.rx.text.asDriver(), - sendCommentTrigger: commentToolView.textField.rx.controlEvent(.editingDidEndOnExit).asDriver()) + sendCommentTrigger: commentToolView.textField.rx.controlEvent(.editingDidEndOnExit).asDriver(), + keyboardWillShowNotification: NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification), + keyboardWillHideNotification: NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification) + + + ) let output = viewModel.transform(input: input) let dataSource = RxTableViewSectionedReloadDataSource( @@ -112,6 +128,15 @@ class CommentViewController: ViewController { viewModel.likeSelected.onNext(comment.id) }.disposed(by: self.rx.disposeBag) + cell.moreButton.rx.tap.subscribe { _ in + + let commentDetailViewModel = CommentDetailViewModel.init(comment: comment, provider: viewModel.provider) + + self.navigator.show(segue: .commentDetail(viewModel:commentDetailViewModel), sender: self, transition: .modal) + + }.disposed(by: self.rx.disposeBag) + + cell.rx.longPressGesture().when(.recognized) .subscribe { [weak self] tap in @@ -144,8 +169,8 @@ class CommentViewController: ViewController { } cell.moreClousres = {[weak self] commentID in - let commentDetailViewModel = CommentDetailViewModel.init(commentID: commentID, provider: viewModel.provider) - self?.navigator.show(segue: .commentDetail(viewModel: commentDetailViewModel), sender: self) +// let commentDetailViewModel = CommentDetailViewModel.init(commentID: commentID, provider: viewModel.provider) +// self?.navigator.show(segue: .commentDetail(viewModel: commentDetailViewModel), sender: self) } return cell @@ -163,6 +188,53 @@ class CommentViewController: ViewController { }.disposed(by: rx.disposeBag) + + // 监听键盘弹出事件 + NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) + .subscribe(onNext: { notification in + if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { + let keyboardHeight = keyboardSize.height + + self.commentToolView.snp.remakeConstraints { make in + make.left.equalTo(self.view) + make.right.equalTo(self.view) + make.bottom.equalTo(self.view).offset(-keyboardHeight) + make.height.equalTo(BaseDimensions.bottomHeight + 48) + + } + + UIView.animate(withDuration: 0.25) { + self.view.layoutIfNeeded() + } + + } + }) + .disposed(by: rx.disposeBag) + + // 监听键盘隐藏事件 + NotificationCenter.default.rx.notification(UIResponder.keyboardWillHideNotification) + .subscribe(onNext: { notification in + + self.commentToolView.snp.remakeConstraints { make in + make.left.equalTo(self.view) + make.right.equalTo(self.view) + make.bottom.equalTo(self.view) + make.height.equalTo(BaseDimensions.bottomHeight + 48) + } + + UIView.animate(withDuration: 0.25) { + self.view.layoutIfNeeded() + } + + }) + .disposed(by: rx.disposeBag) + + + + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + } override func viewDidLayoutSubviews() { @@ -186,7 +258,7 @@ class CommentViewController: ViewController { make.left.equalTo(view) make.right.equalTo(view) make.top.equalTo(commentHeaderView.snp.bottom) - make.bottom.equalTo(commentToolView.snp.top) + make.bottom.equalTo(view).offset(-BaseDimensions.bottomHeight - 48) } diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift index d3b2f52..f542212 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/CommentViewModel.swift @@ -18,6 +18,10 @@ class CommentViewModel: ViewModel, ViewModelType { let currentCommentListType: BehaviorRelay let commentText: Driver let sendCommentTrigger: Driver + + let keyboardWillShowNotification: Observable + let keyboardWillHideNotification: Observable + } struct Output { @@ -26,6 +30,8 @@ class CommentViewModel: ViewModel, ViewModelType { let itemSelected: PublishSubject let insertCommment: PublishSubject + let toCommentDetail: PublishRelay + } @@ -39,18 +45,21 @@ class CommentViewModel: ViewModel, ViewModelType { let insertCommment: PublishSubject = .init() - var journalID: String + let toCommentDetail: PublishRelay = .init() + + + var journal: Journal var subPage: Int = 1 - init(journalID: String, provider: IndieMusicAPI) { - self.journalID = journalID + init(journal: Journal, provider: IndieMusicAPI) { + self.journal = journal super.init(provider: provider) } func transform(input: Input) -> Output { input.viewWillAppear.subscribe { _ in - self.requestHotCommentListData(journalID: self.journalID, page: self.page, size: 10) + self.requestHotCommentListData(journalID: self.journal.id, page: self.page, size: 10) .subscribe { [weak self] commentArray in guard let self = self else { return } @@ -67,7 +76,7 @@ class CommentViewModel: ViewModel, ViewModelType { switch commentListType { case .hot: - self.requestHotCommentListData(journalID: self.journalID, page: self.page, size: 10) + self.requestHotCommentListData(journalID: self.journal.id, page: self.page, size: 10) .subscribe { [weak self] comments in guard let self = self else { return } @@ -80,7 +89,7 @@ class CommentViewModel: ViewModel, ViewModelType { case .latest: - self.requestLatestCommentListData(journalID: self.journalID, page: self.page, size: 10) + self.requestLatestCommentListData(journalID: self.journal.id, page: self.page, size: 10) .subscribe { [weak self] comments in guard let self = self else { return } @@ -135,11 +144,11 @@ class CommentViewModel: ViewModel, ViewModelType { let sendComment = input.sendCommentTrigger - .withLatestFrom(input.commentText) // 使用 commentText 的最新值 + .withLatestFrom(input.commentText) .drive(onNext: { comment in if let comment = comment, !comment.isEmpty { - self.sendCommentData(content: comment, journalId: self.journalID, parentId: nil, journalImage: nil) + self.sendCommentData(content: comment, journalId: self.journal.id, parentId: nil, journalImage: self.journal.image) .subscribe { comment in //TODO 更新cell的点赞状态 @@ -159,7 +168,8 @@ class CommentViewModel: ViewModel, ViewModelType { return Output.init(items: items, itemSelected: itemSelected, - insertCommment: insertCommment) + insertCommment: insertCommment, + toCommentDetail: toCommentDetail) } private func handleReceivedComments(comments: [Comment]) { @@ -167,18 +177,18 @@ class CommentViewModel: ViewModel, ViewModelType { var commentSections = [CommentSection(items: commentTypes)] self.items.accept(commentSections) - for (index, commentType) in commentTypes.enumerated() { - if case let .comment(comment) = commentType, comment.commentCount != 0 { - self.requestSubCommentData(parentId: comment.id, page: 1, size: 1) - .subscribe { [weak self] subCommentArray in - guard let self = self else { return } - - self.handleReceivedSubComments(parentCommentCount: comment.commentCount ?? 0, subComments: subCommentArray, mainCommentIndex: index) - } onError: { error in - print(error) - }.disposed(by: self.rx.disposeBag) - } - } +// for (index, commentType) in commentTypes.enumerated() { +// if case let .comment(comment) = commentType, comment.commentCount != 0 { +// self.requestSubCommentData(parentId: comment.id, page: 1, size: 1) +// .subscribe { [weak self] subCommentArray in +// guard let self = self else { return } +// +// self.handleReceivedSubComments(parentCommentCount: comment.commentCount ?? 0, subComments: subCommentArray, mainCommentIndex: index) +// } onError: { error in +// print(error) +// }.disposed(by: self.rx.disposeBag) +// } +// } } diff --git a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift index 180c971..6639f6f 100644 --- a/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift +++ b/IndieMusic/IndieMusic/Modules/JournalDetail/JournalDetailController.swift @@ -51,6 +51,7 @@ class JournalDetailController: ViewController, UIScrollViewDelegate { doubleColumnSection.boundarySupplementaryItems = [header] doubleColumnSection.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 18, bottom: 0, trailing: 18) + doubleColumnSection.interGroupSpacing = 24 return doubleColumnSection @@ -360,7 +361,7 @@ class JournalDetailController: ViewController, UIScrollViewDelegate { .subscribe { [weak self] tap in guard let self = self else { return } - let commentViewModel = CommentViewModel.init(journalID: viewModel.journal.id, provider: viewModel.provider) + let commentViewModel = CommentViewModel.init(journal: viewModel.journal, provider: viewModel.provider) self.navigator.show(segue: .comment(viewModel: commentViewModel), sender: self) diff --git a/IndieMusic/IndieMusic/Modules/Login/LoginViewController.swift b/IndieMusic/IndieMusic/Modules/Login/LoginViewController.swift index f69ba2a..03d880c 100644 --- a/IndieMusic/IndieMusic/Modules/Login/LoginViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Login/LoginViewController.swift @@ -78,9 +78,7 @@ class LoginViewController: ViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if let tabbar = self.tabBarController as? HomeTabBarController { - tabbar.showAllBar(false, animated: true) - } + } override func makeUI() { @@ -260,4 +258,6 @@ class LoginViewController: ViewController { } + + } diff --git a/IndieMusic/IndieMusic/Modules/Message/MessageViewController.swift b/IndieMusic/IndieMusic/Modules/Message/MessageViewController.swift index c4d2bd1..1ef81cd 100644 --- a/IndieMusic/IndieMusic/Modules/Message/MessageViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Message/MessageViewController.swift @@ -122,7 +122,7 @@ class MessageViewController: ViewController, UIScrollViewDelegate { headerRefresh: refresh, footerRefresh: footerRefreshTrigger, messageType: messageType, - selection: tableView.rx.itemSelected.asDriver()) + modelSelected: tableView.rx.modelSelected(MessageSectionItem.self).asDriver()) @@ -134,14 +134,9 @@ class MessageViewController: ViewController, UIScrollViewDelegate { output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) -// -// output.tableViewItemSelected.subscribe { [weak self] message in -// let commentListViewModel = CommentListViewModel.init(provider: viewModel.provider) -// -// self?.navigator.show(segue: .commentList(viewModel: commentListViewModel), sender: self) -// -// -// }.disposed(by: rx.disposeBag) + output.modelSelected.drive { messageSectionItem in + + }.disposed(by: rx.disposeBag) } @@ -213,6 +208,11 @@ extension MessageViewController { } ) } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + self.navigator.pop(sender: self, toRoot: true) + + } } diff --git a/IndieMusic/IndieMusic/Modules/Message/MessageViewModel.swift b/IndieMusic/IndieMusic/Modules/Message/MessageViewModel.swift index 83535e2..27eb93c 100644 --- a/IndieMusic/IndieMusic/Modules/Message/MessageViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Message/MessageViewModel.swift @@ -17,41 +17,32 @@ class MessageViewModel: ViewModel, ViewModelType { let headerRefresh: Observable let footerRefresh: Observable let messageType: BehaviorRelay - let selection: Driver + let modelSelected: Driver } struct Output { - let messageItems: BehaviorRelay<[MessageSection]> - let activitiesItems: BehaviorRelay<[MessageSection]> + let messageItems: PublishRelay<[MessageSection]> + let activitiesItems: PublishRelay<[MessageSection]> let items: BehaviorRelay<[MessageSection]> let selection: PublishSubject - let itemSelected: PublishSubject + let modelSelected: Driver } - let messageItems = BehaviorRelay<[MessageSection]>.init(value: []) - let activitiesItems = BehaviorRelay<[MessageSection]>.init(value: []) + let messageItems = PublishRelay<[MessageSection]>.init() + let activitiesItems = PublishRelay<[MessageSection]>.init() let items = BehaviorRelay<[MessageSection]>.init(value: []) let selection = PublishSubject() let messageType = BehaviorRelay.init(value: .message) - let itemSelected = PublishSubject() func transform(input: Input) -> Output { - - - input.messageType.subscribe { [weak self] followersType in - guard let self = self else { return } - self.messageType.accept(followersType) - }.disposed(by: rx.disposeBag) - - input.headerRefresh.withLatestFrom(input.messageType) .flatMapLatest({ [weak self] (messageType) -> Observable<[Message]> in guard let self = self else { return Observable.just([]) } @@ -119,6 +110,7 @@ class MessageViewModel: ViewModel, ViewModelType { input.messageType.subscribe { [weak self] messageType in guard let self = self else { return } + self.messageType.accept(messageType) print("messageType \(messageType)") self.page = 1 @@ -176,7 +168,13 @@ class MessageViewModel: ViewModel, ViewModelType { cuttentItems.subscribe { [weak self] followersSections in guard let self = self else { return } - self.items.accept(followersSections) + + if self.messageType.value == .activities { + + } else { + self.items.accept(followersSections) + } + }.disposed(by: rx.disposeBag) @@ -187,7 +185,7 @@ class MessageViewModel: ViewModel, ViewModelType { activitiesItems: activitiesItems, items: items, selection: selection, - itemSelected: itemSelected) + modelSelected: input.modelSelected) } diff --git a/IndieMusic/IndieMusic/Modules/Mine/MineSingleController.swift b/IndieMusic/IndieMusic/Modules/Mine/MineSingleController.swift index 2788133..dec0073 100644 --- a/IndieMusic/IndieMusic/Modules/Mine/MineSingleController.swift +++ b/IndieMusic/IndieMusic/Modules/Mine/MineSingleController.swift @@ -72,6 +72,7 @@ class MineSingleController: ViewController { } }.disposed(by: rx.disposeBag) + } @@ -120,6 +121,28 @@ class MineSingleController: ViewController { }.disposed(by: rx.disposeBag) + + headerView.playAllButton.rx.tap.subscribe { _ in + viewModel.playSubject.accept(()) + }.disposed(by: rx.disposeBag) + + output.toPlay.subscribe { audioTracks in + guard let audioTracks = audioTracks.element, + let audioTrack = audioTracks.first else { return } + + let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) + self.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + // 这里放置延迟执行的代码 + AudioManager.sharedInstance.setPlaylist(list: audioTracks) + AudioManager.sharedInstance.playTrack(track: audioTrack) + } + + + }.disposed(by: rx.disposeBag) + + } diff --git a/IndieMusic/IndieMusic/Modules/Mine/MineSingleViewModel.swift b/IndieMusic/IndieMusic/Modules/Mine/MineSingleViewModel.swift index 63a6e6a..681807f 100644 --- a/IndieMusic/IndieMusic/Modules/Mine/MineSingleViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Mine/MineSingleViewModel.swift @@ -25,6 +25,7 @@ class MineSingleViewModel: ViewModel, ViewModelType { let items: BehaviorRelay<[MineSingleSection]> let selection: Driver let itemSelected: PublishSubject + let toPlay: PublishRelay<[AudioTrack]> } @@ -33,7 +34,10 @@ class MineSingleViewModel: ViewModel, ViewModelType { let needRefresh: BehaviorRelay = .init(value: nil) + let playSubject = PublishRelay.init() + func transform(input: Input) -> Output { + let toPlay = PublishRelay<[AudioTrack]>.init() input.viewWillAppear.subscribe { (_) in @@ -48,10 +52,14 @@ class MineSingleViewModel: ViewModel, ViewModelType { return self.requestSingleList(page: self.page, size: 10) .trackActivity(self.headerLoading) }) - .subscribe(onNext: { (items) in - self.items.accept([MineSingleSection.init(items: items)]) - }).disposed(by: rx.disposeBag) + .subscribe(onNext: { items in + self.items.accept([MineSingleSection.init(items: items)]) + }, onError: { error in + + }).disposed(by: rx.disposeBag) + + input.footerRefresh.flatMapLatest({ [weak self] () -> Observable<[AudioTrack]> in guard let self = self else { return Observable.just([]) } self.page += 1 @@ -77,11 +85,25 @@ class MineSingleViewModel: ViewModel, ViewModelType { self.itemSelected.onNext(sectionItem) }.disposed(by: rx.disposeBag) + + playSubject.subscribe { _ in + self.requestSingleList(page: 0, size: 0) + .subscribe { audioTracks in + + toPlay.accept(audioTracks) + } onError: { error in + + }.disposed(by: self.rx.disposeBag) + + + + }.disposed(by: rx.disposeBag) return Output.init(items: items, selection: input.selection, - itemSelected: itemSelected) + itemSelected: itemSelected, + toPlay: toPlay) } diff --git a/IndieMusic/IndieMusic/Modules/Mine/MineView.swift b/IndieMusic/IndieMusic/Modules/Mine/MineView.swift index d64f3d4..1123dc5 100644 --- a/IndieMusic/IndieMusic/Modules/Mine/MineView.swift +++ b/IndieMusic/IndieMusic/Modules/Mine/MineView.swift @@ -315,6 +315,7 @@ class MineViewCell: UITableViewCell { return playButton }() + var playClousres: (()->())? var mine: Mine? { @@ -337,6 +338,8 @@ class MineViewCell: UITableViewCell { selectionStyle = .none makeUI() + + playButton.addTarget(self, action: #selector(playButtonClick), for: .touchUpInside) } required init?(coder: NSCoder) { @@ -390,5 +393,11 @@ class MineViewCell: UITableViewCell { } } + + @objc func playButtonClick() { + if let playClousres = self.playClousres { + playClousres() + } + } } diff --git a/IndieMusic/IndieMusic/Modules/Mine/MineViewController.swift b/IndieMusic/IndieMusic/Modules/Mine/MineViewController.swift index 0b584cb..e981281 100644 --- a/IndieMusic/IndieMusic/Modules/Mine/MineViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Mine/MineViewController.swift @@ -37,16 +37,34 @@ class MineViewController: TableViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + + self.navigationController?.setNavigationBarHidden(false, animated: true) + if let tabbar = self.tabBarController as? HomeTabBarController { tabbar.showAllBar(true, animated: true) } + + if AudioManager.sharedInstance.playlist?.count ?? 0 > 0 { + tableView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight + 66, right: 0) + } else { + tableView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight, right: 0) + } } override func viewDidLoad() { super.viewDidLoad() + let fixedSpaceBarButtonItem = UIBarButtonItem.init(barButtonSystemItem: .fixedSpace, target: nil, action: nil) + fixedSpaceBarButtonItem.width = -10 + + let spacerView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 1)) + let spacer = UIBarButtonItem(customView: spacerView) + + navigationItem.rightBarButtonItems = [settingBarButton, fixedSpaceBarButtonItem, messageBarButton] + + } @@ -65,17 +83,7 @@ class MineViewController: TableViewController { - - let fixedSpaceBarButtonItem = UIBarButtonItem.init(barButtonSystemItem: .fixedSpace, target: nil, action: nil) - fixedSpaceBarButtonItem.width = -10 - - let spacerView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 1)) - let spacer = UIBarButtonItem(customView: spacerView) - - - navigationItem.rightBarButtonItems = [settingBarButton, fixedSpaceBarButtonItem, messageBarButton] - - + } @@ -91,6 +99,17 @@ class MineViewController: TableViewController { let output = viewModel.transform(input: input) headerView.nameButton.rx.tap.subscribe { [weak self] _ in + if let tabBar = self?.tabBarController as? HomeTabBarController { + for subview in tabBar.customeTabBar.subviews { + // UITabBarButton is a private class of UIKit, so we check by converting the class to a String + let viewClassName = NSStringFromClass(subview.classForCoder) + if viewClassName.contains("UITabBarButton") { + print("Tab Bar Item Frame: \(subview.frame)") + } + } + } + + guard AuthManager.shared.token?.isValid == false else { return } let loginViewModel = LoginViewModel.init(provider: viewModel.provider) @@ -148,7 +167,9 @@ class MineViewController: TableViewController { - let dataSource = MineViewController.dataSource() + let dataSource = MineViewController.dataSource { cell, mine in + viewModel.playSubject.accept(()) + } output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) @@ -204,34 +225,41 @@ class MineViewController: TableViewController { self?.headerView.user = user }.disposed(by: rx.disposeBag) - - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - tableView.snp.remakeConstraints { make in - make.edges.equalTo(view) - if AudioManager.sharedInstance.playlist?.count ?? 0 > 0 { - make.bottom.equalTo(view).offset(-66 - BaseDimensions.tabBarHeight) - } else { - make.bottom.equalTo(view) + output.toPlay.subscribe { audioTracks in + guard let audioTracks = audioTracks.element, + let audioTrack = audioTracks.first else { return } + + let playerViewModel = PlayerViewModel.init(track: audioTrack, provider: viewModel.provider) + self.navigator.show(segue: .player(viewModel: playerViewModel), sender: self, transition: .modal) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + // 这里放置延迟执行的代码 + AudioManager.sharedInstance.setPlaylist(list: audioTracks) + AudioManager.sharedInstance.playTrack(track: audioTrack) } - } + + + }.disposed(by: rx.disposeBag) + + - } + } extension MineViewController { - static func dataSource() -> RxTableViewSectionedReloadDataSource { + static func dataSource(_ buttonTapHandler: @escaping (UITableViewCell, Mine) -> Void) -> RxTableViewSectionedReloadDataSource { return RxTableViewSectionedReloadDataSource( configureCell: { dataSource, tableView, indexPath, item in let cell: MineViewCell = tableView.dequeueReusableCell(withIdentifier: "MineViewCell", for: indexPath) as! MineViewCell cell.mine = item + + cell.playClousres = { + buttonTapHandler(cell, item) + } return cell } ) diff --git a/IndieMusic/IndieMusic/Modules/Mine/MineViewModel.swift b/IndieMusic/IndieMusic/Modules/Mine/MineViewModel.swift index 8dab063..72b512e 100644 --- a/IndieMusic/IndieMusic/Modules/Mine/MineViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Mine/MineViewModel.swift @@ -23,6 +23,7 @@ class MineViewModel: ViewModel, ViewModelType { let selection: Driver let itemSelected: PublishSubject let user: BehaviorRelay + let toPlay: PublishRelay<[AudioTrack]> } @@ -30,8 +31,12 @@ class MineViewModel: ViewModel, ViewModelType { let items = BehaviorRelay<[MineSection]>.init(value: []) let user = BehaviorRelay.init(value: nil) + let playSubject = PublishRelay.init() + func transform(input: Input) -> Output { + let toPlay = PublishRelay<[AudioTrack]>.init() + input.viewWillAppear.subscribe { _ in self.requestUserInfoData().subscribe { user in self.user.accept(user) @@ -75,11 +80,25 @@ class MineViewModel: ViewModel, ViewModelType { }.disposed(by: rx.disposeBag) + playSubject.subscribe { _ in + self.requestAllSingleList() + .subscribe { audioTracks in + + toPlay.accept(audioTracks) + } onError: { error in + + }.disposed(by: self.rx.disposeBag) + + + + }.disposed(by: rx.disposeBag) + return Output.init(items: items, selection: input.selection, itemSelected: itemSelected, - user: user) + user: user, + toPlay: toPlay) } @@ -89,4 +108,12 @@ class MineViewModel: ViewModel, ViewModelType { .trackError(error) } + + func requestAllSingleList() -> Observable<[AudioTrack]> { + return self.provider.collectSongList(userId: UserDefaults.AccountInfo.string(forKey: .userID) ?? "", page: 0, size: 0) + .trackActivity(loading) + .trackError(error) + } + + } diff --git a/IndieMusic/IndieMusic/Modules/Personal/FollowersViewController.swift b/IndieMusic/IndieMusic/Modules/Personal/FollowersViewController.swift index 586a65d..0bfbd10 100644 --- a/IndieMusic/IndieMusic/Modules/Personal/FollowersViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Personal/FollowersViewController.swift @@ -172,9 +172,6 @@ extension FollowersViewController { } else { cell.followingButton.setTitle("取消", for: .normal) cell.followingButton.setTitle("添加", for: .selected) - - - } diff --git a/IndieMusic/IndieMusic/Modules/Personal/FollowingViewController.swift b/IndieMusic/IndieMusic/Modules/Personal/FollowingViewController.swift index a678c6b..01b13d6 100644 --- a/IndieMusic/IndieMusic/Modules/Personal/FollowingViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Personal/FollowingViewController.swift @@ -52,14 +52,12 @@ class FollowingViewController: TableViewController { let input = FollowingViewModel.Input.init(viewWillAppear: rx.viewWillAppear, headerRefresh: refresh, footerRefresh: footerRefreshTrigger, - selection: tableView.rx.itemSelected.asDriver()) + modelSelected: tableView.rx.modelSelected(User.self).asDriver()) let output = viewModel.transform(input: input) let dataSource = FollowingViewController.dataSource { cell, user in viewModel.itemSelected.onNext(user) - - } output.items.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) @@ -67,26 +65,13 @@ class FollowingViewController: TableViewController { - output.itemSelected.subscribe { [weak self] sectionItem in - guard let userID = sectionItem.element?.id else { return } - - let personalViewModel = PersonalViewModel.init(userID: userID, provider: viewModel.provider) - self?.navigator.show(segue: .personal(viewModel: personalViewModel), sender: self) - - }.disposed(by: rx.disposeBag) - - output.selection.drive { [weak self] indexPath in - guard let user = output.items.value.first?.items[indexPath.row] else { return } + output.modelSelected.drive { [weak self] user in let personalViewModel = PersonalViewModel.init(userID: user.id, provider: viewModel.provider) self?.navigator.show(segue: .personal(viewModel: personalViewModel), sender: self) - - - }.disposed(by: rx.disposeBag) - } diff --git a/IndieMusic/IndieMusic/Modules/Personal/FollowingViewModel.swift b/IndieMusic/IndieMusic/Modules/Personal/FollowingViewModel.swift index c8f5f5a..799113f 100644 --- a/IndieMusic/IndieMusic/Modules/Personal/FollowingViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Personal/FollowingViewModel.swift @@ -17,14 +17,13 @@ class FollowingViewModel: ViewModel, ViewModelType { let headerRefresh: Observable let footerRefresh: Observable - let selection: Driver + let modelSelected: Driver } struct Output { let items: BehaviorRelay<[FollowingSection]> - let selection: Driver - let itemSelected: PublishSubject + let modelSelected: Driver } @@ -134,8 +133,8 @@ class FollowingViewModel: ViewModel, ViewModelType { return Output.init(items: items, - selection: input.selection, - itemSelected: itemSelected) +// selection: input.selection, + modelSelected: input.modelSelected) } diff --git a/IndieMusic/IndieMusic/Modules/Personal/PersonalViewController.swift b/IndieMusic/IndieMusic/Modules/Personal/PersonalViewController.swift index 7f5197c..504ba9d 100644 --- a/IndieMusic/IndieMusic/Modules/Personal/PersonalViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Personal/PersonalViewController.swift @@ -113,8 +113,10 @@ class PersonalViewController: ViewController { guard let viewModel = viewModel as? PersonalViewModel else { return } + let refresh = Observable.of(Observable.just(()), headerRefreshTrigger).merge() + let input = PersonalViewModel.Input.init(viewWillAppear: rx.viewWillAppear, - headerRefresh: headerRefreshTrigger, + headerRefresh: refresh, footerRefresh: footerRefreshTrigger, personInfoLikeType: personInfoLikeType, selection: collectionView.rx.itemSelected.asDriver(), @@ -433,6 +435,7 @@ class PersonalHeaderView: UICollectionReusableView { let messageButton: BadgeButton = { let messageButton = BadgeButton.init() messageButton.setImage(UIImage.init(named: "personal_message_btn"), for: .normal) + messageButton.isHidden = true return messageButton }() diff --git a/IndieMusic/IndieMusic/Modules/Personal/PersonalViewModel.swift b/IndieMusic/IndieMusic/Modules/Personal/PersonalViewModel.swift index 236df38..18aedef 100644 --- a/IndieMusic/IndieMusic/Modules/Personal/PersonalViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Personal/PersonalViewModel.swift @@ -92,10 +92,14 @@ class PersonalViewModel: ViewModel, ViewModelType { } }) - .subscribe(onNext: { (items) in - + .subscribe(onNext: { items in self.items.accept([PersonInfoSection.audio(personInfoLikeType: self.personInfoLikeType.value, items: items)]) + + }, onError: { error in + self.items.accept([PersonInfoSection.audio(personInfoLikeType: self.personInfoLikeType.value, items: [])]) + }).disposed(by: rx.disposeBag) + input.footerRefresh.withLatestFrom(input.personInfoLikeType) .flatMapLatest({ [weak self] (personInfoLikeType) -> Observable<[PersonInfoItem]> in diff --git a/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift b/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift index 16d398c..1d15c38 100644 --- a/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Search/SearchViewController.swift @@ -57,6 +57,15 @@ class SearchViewController: ViewController, UIScrollViewDelegate { } self.navigationController?.setNavigationBarHidden(true, animated: false) + + + if AudioManager.sharedInstance.playlist?.count ?? 0 > 0 { + collectionView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight + 66, right: 0) + } else { + collectionView.contentInset = .init(top: 0, left: 0, bottom: BaseDimensions.tabBarHeight, right: 0) + } + + } override func viewWillDisappear(_ animated: Bool) { @@ -136,11 +145,7 @@ class SearchViewController: ViewController, UIScrollViewDelegate { make.left.equalTo(view) make.right.equalTo(view) make.top.equalTo(view).offset(-BaseDimensions.navBarHeight) - if AudioManager.sharedInstance.playlist?.count ?? 0 > 0 { - make.bottom.equalTo(view).offset(-66 - BaseDimensions.tabBarHeight) - } else { - make.bottom.equalTo(view).offset(-BaseDimensions.tabBarHeight) - } + make.bottom.equalTo(view) } } diff --git a/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditInfoViewController.swift b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditInfoViewController.swift index 35a5963..d823f30 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditInfoViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditInfoViewController.swift @@ -43,6 +43,9 @@ class EditInfoViewController: TableViewController { super.bindViewModel() guard let viewModel = viewModel as? EditInfoViewModel else { return } + var nikeName = "" + var signature = "" + let input = EditInfoViewModel.Input.init(viewWillAppear: rx.viewWillAppear, selection: tableView.rx.itemSelected.asDriver(), @@ -65,13 +68,14 @@ class EditInfoViewController: TableViewController { output.selection.drive { [weak self] sectionItem in + self?.deselectSelectedRow() switch sectionItem.row { case 0: - let editNameViewModel = EditNameViewModel.init(provider: viewModel.provider) + let editNameViewModel = EditNameViewModel.init(nickName: nikeName, provider: viewModel.provider) self?.navigator.show(segue: .editName(viewModel: editNameViewModel), sender: self) case 1: - let editSignatureViewModel = EditSignatureViewModel.init(provider: viewModel.provider) + let editSignatureViewModel = EditSignatureViewModel.init(signature: signature, provider: viewModel.provider) self?.navigator.show(segue: .editSignature(viewModel: editSignatureViewModel), sender: self) case 2: let editDate = EditDateViewModel.init(birthDayText: viewModel.birthDayText, provider: viewModel.provider) @@ -83,13 +87,14 @@ class EditInfoViewController: TableViewController { default: break } - }.disposed(by: rx.disposeBag) output.user.subscribe { [weak self] user in guard let user = user.element else { return } + nikeName = user.nickName ?? "" + signature = user.signature ?? "" self?.headerView.avatarView.kf.setImage(with: URL.init(string: user.avatar ?? "")) }.disposed(by: rx.disposeBag) @@ -97,15 +102,11 @@ class EditInfoViewController: TableViewController { self?.headerView.avatarView.kf.setImage(with: URL.init(string: url)) }.disposed(by: rx.disposeBag) + let avatarViewTap = headerView.avatarView.rx.tapGesture().when(.recognized) + let avatarLabelTap = headerView.tipsLabel.rx.tapGesture().when(.recognized) - - headerView.avatarView.rx.tapGesture().when(.recognized) + Observable.merge(avatarViewTap, avatarLabelTap) .subscribe { gestureRecognizer in -// self.navigator.show(segue: .photoConfirm, sender: self, transition: .navigationPresent(type: .photoConfirm)) -// let configuration = PHPickerConfiguration() -// configuration.filter = .images // 只显示图片 - - RxModal.photoPicker(presenter: .viewController(self)) { config in config.filter = .images config.selectionLimit = 1 @@ -127,14 +128,15 @@ class EditInfoViewController: TableViewController { } onFailure: { error in }.disposed(by: self.rx.disposeBag) - + + }.disposed(by: rx.disposeBag) tableView.mj_footer = nil tableView.mj_header = nil } - + } diff --git a/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditNameController.swift b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditNameController.swift index a742829..ef625b3 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditNameController.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditNameController.swift @@ -23,9 +23,17 @@ class EditNameViewModel: ViewModel, ViewModelType { let error: Driver let textCount: Driver let dismiss: PublishSubject + } let dismiss = PublishSubject.init() + let nickName: String + + init(nickName: String, provider: IndieMusicAPI) { + self.nickName = nickName + + super.init(provider: provider) + } func transform(input: Input) -> Output { let errorSubject = PublishSubject() @@ -98,7 +106,11 @@ class EditNameController: ViewController { super.viewDidLoad() - +// viewModel.nickNameSubject +// .take(1) // 只取第一次发出的值,即初始值 +// .asDriver(onErrorJustReturn: "") +// .drive(editNameTextFieldView.textFieldView.rx.text) +// .disposed(by: rx.disposeBag) } @@ -109,7 +121,7 @@ class EditNameController: ViewController { view.addSubview(editNameTextFieldView) - confirmBarButton + self.navigationItem.rightBarButtonItem = confirmBarButton } @@ -117,7 +129,8 @@ class EditNameController: ViewController { super.bindViewModel() guard let viewModel = viewModel as? EditNameViewModel else { return } - + editNameTextFieldView.textFieldView.text = viewModel.nickName + let input = EditNameViewModel.Input.init(viewWillAppear: rx.viewWillAppear, nickName: editNameTextFieldView.textFieldView.rx.text.filterNil().asDriver(onErrorJustReturn: ""), saveTrigger: confirmBarButton.rx.tap.asDriver()) @@ -126,6 +139,10 @@ class EditNameController: ViewController { let output = viewModel.transform(input: input) + + + + output.textCount.drive { textCount in self.editNameTextFieldView.countTipsLabel.addAttributed(attributedes: [("\(textCount)", [NSAttributedString.Key.foregroundColor: UIColor.primaryText()]), ("/10", [NSAttributedString.Key.foregroundColor: UIColor.tertiaryText()]) @@ -144,7 +161,6 @@ class EditNameController: ViewController { self?.navigator.pop(sender: self) }.disposed(by: rx.disposeBag) - } diff --git a/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSexViewController.swift b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSexViewController.swift index 8ebec05..61dced4 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSexViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSexViewController.swift @@ -133,7 +133,7 @@ class EditSexViewController: ViewController, UIScrollViewDelegate { output.popView.subscribe { [weak self] _ in - self?.navigator.pop(sender: self) + self?.navigator.dismiss(sender: self) }.disposed(by: rx.disposeBag) dismissButton.rx.tap.subscribe { [weak self] _ in diff --git a/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSignatureViewController.swift b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSignatureViewController.swift index 9fa4201..31000af 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSignatureViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/EditInfo/EditSignatureViewController.swift @@ -26,6 +26,12 @@ class EditSignatureViewModel: ViewModel, ViewModelType { } let dismiss = PublishSubject.init() + let signature: String + + init(signature: String, provider: IndieMusicAPI) { + self.signature = signature + super.init(provider: provider) + } func transform(input: Input) -> Output { let errorSubject = PublishSubject() @@ -118,6 +124,8 @@ class EditSignatureViewController: ViewController { guard let viewModel = viewModel as? EditSignatureViewModel else { return } + editNameTextFieldView.textFieldView.text = viewModel.signature + let input = EditSignatureViewModel.Input.init(viewWillAppear: rx.viewWillAppear, signature: editNameTextFieldView.textFieldView.rx.text.filterNil().asDriver(onErrorJustReturn: ""), saveTrigger: confirmBarButton.rx.tap.asDriver()) diff --git a/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewController.swift b/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewController.swift index bcdf985..ca2dbaf 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewController.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewController.swift @@ -10,6 +10,7 @@ import GrowingTextView import RxSwift import RxCocoa import RxDataSources +import RxModal class FeedbackViewController: ViewController { let feedbackTypeView: FeedbackTypeView = { @@ -42,6 +43,9 @@ class FeedbackViewController: ViewController { return confirmButton }() + let photoItems = PublishRelay<[UIImage]>.init() + let category = PublishRelay.init() + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -78,14 +82,63 @@ class FeedbackViewController: ViewController { guard let viewModel = viewModel as? FeedbackViewModel else { return } - let input = FeedbackViewModel.Input.init(viewWillAppear: rx.viewWillAppear) + let input = FeedbackViewModel.Input.init(viewWillAppear: rx.viewWillAppear, + modelSelected: feedbackDetailView.feedbackDetailSubView.collectionView.rx.modelSelected(FeedbackPhotoType.self).asDriver(), + photoItems: photoItems, + category: category, + content: feedbackDetailView.feedbackDetailSubView.textView.rx.text.asDriver(), + contact: feedbackContactView.textFieldView.rx.text.asDriver(), + confirmTrigger: confirmButton.rx.tap.asDriver()) + let output = viewModel.transform(input: input) - let dataSource = FeedbackDetailSubView.dataSource() + let dataSource = FeedbackDetailSubView.dataSource { cell, image in + viewModel.deleteImage(image, from: viewModel.photoItems) + + } output.photoItems.bind(to: feedbackDetailView.feedbackDetailSubView.collectionView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag) - + output.photoItems.subscribe { feedbackSection in + if feedbackSection.first?.items.isEmpty == false { + self.feedbackDetailView.feedbackDetailSubView.setNeedsLayout() + self.feedbackDetailView.feedbackDetailSubView.layoutIfNeeded() + } + + }.disposed(by: rx.disposeBag) + + + + output.toPhotoSelect.subscribe { imageArray in + + RxModal.photoPicker(presenter: .viewController(self)) { config in + config.filter = .images + config.selectionLimit = 9 + }.subscribe { results in + var images = [UIImage]() + let itemProviders = results.map(\.itemProvider) + for item in itemProviders { + if item.canLoadObject(ofClass: UIImage.self) { + item.loadObject(ofClass: UIImage.self) { (image, error) in + DispatchQueue.main.async { + if let image = image as? UIImage { + images.append(image) + self.photoItems.accept(images) + } + } + } + } + } + + + } onFailure: { error in + + }.disposed(by: self.rx.disposeBag) + + + }.disposed(by: rx.disposeBag) + + } @@ -312,36 +365,37 @@ class FeedbackDetailSubView: UIView { var collectionView: UICollectionView = { let layout = UICollectionViewCompositionalLayout { (sectionIndex, environment) -> NSCollectionLayoutSection? in - - // 创建item大小 - let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(84), heightDimension: .absolute(84)) + + // 创建Item + let collectionViewWidth = BaseDimensions.screenWidth - 2 * 18 + // 计算总的间距宽度 + let totalSpacingWidth: CGFloat = (2 * 18) + (2 * 18) // 两边的边缘间距加上两个 item 之间的间距 + // 计算每个 item 的宽度 + let adjustedItemWidth = (collectionViewWidth - totalSpacingWidth) / 3 + + // 然后使用 adjustedItemWidth 来设置 itemSize + let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(adjustedItemWidth), heightDimension: .absolute(adjustedItemWidth)) + +// let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(84), heightDimension: .absolute(84)) let item = NSCollectionLayoutItem(layoutSize: itemSize) - // 创建group大小 - let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(84), heightDimension: .absolute(84)) + // 创建水平子组(每排) + let verticalGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(84)) + let verticalGroup = NSCollectionLayoutGroup.horizontal(layoutSize: verticalGroupSize, subitem: item, count: 3) + verticalGroup.interItemSpacing = .fixed(18) - // 创建group,这里使用.horizontal来使其横向滚动 - let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) - - // 创建section - let section = NSCollectionLayoutSection(group: group) + // 创建分区 + let section = NSCollectionLayoutSection(group: verticalGroup) section.contentInsets = .init(top: 18, leading: 18, bottom: 18, trailing: 18) section.interGroupSpacing = 18 - - // 设置横向滚动行为 - section.orthogonalScrollingBehavior = .continuous return section } - let collectionView = UICollectionView.init(frame: CGRect.zero, collectionViewLayout: layout) collectionView.register(FeedbackDetailViewCell.self, forCellWithReuseIdentifier: "FeedbackDetailViewCell") collectionView.showsHorizontalScrollIndicator = false collectionView.showsVerticalScrollIndicator = false - - -// collectionView.backgroundColor = .init(hex: 0xfbfbfb) - + return collectionView }() @@ -387,38 +441,46 @@ class FeedbackDetailSubView: UIView { } + // 创建Item + let collectionViewWidth = BaseDimensions.screenWidth - 2 * 18 + // 计算总的间距宽度 + let totalSpacingWidth: CGFloat = (2 * 18) + (2 * 18) // 两边的边缘间距加上两个 item 之间的间距 + // 计算每个 item 的宽度 + let adjustedItemWidth = (collectionViewWidth - totalSpacingWidth) / 3 + let rowsPerSection = ceil(Double(collectionView.numberOfItems(inSection: 0)) / Double(3)) + let totalSectionHeight = CGFloat(rowsPerSection) * adjustedItemWidth + CGFloat(rowsPerSection - 1) * 18 + 36 + + print("collectionView height \(rowsPerSection) \(totalSectionHeight)") + collectionView.snp.remakeConstraints { make in make.left.equalTo(self) make.right.equalTo(self) make.top.equalTo(countLabel.snp.bottom).offset(53) make.bottom.equalTo(self).offset(0) - make.height.equalTo(120) + make.height.equalTo(totalSectionHeight) } +// print("collectionView height \(84 * collectionView.numberOfItems(inSection: 0) % 3) 个数\(collectionView.numberOfItems(inSection: 0))") + } } extension FeedbackDetailSubView { - static func dataSource() -> RxCollectionViewSectionedReloadDataSource { + static func dataSource(_ buttonTapHandler: @escaping (UICollectionViewCell, UIImage) -> Void) -> RxCollectionViewSectionedReloadDataSource { return RxCollectionViewSectionedReloadDataSource( configureCell: { dataSource, collectionView, indexPath, item in - switch dataSource[indexPath] { - case .add: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FeedbackDetailViewCell", for: indexPath) as? FeedbackDetailViewCell - cell?.feedbackPhotoType = item - - return cell! - case .photo(let photo): - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FeedbackDetailViewCell", for: indexPath) as? FeedbackDetailViewCell - cell?.feedbackPhotoType = item - - return cell! + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FeedbackDetailViewCell", for: indexPath) as! FeedbackDetailViewCell + cell.feedbackPhotoType = item + + cell.closeClosures = { image in + buttonTapHandler(cell, image) } + return cell } ) } @@ -450,6 +512,7 @@ class FeedbackDetailViewCell: UICollectionViewCell { return addImageView }() + var closeClosures: ((UIImage) -> ())? var feedbackPhotoType: FeedbackPhotoType? { didSet { @@ -470,6 +533,7 @@ class FeedbackDetailViewCell: UICollectionViewCell { addImageView.isHidden = true imageView.isHidden = false closeButton.isHidden = false + imageView.image = image contentView.layer.borderColor = UIColor.clear.cgColor contentView.layer.borderWidth = 1 @@ -486,6 +550,8 @@ class FeedbackDetailViewCell: UICollectionViewCell { super.init(frame: frame) makeUI() + + closeButton.addTarget(self, action: #selector(closeButtonClick), for: .touchUpInside) } required init?(coder: NSCoder) { @@ -513,6 +579,15 @@ class FeedbackDetailViewCell: UICollectionViewCell { } } + @objc func closeButtonClick() { + guard let feedbackPhotoType = feedbackPhotoType, + let closeClosures = self.closeClosures else { return } + + if case FeedbackPhotoType.photo(let image) = feedbackPhotoType{ + closeClosures(image) + } + } + } diff --git a/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewModel.swift b/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewModel.swift index 7e84117..bca5dad 100644 --- a/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewModel.swift +++ b/IndieMusic/IndieMusic/Modules/Setting/FeedbackViewModel.swift @@ -13,19 +13,22 @@ class FeedbackViewModel: ViewModel, ViewModelType { struct Input { let viewWillAppear: ControlEvent - - + let modelSelected: Driver + let photoItems: PublishRelay<[UIImage]> + let category: PublishRelay + let content: Driver + let contact: Driver + let confirmTrigger: Driver } struct Output { let photoItems: BehaviorRelay<[FeedbackSection]> -// let itemSelected: PublishSubject - + let toPhotoSelect: PublishRelay<[UIImage]> } -// let itemSelected = PublishSubject() - let items = BehaviorRelay<[FeedbackSection]>.init(value: []) - + let photoItems = BehaviorRelay<[FeedbackSection]>.init(value: [.init(items: [.add])]) + let toPhotoSelect = PublishRelay<[UIImage]>.init() + func transform(input: Input) -> Output { input.viewWillAppear.subscribe { (_) in @@ -33,12 +36,103 @@ class FeedbackViewModel: ViewModel, ViewModelType { }.disposed(by: rx.disposeBag) - let add = FeedbackPhotoType.add - - self.items.accept([FeedbackSection.init(items: [.add, .add, .add, .add, .add, .add, .add ,.add, .add])]) + + input.photoItems.subscribe { imageArray in + + guard let imageArray = imageArray.element else { return } + + + var photos = imageArray.map { image in + return FeedbackPhotoType.photo(image) + } + + if imageArray.count < 9 { + photos.append(.add) + } + self.photoItems.accept([FeedbackSection.init(items: photos)]) + + }.disposed(by: rx.disposeBag) + + + input.modelSelected.drive { feedbackPhotoType in + + switch feedbackPhotoType { + case .add: + self.toPhotoSelect.accept([]) + case .photo(let image): + break + } + + + + }.disposed(by: rx.disposeBag) - return Output.init(photoItems: items) + +// input.confirmTrigger.drive { _ in + +// self.uploadFeedback(type: 0, content: "", images: [], contact: "") +// .subscribe { _ in +// +// } onError: { error in +// +// }.disposed(by: self.rx.disposeBag) +// }.disposed(by: rx.disposeBag) + + + let parms = Driver.combineLatest( + input.photoItems.startWith([]).asDriver(onErrorJustReturn: []), + input.category.startWith("").asDriver(onErrorJustReturn: ""), + input.content, + input.contact + ) + + input.confirmTrigger + .withLatestFrom(parms) // 这里不需要额外的解构,因为闭包会直接接收到元组 + .flatMapLatest { tuple in // 使用单个参数接收元组 + // 解构元组以获取各个值 + print("点击发送") + let (photoItems, category, content, contact) = tuple + // 确保content和contact有默认值,以避免Optional + return self.uploadFeedback(type: 0, content: "", images: [], contact: "") + .asDriver(onErrorJustReturn: ()) + } + .drive(onNext: { _ in + + }) + .disposed(by: rx.disposeBag) + + + + + return Output.init(photoItems: self.photoItems, + toPhotoSelect: toPhotoSelect) } + + func deleteImage(_ imageToDelete: UIImage, from photoItems: BehaviorRelay<[FeedbackSection]>) { + var sections = photoItems.value + + var didDeleteImage = false + + for sectionIndex in sections.indices where !didDeleteImage { + let items = sections[sectionIndex].items + if let itemIndex = items.firstIndex(where: { if case .photo(let image) = $0, image == imageToDelete { return true } else { return false } }) { + sections[sectionIndex].items.remove(at: itemIndex) + didDeleteImage = true + } + } + + if didDeleteImage { + photoItems.accept(sections) + } + } + + + func uploadFeedback(type: Int, content: String, images: [UIImage], contact: String) -> Observable { + return self.provider.feedback(type: type, content: content, images: images, contact: contact) + .trackActivity(loading) + .trackError(error) + + } } diff --git a/IndieMusic/IndieMusic/Networking/Api.swift b/IndieMusic/IndieMusic/Networking/Api.swift index 0d61ea8..17b8449 100644 --- a/IndieMusic/IndieMusic/Networking/Api.swift +++ b/IndieMusic/IndieMusic/Networking/Api.swift @@ -95,5 +95,7 @@ protocol IndieMusicAPI { /// 我的评论 func myCommentList(page: Int, size: Int) -> Single<[Comment]> + /// 反馈 + func feedback(type: Int, content: String, images: [UIImage], contact: String) -> Single } diff --git a/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift b/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift index 092befe..77c6720 100644 --- a/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift +++ b/IndieMusic/IndieMusic/Networking/Rest/APIConfig.swift @@ -35,7 +35,7 @@ enum APIConfig { case journal(String) case messageList(Int, Int) - case collectSongList(String, Int, Int) + case collectSongList([String: Any]) case journalCollectList(String, Int, Int) case followingList(String, Int, Int) case followerList(String, Int, Int) @@ -58,6 +58,7 @@ enum APIConfig { case myThumbupList(Int, Int) case myCommentReplyList(Int, Int) + case feedback([Data], [String: Any]) } @@ -103,8 +104,8 @@ extension APIConfig: TargetType { return "luoo-music/song/getByJournalNo/\(journalNo)" case .messageList(let page, let size): return "luoo-user/userMessage/list/\(page)/\(size)" - case .collectSongList(let userId, let page, let size): - return "luoo-music/song/collect/\(userId)/\(page)/\(size)" + case .collectSongList: + return "luoo-music/song/collect" case .journalCollectList(let userId, let page, let size): return "luoo-music/journal/collect/\(userId)/\(page)/\(size)" case .followingList(let userId, let page, let size): @@ -143,6 +144,9 @@ extension APIConfig: TargetType { return "luoo-user/my/myThumbupList/\(page)/\(size)" case .myCommentReplyList(let page, let size): return "luoo-user/my/myCommentReplyList/\(page)/\(size)" + + case .feedback: + return "luoo-user/my/feedback" } } @@ -150,7 +154,7 @@ extension APIConfig: TargetType { switch self { case .wechatAccessToken, .journalList, .journalMusic, .countryCode, .imageCheckCode, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .collectSongList, .journalCollectList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .serach, .randomAudioTrack, .myThumbupList, .myCommentReplyList: return .get - case .sendsms, .login, .autoLogin, .editAvatar, .like, .checkVersion, .logout, .sendComment: + case .sendsms, .login, .autoLogin, .editAvatar, .like, .checkVersion, .logout, .sendComment, .feedback: return .post case .editUserInfo, .commentLike: return .put @@ -165,7 +169,7 @@ extension APIConfig: TargetType { case .wechatAccessToken, .journalList, .journalMusic, .countryCode, .sendsms, .imageCheckCode, .login, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .like, .cancelLike, .collectSongList, .journalCollectList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .serach, .randomAudioTrack, .myThumbupList, .myCommentReplyList: return URLEncoding.default - case .autoLogin, .editUserInfo, .editAvatar, .checkVersion, .logout, .commentLike, .sendComment: + case .autoLogin, .editUserInfo, .editAvatar, .checkVersion, .logout, .commentLike, .sendComment, .feedback: return JSONEncoding.default } } @@ -174,10 +178,10 @@ extension APIConfig: TargetType { var task: Task { var parameters: [String: Any] = [:] switch self { - case .wechatAccessToken, .countryCode, .journalMusic, .imageCheckCode, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .collectSongList, .journalCollectList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .serach, .randomAudioTrack, .myThumbupList, .myCommentReplyList: + case .wechatAccessToken, .countryCode, .journalMusic, .imageCheckCode, .getUserInfo, .carousel, .otherUserInfo, .single, .journal, .messageList, .journalCollectList, .followingList, .followerList, .blackList, .hotCommentList, .latestCommentList, .subCommentList, .filterMenu, .journalRecommend, .searchCategory, .serach, .randomAudioTrack, .myThumbupList, .myCommentReplyList: return .requestPlain - case .login(let dic), .journalList(let dic), .sendsms(let dic), .autoLogin(let dic), .editUserInfo(let dic), .like(let dic), .cancelLike(let dic), .logout(let dic), .checkVersion(let dic), .commentLike(_, let dic), .sendComment(let dic): + case .login(let dic), .journalList(let dic), .sendsms(let dic), .autoLogin(let dic), .editUserInfo(let dic), .like(let dic), .cancelLike(let dic), .logout(let dic), .checkVersion(let dic), .commentLike(_, let dic), .sendComment(let dic), .collectSongList(let dic): parameters = dic return .requestParameters(parameters: parameters, encoding: parameterEncoding) @@ -185,6 +189,23 @@ extension APIConfig: TargetType { case .editAvatar(let imageData): let formData = [MultipartFormData(provider: .data(imageData), name: "file", fileName: Date.init().description, mimeType: "image/png")] return .uploadMultipart(formData) + + case .feedback(let imageDataArray, let dic): + var formData = imageDataArray.map { imageData in + return MultipartFormData(provider: .data(imageData), + name: "images_feedback", // 服务器期待的字段名 + fileName: Date.init().description, + mimeType: "image/png") + } + + for (key, value) in dic { + if let stringValue = value as? String, + let data = stringValue.data(using: .utf8) { + formData.append(MultipartFormData(provider: .data(data), name: key)) + } + } + + return .uploadMultipart(formData) } } @@ -203,7 +224,7 @@ extension APIConfig: TargetType { var headers : [String : String]? { switch self { - case .autoLogin, .getUserInfo, .journalList, .journalMusic, .otherUserInfo, .like, .cancelLike, .single, .journal, .collectSongList, .journalCollectList, .followingList, .messageList, .followerList, .blackList, .editUserInfo, .logout, .editAvatar, .hotCommentList, .latestCommentList, .subCommentList, .commentLike, .filterMenu, .journalRecommend, .serach, .randomAudioTrack, .sendComment, .myThumbupList, .myCommentReplyList: + case .autoLogin, .getUserInfo, .journalList, .journalMusic, .otherUserInfo, .like, .cancelLike, .single, .journal, .collectSongList, .journalCollectList, .followingList, .messageList, .followerList, .blackList, .editUserInfo, .logout, .editAvatar, .hotCommentList, .latestCommentList, .subCommentList, .commentLike, .filterMenu, .journalRecommend, .serach, .randomAudioTrack, .sendComment, .myThumbupList, .myCommentReplyList, .feedback: return ["Authorization": AuthManager.shared.token?.basicToken ?? ""] default: return nil diff --git a/IndieMusic/IndieMusic/Networking/Rest/Networking.swift b/IndieMusic/IndieMusic/Networking/Rest/Networking.swift index 0c235d4..a7fa3a6 100644 --- a/IndieMusic/IndieMusic/Networking/Rest/Networking.swift +++ b/IndieMusic/IndieMusic/Networking/Rest/Networking.swift @@ -59,7 +59,6 @@ extension ObservableType where Element == Response { //这里的情况是,只有code为0的时候,数据data才有东西 if let errodModel = try? JSONDecoder().decode(ErrorResponse.self, from: response.data) { - print("ssss") if let msg = errodModel.message, msg == "处理失败,未知异常" { print("接口错误:\(String(describing: response.request?.url))") } diff --git a/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift b/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift index 58b4bfc..4687e4b 100644 --- a/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift +++ b/IndieMusic/IndieMusic/Networking/Rest/RestApi.swift @@ -9,6 +9,7 @@ import Foundation import RxSwift import RxCocoa import Moya +import RxOptional typealias MoyaError = Moya.MoyaError @@ -142,7 +143,6 @@ extension RestApi { func editAvatar(imageData: Data) -> Single { return requestObject(.editAvatar(imageData), with: "data", type: String.self) - } func journalList(categoryId: String?, journalNoRange: String?, pageNum: Int, pageSize: Int) -> Single<[Journal]> { @@ -208,7 +208,11 @@ extension RestApi { func collectSongList(userId: String, page: Int, size: Int) -> Single<[AudioTrack]> { - return requestObject(.collectSongList(userId, page, size), with: "data.rows", type: [AudioTrack].self) + let dic = ["userId": userId, + "pageNum": page, + "pageSize": size + ] as [String : Any] + return requestObject(.collectSongList(dic), with: "data.rows", type: [AudioTrack].self) } func journalCollectList(userId: String, page: Int, size: Int) -> Single<[Journal]> { @@ -307,4 +311,17 @@ extension RestApi { } + func feedback(type: Int, content: String, images: [UIImage], contact: String) -> Single { + let dic = ["type": type, + "content": content, + "contact": contact] as [String : Any] + + let imageDataArray = images.map { image in + return image.pngData() + } + let nonNilDataArray = imageDataArray.compactMap { $0 } + + return requestWithoutMapping(.feedback(nonNilDataArray, dic)).map { _ in } + } + }