From 09d30ee16c20ac1381c5d1498817dc1ab2a85115 Mon Sep 17 00:00:00 2001 From: Yam Liu <1056803+yam-liu@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:25:31 +0800 Subject: [PATCH] feat(Comment): support multi-level comment preview, slightly refactor ReplyCell for reuse. (#107) Co-authored-by: Yam --- BilibiliLive.xcodeproj/project.pbxproj | 29 +++++ .../xcshareddata/swiftpm/Package.resolved | 12 +- BilibiliLive/Base.lproj/Main.storyboard | 13 +- .../Video/ReplyDetailViewController.swift | 111 ++++++++++++++++++ .../Video/VideoDetailViewController.swift | 23 ++-- BilibiliLive/Component/View/ReplyCell.swift | 29 +++++ BilibiliLive/Component/View/ReplyCell.xib | 82 +++++++++++++ BilibiliLive/Request/WebRequest.swift | 1 + 8 files changed, 280 insertions(+), 20 deletions(-) create mode 100644 BilibiliLive/Component/Video/ReplyDetailViewController.swift create mode 100644 BilibiliLive/Component/View/ReplyCell.swift create mode 100644 BilibiliLive/Component/View/ReplyCell.xib diff --git a/BilibiliLive.xcodeproj/project.pbxproj b/BilibiliLive.xcodeproj/project.pbxproj index 3235a2ee..ce3d72e3 100644 --- a/BilibiliLive.xcodeproj/project.pbxproj +++ b/BilibiliLive.xcodeproj/project.pbxproj @@ -11,6 +11,10 @@ 0A41EE1C2A63102B0066444C /* dm.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41EE1A2A63102B0066444C /* dm.pb.swift */; }; 0A41EE1D2A63102B0066444C /* dmView.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41EE1B2A63102B0066444C /* dmView.pb.swift */; }; 27FECFCC2B0B98F400EC6A6D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 27FECFCB2B0B98F400EC6A6D /* Localizable.xcstrings */; }; + 2806E51E2C1593A000164C10 /* LookinServer in Frameworks */ = {isa = PBXBuildFile; productRef = 2806E51D2C1593A000164C10 /* LookinServer */; }; + 2806E5202C15A59E00164C10 /* ReplyDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2806E51F2C15A59E00164C10 /* ReplyDetailViewController.swift */; }; + 2806E5232C16011B00164C10 /* ReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2806E5212C16011B00164C10 /* ReplyCell.swift */; }; + 2806E5242C16011B00164C10 /* ReplyCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2806E5222C16011B00164C10 /* ReplyCell.xib */; }; 2DBE4C4D2628818F00D20413 /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBE4C4C2628818F00D20413 /* HistoryViewController.swift */; }; 490425F729AB54B200CDBC60 /* CategoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490425F629AB54B200CDBC60 /* CategoryViewController.swift */; }; 49078E47291BEA2400F556BD /* PocketSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 49078E46291BEA2400F556BD /* PocketSVG */; }; @@ -138,6 +142,9 @@ 0A41EE1A2A63102B0066444C /* dm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dm.pb.swift; sourceTree = ""; }; 0A41EE1B2A63102B0066444C /* dmView.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dmView.pb.swift; sourceTree = ""; }; 27FECFCB2B0B98F400EC6A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + 2806E51F2C15A59E00164C10 /* ReplyDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDetailViewController.swift; sourceTree = ""; }; + 2806E5212C16011B00164C10 /* ReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ReplyCell.swift; path = BilibiliLive/Component/View/ReplyCell.swift; sourceTree = SOURCE_ROOT; }; + 2806E5222C16011B00164C10 /* ReplyCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = ReplyCell.xib; path = BilibiliLive/Component/View/ReplyCell.xib; sourceTree = SOURCE_ROOT; }; 2DBE4C4C2628818F00D20413 /* HistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewController.swift; sourceTree = ""; }; 490425F629AB54B200CDBC60 /* CategoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryViewController.swift; sourceTree = ""; }; 490EC3E6290CC8F8001E00B6 /* RankingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingViewController.swift; sourceTree = ""; }; @@ -317,6 +324,7 @@ 49508E0F2943420100D26812 /* CocoaLumberjack in Frameworks */, 499C75EC293058C9003160FB /* CocoaAsyncSocket in Frameworks */, F927ED9F2610B5C300EAB8E3 /* Kingfisher in Frameworks */, + 2806E51E2C1593A000164C10 /* LookinServer in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -364,6 +372,9 @@ children = ( F9562C91261A0D2200573B74 /* VideoPlayerViewController.swift */, F9EDADD2262AA421007CB99F /* VideoDetailViewController.swift */, + 2806E51F2C15A59E00164C10 /* ReplyDetailViewController.swift */, + 2806E5212C16011B00164C10 /* ReplyCell.swift */, + 2806E5222C16011B00164C10 /* ReplyCell.xib */, 49389D6128AFEA2900B9DAFD /* VideoDanmuProvider.swift */, 498DB1DE291BC24700F95607 /* BMaskProvider.swift */, 49FB8EBF291F4C520045D5DE /* VMaskProvider.swift */, @@ -697,6 +708,7 @@ 49508E0E2943420100D26812 /* CocoaLumberjack */, 49508E102943420100D26812 /* CocoaLumberjackSwift */, 0A41EE182A630FEA0066444C /* SwiftProtobuf */, + 2806E51D2C1593A000164C10 /* LookinServer */, ); productName = BilibiliLive; productReference = F9B57350260F5F7400771ED5 /* BilibiliLive.app */; @@ -740,6 +752,7 @@ 499C76142931A7AE003160FB /* XCRemoteSwiftPackageReference "SwiftyXMLParser" */, 49508E0D2943420100D26812 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */, 0A41EE172A630FEA0066444C /* XCRemoteSwiftPackageReference "swift-protobuf" */, + 2806E51C2C1593A000164C10 /* XCRemoteSwiftPackageReference "LookinServer" */, ); productRefGroup = F9B57351260F5F7400771ED5 /* Products */; projectDirPath = ""; @@ -759,6 +772,7 @@ 49F918722931E927001D3EC3 /* AvTransportScpd.xml in Resources */, F9B5735E260F5F7600771ED5 /* LaunchScreen.storyboard in Resources */, F9B5735B260F5F7600771ED5 /* Assets.xcassets in Resources */, + 2806E5242C16011B00164C10 /* ReplyCell.xib in Resources */, 49F9186E2931E3C9001D3EC3 /* DLNAInfo.xml in Resources */, 27FECFCC2B0B98F400EC6A6D /* Localizable.xcstrings in Resources */, F9B57359260F5F7400771ED5 /* Main.storyboard in Resources */, @@ -801,6 +815,7 @@ 497361082BF1A16600ED213F /* Keys.swift in Sources */, 49E5F85028AF73C500FAA3CE /* BilibiliVideoResourceLoaderDelegate.swift in Sources */, 498CF2A22B63AABE0009793E /* dictionary.c in Sources */, + 2806E5202C15A59E00164C10 /* ReplyDetailViewController.swift in Sources */, 494741C82902C45D005D6885 /* Array+..swift in Sources */, 498CF29E2B63AABE0009793E /* bit_cost.c in Sources */, 498CF29B2B63AABE0009793E /* utf8_util.c in Sources */, @@ -849,6 +864,7 @@ 494741E7290391A7005D6885 /* BLButton.swift in Sources */, F927ED992610AD8D00EAB8E3 /* LiveViewController.swift in Sources */, 490EC3E7290CC8F8001E00B6 /* RankingViewController.swift in Sources */, + 2806E5232C16011B00164C10 /* ReplyCell.swift in Sources */, F927ED732610395300EAB8E3 /* DanmakuCell.swift in Sources */, 498CF2A92B63AABE0009793E /* bit_reader.c in Sources */, 498CF2972B63AABE0009793E /* encode.c in Sources */, @@ -1126,6 +1142,14 @@ minimumVersion = 1.0.0; }; }; + 2806E51C2C1593A000164C10 /* XCRemoteSwiftPackageReference "LookinServer" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/QMUI/LookinServer/"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.2.8; + }; + }; 49078E45291BEA2400F556BD /* XCRemoteSwiftPackageReference "PocketSVG" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pocketsvg/PocketSVG.git"; @@ -1222,6 +1246,11 @@ package = 0A41EE172A630FEA0066444C /* XCRemoteSwiftPackageReference "swift-protobuf" */; productName = SwiftProtobuf; }; + 2806E51D2C1593A000164C10 /* LookinServer */ = { + isa = XCSwiftPackageProductDependency; + package = 2806E51C2C1593A000164C10 /* XCRemoteSwiftPackageReference "LookinServer" */; + productName = LookinServer; + }; 49078E46291BEA2400F556BD /* PocketSVG */ = { isa = XCSwiftPackageProductDependency; package = 49078E45291BEA2400F556BD /* XCRemoteSwiftPackageReference "PocketSVG" */; diff --git a/BilibiliLive.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/BilibiliLive.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b805fee4..323c2904 100644 --- a/BilibiliLive.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/BilibiliLive.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "f89b743e598074e66556cd063dd1258c33483d783163efc65f648349cece8790", "pins" : [ { "identity" : "alamofire", @@ -45,6 +46,15 @@ "version" : "6.3.1" } }, + { + "identity" : "lookinserver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/QMUI/LookinServer/", + "state" : { + "revision" : "e553d1b689d147817dc54ad5c28fcff71e860101", + "version" : "1.2.8" + } + }, { "identity" : "marqueelabel", "kind" : "remoteSourceControl", @@ -118,5 +128,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/BilibiliLive/Base.lproj/Main.storyboard b/BilibiliLive/Base.lproj/Main.storyboard index b4025e69..75731c80 100644 --- a/BilibiliLive/Base.lproj/Main.storyboard +++ b/BilibiliLive/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -646,15 +646,15 @@ - + - + - + @@ -801,6 +801,7 @@ + @@ -936,7 +937,7 @@ - + diff --git a/BilibiliLive/Component/Video/ReplyDetailViewController.swift b/BilibiliLive/Component/Video/ReplyDetailViewController.swift new file mode 100644 index 00000000..c238fc30 --- /dev/null +++ b/BilibiliLive/Component/Video/ReplyDetailViewController.swift @@ -0,0 +1,111 @@ +// +// Created by Yam on 2024/6/9. +// + +import UIKit + +class ReplyDetailViewController: UIViewController { + private var titleLabel: UILabel! + private var replyLabel: UILabel! + private var replyCollectionView: UICollectionView! + + var reply: Replys.Reply + + init(reply: Replys.Reply) { + self.reply = reply + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setUpViews() + replyLabel.text = reply.content.message + } + + // MARK: - Private + + private func setUpViews() { + titleLabel = { + let label = UILabel() + self.view.addSubview(label) + label.font = .boldSystemFont(ofSize: 60) + label.text = "评论" + + label.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.top.equalTo(view.safeAreaLayoutGuide) + } + + return label + }() + + replyLabel = { + let label = UILabel() + self.view.addSubview(label) + label.numberOfLines = 0 + label.font = .preferredFont(forTextStyle: .headline) + + label.snp.makeConstraints { make in + make.top.equalTo(self.titleLabel.snp.bottom).offset(60) + make.leading.equalTo(self.view.snp.leadingMargin) + make.trailing.equalTo(self.view.snp.trailingMargin) + } + + return label + }() + + replyCollectionView = { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.itemSize = CGSize(width: 582, height: 360) + flowLayout.sectionInset = .init(top: 0, left: 60, bottom: 0, right: 60) + flowLayout.minimumLineSpacing = 10 + flowLayout.minimumInteritemSpacing = 10 + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + self.view.addSubview(collectionView) + collectionView.dataSource = self + collectionView.delegate = self + collectionView.register(UINib(nibName: ReplyCell.identifier, bundle: nil), forCellWithReuseIdentifier: ReplyCell.identifier) + + collectionView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.top.equalTo(self.replyLabel.snp.bottom).offset(60) + make.bottom.equalToSuperview() + } + + return collectionView + }() + } +} + +extension ReplyDetailViewController: UICollectionViewDataSource, UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return reply.replies?.count ?? 0 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ReplyCell.identifier, for: indexPath) as? ReplyCell else { + fatalError("cell not found") + } + + guard let reply = reply.replies?[indexPath.row] else { + fatalError("reply not found") + } + + cell.config(replay: reply) + + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let reply = reply.replies?[indexPath.item] else { return } + let detail = ReplyDetailViewController(reply: reply) + present(detail, animated: true) + } +} diff --git a/BilibiliLive/Component/Video/VideoDetailViewController.swift b/BilibiliLive/Component/Video/VideoDetailViewController.swift index f9ef9a3f..0fbf82cd 100644 --- a/BilibiliLive/Component/Video/VideoDetailViewController.swift +++ b/BilibiliLive/Component/Video/VideoDetailViewController.swift @@ -6,6 +6,7 @@ // import AVKit +import Combine import Foundation import UIKit @@ -43,6 +44,7 @@ class VideoDetailViewController: UIViewController { @IBOutlet var pageCollectionView: UICollectionView! @IBOutlet var recommandCollectionView: UICollectionView! @IBOutlet var replysCollectionView: UICollectionView! + @IBOutlet var repliesCollectionViewHeightConstraints: NSLayoutConstraint! @IBOutlet var ugcCollectionView: UICollectionView! @IBOutlet var pageView: UIView! @@ -71,6 +73,8 @@ class VideoDetailViewController: UIViewController { private var allUgcEpisodes = [VideoDetail.Info.UgcSeason.UgcVideoInfo]() + private var subscriptions = [AnyCancellable]() + static func create(aid: Int, cid: Int?, epid: Int? = nil) -> VideoDetailViewController { let vc = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(identifier: String(describing: self)) as! VideoDetailViewController vc.aid = aid @@ -126,6 +130,11 @@ class VideoDetailViewController: UIViewController { focusGuide.bottomAnchor.constraint(equalTo: actionButtonSpaceView.bottomAnchor), ]) focusGuide.preferredFocusEnvironments = [dislikeButton] + + replysCollectionView.publisher(for: \.contentSize).sink { [weak self] newSize in + self?.repliesCollectionViewHeightConstraints.constant = newSize.height + self?.view.setNeedsLayout() + }.store(in: &subscriptions) } override var preferredFocusedView: UIView? { @@ -456,7 +465,7 @@ extension VideoDetailViewController: UICollectionViewDelegate { present(player, animated: true, completion: nil) case replysCollectionView: guard let reply = replys?.replies?[indexPath.item] else { return } - let detail = ContentDetailViewController.createReply(content: reply.content.message) + let detail = ReplyDetailViewController(reply: reply) present(detail, animated: true) case ugcCollectionView: let video = allUgcEpisodes[indexPath.item] @@ -576,18 +585,6 @@ extension VideoDetailViewController { } } -class ReplyCell: UICollectionViewCell { - @IBOutlet var avatarImageView: UIImageView! - @IBOutlet var userNameLabel: UILabel! - @IBOutlet var contenLabel: UILabel! - - func config(replay: Replys.Reply) { - avatarImageView.kf.setImage(with: URL(string: replay.member.avatar), options: [.processor(DownsamplingImageProcessor(size: CGSize(width: 80, height: 80))), .processor(RoundCornerImageProcessor(radius: .widthFraction(0.5))), .cacheSerializer(FormatIndicatedCacheSerializer.png)]) - userNameLabel.text = replay.member.uname - contenLabel.text = replay.content.message - } -} - class RelatedVideoCell: BLMotionCollectionViewCell { let titleLabel = MarqueeLabel() let imageView = UIImageView() diff --git a/BilibiliLive/Component/View/ReplyCell.swift b/BilibiliLive/Component/View/ReplyCell.swift new file mode 100644 index 00000000..67af3bf6 --- /dev/null +++ b/BilibiliLive/Component/View/ReplyCell.swift @@ -0,0 +1,29 @@ +// +// Created by Yam on 2024/6/9. +// + +import Kingfisher +import UIKit + +class ReplyCell: UICollectionViewCell { + class var identifier: String { + return String(describing: Self.self) + } + + @IBOutlet var avatarImageView: UIImageView! + @IBOutlet var userNameLabel: UILabel! + @IBOutlet var contenLabel: UILabel! + + func config(replay: Replys.Reply) { + avatarImageView.kf.setImage( + with: URL(string: replay.member.avatar), + options: [ + .processor(DownsamplingImageProcessor(size: CGSize(width: 80, height: 80))), + .processor(RoundCornerImageProcessor(radius: .widthFraction(0.5))), + .cacheSerializer(FormatIndicatedCacheSerializer.png), + ] + ) + userNameLabel.text = replay.member.uname + contenLabel.text = replay.content.message + } +} diff --git a/BilibiliLive/Component/View/ReplyCell.xib b/BilibiliLive/Component/View/ReplyCell.xib new file mode 100644 index 00000000..4683fe60 --- /dev/null +++ b/BilibiliLive/Component/View/ReplyCell.xib @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BilibiliLive/Request/WebRequest.swift b/BilibiliLive/Request/WebRequest.swift index 431b67ec..86f1fec9 100644 --- a/BilibiliLive/Request/WebRequest.swift +++ b/BilibiliLive/Request/WebRequest.swift @@ -707,6 +707,7 @@ struct Replys: Codable, Hashable { let member: Member let content: Content + let replies: [Reply]? } let replies: [Reply]?