Commit e0ed7074 authored by Alex朱枝文's avatar Alex朱枝文

社区相关功能模块开发

parent 59b41db5
......@@ -254,7 +254,8 @@ NSString *const TUILogoutFailNotification = @"TUILogoutFailNotification";
[NSNotificationCenter.defaultCenter postNotificationName:TUILoginSuccessNotification object:nil];
}
fail:^(int code, NSString *desc) {
self.loginWithInit = NO;
__strong __typeof(weakSelf) strongSelf = weakSelf;
strongSelf.loginWithInit = NO;
if (fail) {
fail(code, desc);
}
......
......@@ -1292,6 +1292,14 @@
04D8FFB62DAE489A00703C75 /* YHVisitHKAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D8FFB52DAE489A00703C75 /* YHVisitHKAlertView.swift */; };
04D8FFB82DB0D50B00703C75 /* YHGalaxyNewsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D8FFB72DB0D50B00703C75 /* YHGalaxyNewsListViewController.swift */; };
04D8FFBA2DB0D95A00703C75 /* YHGalaxyNewsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D8FFB92DB0D95A00703C75 /* YHGalaxyNewsCell.swift */; };
04E0D3C82E866A6300F1824B /* YHCirclePhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E0D3C72E866A6300F1824B /* YHCirclePhotoCell.swift */; };
04E0D3CA2E866A9800F1824B /* YHCircleAddPhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E0D3C92E866A9800F1824B /* YHCircleAddPhotoCell.swift */; };
04E0D3CC2E877D4D00F1824B /* YHMediaUploadSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E0D3CB2E877D4D00F1824B /* YHMediaUploadSheetView.swift */; };
04E0D3CE2E87980E00F1824B /* YHCircleMediaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E0D3CD2E87980D00F1824B /* YHCircleMediaCell.swift */; };
04E0D3D02E87C5C700F1824B /* YHMediaBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E0D3CF2E87C5C700F1824B /* YHMediaBrowserViewController.swift */; };
04E0D3D22E87CCB300F1824B /* YHVideoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E0D3D12E87CCB300F1824B /* YHVideoCell.swift */; };
04E0D3D42E87CF3200F1824B /* YHPreviewMediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E0D3D32E87CF3200F1824B /* YHPreviewMediaItem.swift */; };
04E0D3D62E87CF7400F1824B /* YHSelectMediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E0D3D52E87CF7400F1824B /* YHSelectMediaItem.swift */; };
04E4CF3E2D5C6D32004D4013 /* YHCountryMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E4CF3D2D5C6D32004D4013 /* YHCountryMessageView.swift */; };
04E4CF402D5C83AE004D4013 /* YHSelectPhoneCountryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E4CF3F2D5C83AE004D4013 /* YHSelectPhoneCountryViewController.swift */; };
04E507D62D6EE856005F758B /* YHUserLevelAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E507D52D6EE856005F758B /* YHUserLevelAlertView.swift */; };
......@@ -2646,6 +2654,14 @@
04D8FFB52DAE489A00703C75 /* YHVisitHKAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHVisitHKAlertView.swift; sourceTree = "<group>"; };
04D8FFB72DB0D50B00703C75 /* YHGalaxyNewsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHGalaxyNewsListViewController.swift; sourceTree = "<group>"; };
04D8FFB92DB0D95A00703C75 /* YHGalaxyNewsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHGalaxyNewsCell.swift; sourceTree = "<group>"; };
04E0D3C72E866A6300F1824B /* YHCirclePhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHCirclePhotoCell.swift; sourceTree = "<group>"; };
04E0D3C92E866A9800F1824B /* YHCircleAddPhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHCircleAddPhotoCell.swift; sourceTree = "<group>"; };
04E0D3CB2E877D4D00F1824B /* YHMediaUploadSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHMediaUploadSheetView.swift; sourceTree = "<group>"; };
04E0D3CD2E87980D00F1824B /* YHCircleMediaCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHCircleMediaCell.swift; sourceTree = "<group>"; };
04E0D3CF2E87C5C700F1824B /* YHMediaBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHMediaBrowserViewController.swift; sourceTree = "<group>"; };
04E0D3D12E87CCB300F1824B /* YHVideoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHVideoCell.swift; sourceTree = "<group>"; };
04E0D3D32E87CF3200F1824B /* YHPreviewMediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHPreviewMediaItem.swift; sourceTree = "<group>"; };
04E0D3D52E87CF7400F1824B /* YHSelectMediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHSelectMediaItem.swift; sourceTree = "<group>"; };
04E4CF3D2D5C6D32004D4013 /* YHCountryMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHCountryMessageView.swift; sourceTree = "<group>"; };
04E4CF3F2D5C83AE004D4013 /* YHSelectPhoneCountryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHSelectPhoneCountryViewController.swift; sourceTree = "<group>"; };
04E507D52D6EE856005F758B /* YHUserLevelAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YHUserLevelAlertView.swift; sourceTree = "<group>"; };
......@@ -6108,9 +6124,13 @@
045C0F002D12CA5E00BD2DC0 /* PictureReview(图片预览) */ = {
isa = PBXGroup;
children = (
04E0D3D12E87CCB300F1824B /* YHVideoCell.swift */,
045C0EFE2D12CA5E00BD2DC0 /* YHLongtapPictureSheetView.swift */,
045C0EFF2D12CA5E00BD2DC0 /* YHPictureReviewManager.swift */,
04D8FFB12DA5007A00703C75 /* YHPictureBrowserViewController.swift */,
04E0D3CF2E87C5C700F1824B /* YHMediaBrowserViewController.swift */,
04E0D3D32E87CF3200F1824B /* YHPreviewMediaItem.swift */,
04E0D3D52E87CF7400F1824B /* YHSelectMediaItem.swift */,
);
path = "PictureReview(图片预览)";
sourceTree = "<group>";
......@@ -6963,7 +6983,11 @@
04D4EC2A2E839B3000B0329B /* V */ = {
isa = PBXGroup;
children = (
04E0D3CD2E87980D00F1824B /* YHCircleMediaCell.swift */,
04E0D3CB2E877D4D00F1824B /* YHMediaUploadSheetView.swift */,
04D4EC4D2E84F22500B0329B /* YHCircleHeaderReusableView.swift */,
04E0D3C92E866A9800F1824B /* YHCircleAddPhotoCell.swift */,
04E0D3C72E866A6300F1824B /* YHCirclePhotoCell.swift */,
04D4EC452E83D11500B0329B /* YHCircleCollectionViewCell.swift */,
);
path = V;
......@@ -7898,6 +7922,7 @@
045C10E22D12CA5F00BD2DC0 /* YHInvitationWithGiftsDetailView.swift in Sources */,
045C10E32D12CA5F00BD2DC0 /* YHRiskWarningCell.swift in Sources */,
045C10E42D12CA5F00BD2DC0 /* YHFileRenameInputView.swift in Sources */,
04E0D3CA2E866A9800F1824B /* YHCircleAddPhotoCell.swift in Sources */,
045C10E52D12CA5F00BD2DC0 /* YHLookResignResultFootView.swift in Sources */,
04AE204E2D1941FC00891D24 /* YHGCCertificateListContainerVC.swift in Sources */,
045C10E62D12CA5F00BD2DC0 /* YHInvitationWithGiftsViewController.swift in Sources */,
......@@ -8031,6 +8056,7 @@
045C11522D12CA5F00BD2DC0 /* YHResignAppointSubmitTipsView.swift in Sources */,
045C11532D12CA5F00BD2DC0 /* YHLookResignResultStateOneTableViewCell.swift in Sources */,
045C11542D12CA5F00BD2DC0 /* YHWorkExperienceCompanyModel.swift in Sources */,
04E0D3CE2E87980E00F1824B /* YHCircleMediaCell.swift in Sources */,
045C11552D12CA5F00BD2DC0 /* YHResignInfoItemView.swift in Sources */,
04AE203B2D13C01B00891D24 /* YHGCEducationInfoListVC.swift in Sources */,
045C11562D12CA5F00BD2DC0 /* YHHUDSquareBaseView.swift in Sources */,
......@@ -8110,7 +8136,9 @@
045C11942D12CA5F00BD2DC0 /* YHServiceBannerView.swift in Sources */,
045C11952D12CA5F00BD2DC0 /* YHFourKingViewController.swift in Sources */,
045C11962D12CA5F00BD2DC0 /* YHActivityTipsItemView.swift in Sources */,
04E0D3D22E87CCB300F1824B /* YHVideoCell.swift in Sources */,
045C11972D12CA5F00BD2DC0 /* YHHUDRotatingImageView.swift in Sources */,
04E0D3D62E87CF7400F1824B /* YHSelectMediaItem.swift in Sources */,
04307B6C2D1A547C00ED8E8D /* YHIncomeDateTillNowCell.swift in Sources */,
045C11982D12CA5F00BD2DC0 /* YHCerAppointViewModel.swift in Sources */,
045C11992D12CA5F00BD2DC0 /* YHPayMembersCell.swift in Sources */,
......@@ -8210,6 +8238,7 @@
045C11EA2D12CA5F00BD2DC0 /* YHMySignatureDetailModel.swift in Sources */,
045C11EB2D12CA5F00BD2DC0 /* YHPlayerControlView.swift in Sources */,
04307B6E2D1A5F4200ED8E8D /* YHIncomeUploadWorkIDCell.swift in Sources */,
04E0D3D02E87C5C700F1824B /* YHMediaBrowserViewController.swift in Sources */,
045C11EC2D12CA5F00BD2DC0 /* YHMyDocumentsListViewController.swift in Sources */,
045C11ED2D12CA5F00BD2DC0 /* YHCertificateEntryBottomView.swift in Sources */,
045C11EE2D12CA5F00BD2DC0 /* YHAppointItem.swift in Sources */,
......@@ -8239,6 +8268,7 @@
045C12002D12CA5F00BD2DC0 /* YHPrincipleApprovedAlertView.swift in Sources */,
045C12012D12CA5F00BD2DC0 /* YHSchemeTableHeadView.swift in Sources */,
045C12022D12CA5F00BD2DC0 /* YHResignDocumentUploadStatus.swift in Sources */,
04E0D3D42E87CF3200F1824B /* YHPreviewMediaItem.swift in Sources */,
045C12032D12CA5F00BD2DC0 /* YHActivityListCell.swift in Sources */,
045C12042D12CA5F00BD2DC0 /* YHTravelCertificateTipsCell.swift in Sources */,
045C12052D12CA5F00BD2DC0 /* YHVisaRenewalPayMethodQrcodeCell.swift in Sources */,
......@@ -8256,6 +8286,7 @@
045C12102D12CA5F00BD2DC0 /* YHBaseViewController.swift in Sources */,
045C12112D12CA5F00BD2DC0 /* YHInfoItemOptionView.swift in Sources */,
045C12122D12CA5F00BD2DC0 /* YHPreviewInfoWorkSummaryView.swift in Sources */,
04E0D3CC2E877D4D00F1824B /* YHMediaUploadSheetView.swift in Sources */,
045C12132D12CA5F00BD2DC0 /* YHVisaRenewalPayOccupyingSpaceCell.swift in Sources */,
045C12142D12CA5F00BD2DC0 /* YHCheckEamilAlertView.swift in Sources */,
045C12152D12CA5F00BD2DC0 /* YHResignAppointedScheduleRiskTipsView.swift in Sources */,
......@@ -8650,6 +8681,7 @@
045C13562D12CA5F00BD2DC0 /* YHOtherPickerView.swift in Sources */,
045C13572D12CA5F00BD2DC0 /* YHLivePlayerViewController.swift in Sources */,
045C13582D12CA5F00BD2DC0 /* YHAIChatInputShadowView.swift in Sources */,
04E0D3C82E866A6300F1824B /* YHCirclePhotoCell.swift in Sources */,
0411CF002D1A805A00644D35 /* YHGCMySignatureListViewModel.swift in Sources */,
045C13592D12CA5F00BD2DC0 /* YHMyFileListNoneCell.swift in Sources */,
045C135A2D12CA5F00BD2DC0 /* YHCardButton.swift in Sources */,
......
......@@ -39,7 +39,7 @@ class YHNavigationController: UINavigationController {
if let lastVC = viewControllers.last {
let className = String(describing: type(of: lastVC))
if !className.hasPrefix("TUI") { // 模糊匹配类名,使得腾讯IM页面不用隐藏NavigationBar
var needAnimated = false
var needAnimated = animated
let lastSecondCount = viewControllers.count - 2
if lastSecondCount >= 0 {
let lastSecondVC = viewControllers[lastSecondCount]
......
......@@ -7,48 +7,459 @@
//
import UIKit
import SnapKit
class YHCirclePublishViewController: YHBaseViewController {
private let marginX: CGFloat = 24
private let itemSpace: CGFloat = 8
var completion: (() -> Void)?
// MARK: - Navigation Items
private lazy var leftBarItem: UIBarButtonItem = {
let item = UIBarButtonItem(image: UIImage(named: "nav_black_24")?.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(cancelButtonTapped))
return item
}()
private lazy var rightButton: UIButton = {
let button = UIButton(type: .custom)
button.frame = CGRect(x: 0, y: 0, width: 56, height: 32)
button.layer.cornerRadius = 16
button.clipsToBounds = true
button.setTitle("发布", for: .normal)
button.titleLabel?.font = UIFont.PFSC_M(ofSize: 12)
button.setTitleColor(UIColor.brandGrayColor0, for: .normal)
button.setTitleColor(UIColor.brandGrayColor0, for: .disabled)
button.isEnabled = false
button.backgroundColor = UIColor.brandGrayColor4
button.addTarget(self, action: #selector(publishButtonTapped), for: .touchUpInside)
return button
}()
// MARK: - UI Components
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = .white
scrollView.showsVerticalScrollIndicator = false
return scrollView
}()
private lazy var contentView: UIView = {
let view = UIView()
view.backgroundColor = .white
return view
}()
private lazy var userInfoView: UIView = {
let view = UIView()
view.backgroundColor = .white
return view
}()
private lazy var avatarImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "people_head_default") // 设置默认头像
imageView.layer.cornerRadius = 17
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
return imageView
}()
private lazy var usernameLabel: UILabel = {
let label = UILabel()
label.text = "Monica杨晓丽"
label.font = UIFont.PFSC_R(ofSize: 14)
label.textColor = .brandGrayColor8
return label
}()
private lazy var subtitleLabel: UILabel = {
let label = UILabel()
label.text = "董事长"
label.font = UIFont.PFSC_R(ofSize: 11)
label.textColor = UIColor.brandGrayColor6
return label
}()
private lazy var textView: UITextView = {
let textView = UITextView()
textView.font = UIFont.PFSC_B(ofSize: 17)
textView.textColor = .brandGrayColor8
textView.backgroundColor = .clear
textView.delegate = self
textView.textContainer.lineFragmentPadding = 0
textView.textContainerInset = UIEdgeInsets.zero
textView.showsVerticalScrollIndicator = false
return textView
}()
private lazy var placeholderLabel: UILabel = {
let label = UILabel()
label.text = "添加标题"
label.font = UIFont.PFSC_R(ofSize: 17)
label.textColor = UIColor.brandGrayColor5
return label
}()
private lazy var detailTextView: UITextView = {
let textView = UITextView()
textView.font = UIFont.PFSC_R(ofSize: 14)
textView.textColor = UIColor.brandGrayColor8
textView.backgroundColor = .clear
textView.delegate = self
textView.textContainer.lineFragmentPadding = 0
textView.textContainerInset = UIEdgeInsets.zero
textView.showsVerticalScrollIndicator = false
return textView
}()
private lazy var detailPlaceholderLabel: UILabel = {
let label = UILabel()
label.text = "分享生活,表达想法,随时随地..."
label.font = UIFont.PFSC_R(ofSize: 14)
label.textColor = UIColor.brandGrayColor5
label.numberOfLines = 0
return label
}()
private lazy var mediaCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = itemSpace
layout.minimumLineSpacing = itemSpace
let itemWidth = getItemWidth()
layout.itemSize = CGSize(width: itemWidth, height: itemWidth)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .white
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(YHCircleMediaCell.self, forCellWithReuseIdentifier: "YHCircleMediaCell")
collectionView.register(YHCircleAddPhotoCell.self, forCellWithReuseIdentifier: "YHCircleAddPhotoCell")
collectionView.showsVerticalScrollIndicator = false
return collectionView
}()
private lazy var divideLine: UIView = {
let view = UIView()
view.backgroundColor = UIColor.brandGrayColor3
return view
}()
// MARK: - Properties
private var mediaItems: [YHSelectMediaItem] = []
private let maxMediaCount = 9
override func viewDidLoad() {
super.viewDidLoad()
setupNav()
setupUI()
setupConstraints()
setupNotifications()
}
deinit {
NotificationCenter.default.removeObserver(self)
printLog("YHCirclePublishViewController deinit")
}
// MARK: - Setup Methods
private func setupNav() {
gk_navTitle = "发布动态"
gk_navLeftBarButtonItem = leftBarItem
gk_navRightBarButtonItem = UIBarButtonItem(customView: rightButton)
gk_navItemRightSpace = 16
gk_navItemLeftSpace = 16
gk_navBackgroundColor = .white
}
private func setupUI() {
view.backgroundColor = .white
gk_navigationBar.isHidden = true
title = "发布动态"
navigationItem.leftBarButtonItem = UIBarButtonItem(
title: "取消",
style: .plain,
target: self,
action: #selector(cancelButtonTapped)
)
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "发布",
style: .done,
target: self,
action: #selector(publishButtonTapped)
)
let label = UILabel()
label.text = "发布页面"
label.textAlignment = .center
label.textColor = .gray
view.addSubview(label)
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(userInfoView)
userInfoView.addSubview(avatarImageView)
userInfoView.addSubview(usernameLabel)
userInfoView.addSubview(subtitleLabel)
contentView.addSubview(textView)
contentView.addSubview(placeholderLabel)
contentView.addSubview(divideLine)
contentView.addSubview(detailTextView)
contentView.addSubview(detailPlaceholderLabel)
contentView.addSubview(mediaCollectionView)
}
private func setupConstraints() {
scrollView.snp.makeConstraints { make in
make.top.equalTo(k_Height_NavigationtBarAndStatuBar)
make.left.right.bottom.equalToSuperview()
}
contentView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.width.equalTo(view.snp.width)
}
userInfoView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20)
make.left.right.equalToSuperview().inset(marginX)
}
avatarImageView.snp.makeConstraints { make in
make.left.top.equalToSuperview()
make.width.height.equalTo(34)
make.bottom.equalToSuperview()
}
usernameLabel.snp.makeConstraints { make in
make.left.equalTo(avatarImageView.snp.right).offset(8)
make.top.equalTo(avatarImageView.snp.top)
make.right.equalToSuperview()
}
label.snp.makeConstraints { make in
make.center.equalToSuperview()
subtitleLabel.snp.makeConstraints { make in
make.left.equalTo(usernameLabel)
make.bottom.equalTo(avatarImageView.snp.bottom)
make.right.equalToSuperview()
}
textView.snp.makeConstraints { make in
make.top.equalTo(userInfoView.snp.bottom).offset(24)
make.left.right.equalToSuperview().inset(marginX)
make.height.greaterThanOrEqualTo(24)
}
placeholderLabel.snp.makeConstraints { make in
make.top.left.equalTo(textView)
}
divideLine.snp.makeConstraints { make in
make.top.equalTo(textView.snp.bottom).offset(16)
make.left.right.equalTo(textView)
make.height.equalTo(1)
}
detailTextView.snp.makeConstraints { make in
make.top.equalTo(divideLine.snp.bottom).offset(16)
make.left.right.equalToSuperview().inset(marginX)
make.height.greaterThanOrEqualTo(20)
}
detailPlaceholderLabel.snp.makeConstraints { make in
make.top.left.equalTo(detailTextView)
}
mediaCollectionView.snp.makeConstraints { make in
make.top.equalTo(detailTextView.snp.bottom).offset(96)
make.left.right.equalToSuperview().inset(marginX)
make.height.equalTo(200)
make.bottom.equalToSuperview().offset(-30)
}
updateCollectionViewHeight()
}
private func setupNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
private func getItemWidth() -> CGFloat {
let itemWidth = (UIScreen.main.bounds.width - marginX * 2 - itemSpace * 2) / 3
return itemWidth
}
// MARK: - Actions
@objc private func cancelButtonTapped() {
dismiss(animated: true)
if hasContent() {
showCancelAlert()
} else {
dismiss(animated: true)
}
}
@objc private func publishButtonTapped() {
completion?()
dismiss(animated: true)
// 发布逻辑
showPublishingAlert()
// 模拟发布延迟
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.completion?()
self.dismiss(animated: true)
}
}
// MARK: - Keyboard Handling
@objc private func keyboardWillShow(notification: NSNotification) {
guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
let keyboardHeight = keyboardFrame.height
UIView.animate(withDuration: duration) {
self.scrollView.contentInset.bottom = keyboardHeight
self.scrollView.verticalScrollIndicatorInsets.bottom = keyboardHeight
}
}
@objc private func keyboardWillHide(notification: NSNotification) {
guard let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
UIView.animate(withDuration: duration) {
self.scrollView.contentInset.bottom = 0
self.scrollView.verticalScrollIndicatorInsets.bottom = 0
}
}
// MARK: - Helper Methods
private func hasContent() -> Bool {
return !textView.text.isEmpty || !detailTextView.text.isEmpty || !mediaItems.isEmpty
}
private func updatePublishButton() {
let hasContent = hasContent()
rightButton.isEnabled = hasContent
rightButton.backgroundColor = hasContent ? UIColor.brandGrayColor8 : UIColor.brandGrayColor4
}
private func updateCollectionViewHeight() {
let rows = ceil(Double(mediaItems.count + 1) / 3.0)
let itemWidth = getItemWidth()
let height = CGFloat(rows) * itemWidth + CGFloat(max(0, rows - 1)) * 8
mediaCollectionView.snp.updateConstraints { make in
make.height.equalTo(max(height, itemWidth))
}
}
private func showCancelAlert() {
let alert = UIAlertController(title: "确定要离开吗?", message: "离开后内容将不会保存", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确定", style: .destructive) { _ in
self.dismiss(animated: true)
})
present(alert, animated: true)
}
private func showPublishingAlert() {
let alert = UIAlertController(title: "发布中...", message: nil, preferredStyle: .alert)
present(alert, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
alert.dismiss(animated: true)
}
}
}
// MARK: - UITextViewDelegate
extension YHCirclePublishViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
if textView == self.textView {
placeholderLabel.isHidden = !textView.text.isEmpty
} else if textView == detailTextView {
detailPlaceholderLabel.isHidden = !textView.text.isEmpty
}
updatePublishButton()
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if textView == self.textView {
let currentText = textView.text ?? ""
guard let stringRange = Range(range, in: currentText) else { return false }
let updatedText = currentText.replacingCharacters(in: stringRange, with: text)
return updatedText.count <= 100
}
return true
}
}
// MARK: - UICollectionViewDataSource & Delegate
extension YHCirclePublishViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return min(mediaItems.count + 1, maxMediaCount)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item < mediaItems.count {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "YHCircleMediaCell", for: indexPath) as? YHCircleMediaCell {
cell.configure(with: mediaItems[indexPath.item])
cell.deleteCallback = { [weak self] in
self?.removeMedia(at: indexPath.item)
}
return cell
}
} else {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "YHCircleAddPhotoCell", for: indexPath) as? YHCircleAddPhotoCell {
return cell
}
}
return UICollectionViewCell()
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.item >= mediaItems.count {
showMediaUploadSheet()
} else {
// 预览本地选中的媒体
previewLocalMedia(at: indexPath.item)
}
}
// 预览本地选择的媒体文件
private func previewLocalMedia(at index: Int) {
YHPictureReviewManager.shared.showLocalMedia(
curIndex: index,
selectMediaItems: self.mediaItems
) { [weak self] deletedIndex in
// 删除回调处理
guard let self = self else { return }
DispatchQueue.main.async {
// 从本地媒体数组中删除对应项
if deletedIndex < self.mediaItems.count {
self.mediaItems.remove(at: deletedIndex)
// 刷新集合视图
self.mediaCollectionView.reloadData()
self.updateCollectionViewHeight()
self.updatePublishButton()
printLog("已从发布页面删除媒体项,剩余: \(self.mediaItems.count)")
}
}
}
}
private func removeMedia(at index: Int) {
mediaItems.remove(at: index)
mediaCollectionView.reloadData()
updateCollectionViewHeight()
updatePublishButton()
}
private func showMediaUploadSheet() {
YHMediaUploadSheetView.sheetView().show { [weak self] selectedMediaItems in
guard let self = self else { return }
// 检查是否超出最大数量限制
let remainingSlots = maxMediaCount - mediaItems.count
let itemsToAdd = Array(selectedMediaItems.prefix(remainingSlots))
if itemsToAdd.count < selectedMediaItems.count {
YHHUD.flash(message: "最多只能选择\(maxMediaCount)个媒体文件")
}
// 添加新选择的媒体项
mediaItems.append(contentsOf: itemsToAdd)
mediaCollectionView.reloadData()
updateCollectionViewHeight()
updatePublishButton()
print("获得 \(itemsToAdd.count) 个媒体文件")
}
}
}
//
// YHCircleAddPhotoCell.swift
// galaxy
//
// Created by alexzzw on 2025/9/26.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
class YHCircleAddPhotoCell: UICollectionViewCell {
private lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.systemGray6
return view
}()
private lazy var plusImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "circle_plus_icon")
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
contentView.addSubview(containerView)
containerView.addSubview(plusImageView)
containerView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
plusImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(40)
}
}
}
//
// YHCircleMediaCell.swift
// galaxy
//
// Created by alexzzw on 2025/9/27.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
import AVFoundation
class YHCircleMediaCell: UICollectionViewCell {
var deleteCallback: (() -> Void)?
// MARK: - UI Components
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.backgroundColor = UIColor.brandGrayColor2
return imageView
}()
private lazy var deleteButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
button.layer.cornerRadius = 10
button.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
button.isHidden = true
return button
}()
private lazy var playIcon: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "circle_play_icon")
imageView.isHidden = true
return imageView
}()
private lazy var durationLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12, weight: .medium)
label.textColor = .white
label.backgroundColor = UIColor.black.withAlphaComponent(0.5)
label.textAlignment = .center
label.layer.cornerRadius = 8
label.clipsToBounds = true
label.isHidden = true
return label
}()
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Setup UI
private func setupUI() {
contentView.addSubview(imageView)
contentView.addSubview(playIcon)
contentView.addSubview(durationLabel)
contentView.addSubview(deleteButton)
imageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
playIcon.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(40)
}
durationLabel.snp.makeConstraints { make in
make.bottom.right.equalToSuperview().inset(8)
make.height.equalTo(20)
make.width.greaterThanOrEqualTo(35)
}
deleteButton.snp.makeConstraints { make in
make.top.right.equalToSuperview().inset(4)
make.width.height.equalTo(20)
}
}
// MARK: - Configure Cell
func configure(with mediaItem: YHSelectMediaItem) {
resetCell()
switch mediaItem.type {
case .image:
configureForImage(mediaItem)
case .video:
configureForVideo(mediaItem)
}
}
private func resetCell() {
imageView.image = nil
playIcon.isHidden = true
durationLabel.isHidden = true
durationLabel.text = nil
}
private func configureForImage(_ mediaItem: YHSelectMediaItem) {
imageView.image = mediaItem.image
playIcon.isHidden = true
durationLabel.isHidden = true
}
private func configureForVideo(_ mediaItem: YHSelectMediaItem) {
// 显示视频相关UI
playIcon.isHidden = false
// 生成视频缩略图
if let videoURL = mediaItem.videoURL {
generateVideoThumbnail(from: videoURL) { [weak self] thumbnail in
DispatchQueue.main.async {
self?.imageView.image = thumbnail
}
}
}
// 显示视频时长
// if let duration = mediaItem.duration {
// durationLabel.text = formatDuration(duration)
// durationLabel.isHidden = false
// } else if let videoURL = mediaItem.videoURL {
// // 如果没有时长信息,尝试获取
// getVideoDuration(from: videoURL) { [weak self] duration in
// DispatchQueue.main.async {
// if duration > 0 {
// self?.durationLabel.text = self?.formatDuration(duration)
// self?.durationLabel.isHidden = false
// }
// }
// }
// }
}
// MARK: - Video Helpers
private func generateVideoThumbnail(from url: URL, completion: @escaping (UIImage?) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let asset = AVAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
imageGenerator.appliesPreferredTrackTransform = true
imageGenerator.maximumSize = CGSize(width: 300, height: 300)
let time = CMTime(seconds: 0.0, preferredTimescale: 600)
do {
let cgImage = try imageGenerator.copyCGImage(at: time, actualTime: nil)
let thumbnail = UIImage(cgImage: cgImage)
completion(thumbnail)
} catch {
print("生成视频缩略图失败: \(error)")
completion(nil)
}
}
}
private func getVideoDuration(from url: URL, completion: @escaping (TimeInterval) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let asset = AVAsset(url: url)
let duration = CMTimeGetSeconds(asset.duration)
completion(duration.isNaN ? 0 : duration)
}
}
private func formatDuration(_ duration: TimeInterval) -> String {
let totalSeconds = Int(duration)
let minutes = totalSeconds / 60
let seconds = totalSeconds % 60
if minutes > 0 {
return String(format: "%d:%02d", minutes, seconds)
} else {
return String(format: "0:%02d", seconds)
}
}
// MARK: - Actions
@objc private func deleteButtonTapped() {
deleteCallback?()
}
}
//
// YHCirclePhotoCell.swift
// galaxy
//
// Created by alexzzw on 2025/9/26.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
// MARK: - Custom Cells
class YHCirclePhotoCell: UICollectionViewCell {
var deleteCallback: (() -> Void)?
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
return imageView
}()
private lazy var deleteButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
button.layer.cornerRadius = 10
button.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
button.isHidden = true
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
contentView.addSubview(imageView)
contentView.addSubview(deleteButton)
imageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
deleteButton.snp.makeConstraints { make in
make.top.right.equalToSuperview().inset(4)
make.width.height.equalTo(20)
}
}
func configure(with image: UIImage) {
imageView.image = image
}
@objc private func deleteButtonTapped() {
deleteCallback?()
}
}
//
// YHMediaUploadSheetView.swift
// galaxy
//
// Created by alexzzw on 2025/9/27.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
/*
【Usage】
YHMediaUploadSheetView.sheetView().show { [weak self] mediaItems in
guard let self = self else { return }
print("获得 \(mediaItems.count) 个媒体文件")
// 处理选中的媒体文件
}
*/
import UIKit
import Photos
import PhotosUI
import AVFoundation
import MobileCoreServices
import SnapKit
enum YHMediaUploadType: Int {
case camera = 1 // 拍照/录像
case photoLibrary = 2 // 相册选择
case cancel = 3 // 取消
}
class YHMediaUploadItem {
var type: YHMediaUploadType
var title: String
init(type: YHMediaUploadType, title: String) {
self.type = type
self.title = title
}
}
class YHMediaUploadSheetView: UIView {
// 决定最终媒体选择数量最大值
var maxSelectCount: Int = 9
var uploadTypeArr = [
YHMediaUploadItem(type: .camera, title: "拍照"),
YHMediaUploadItem(type: .photoLibrary, title: "从手机相册选择"),
YHMediaUploadItem(type: .cancel, title: "取消")
]
// 上传媒体回调
var uploadMediaBlock: (([YHSelectMediaItem]) -> Void)?
private var needSelectVideo = false
lazy var blackMaskView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hex: 0x0F1214, alpha: 0.5)
let tap = UITapGestureRecognizer(target: self, action: #selector(dismiss))
view.addGestureRecognizer(tap)
return view
}()
lazy var whiteContentView: UIView = {
let view = UIView()
view.backgroundColor = .white
return view
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
if #available(iOS 11.0, *) {
tableView.contentInsetAdjustmentBehavior = .never
}
tableView.showsVerticalScrollIndicator = false
tableView.separatorStyle = .none
tableView.delegate = self
tableView.dataSource = self
tableView.backgroundColor = .white
tableView.isScrollEnabled = false
tableView.register(YHMediaUploadTypeCell.self, forCellReuseIdentifier: YHMediaUploadTypeCell.cellReuseIdentifier)
return tableView
}()
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
createUI()
}
static func sheetView() -> YHMediaUploadSheetView {
let view = YHMediaUploadSheetView(frame: UIScreen.main.bounds)
return view
}
func createUI() {
self.addSubview(blackMaskView)
self.addSubview(whiteContentView)
whiteContentView.addSubview(tableView)
blackMaskView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
// 计算内容高度:2个section,第一个section 2行,第二个section 1行,加上section间距和安全区域
let contentHeight = 54.0 * 3 + 8.0 + k_Height_safeAreaInsetsBottom()
whiteContentView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.height.equalTo(contentHeight)
}
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
extension YHMediaUploadSheetView {
func show(needSelectVideo: Bool = false, completion: @escaping ([YHSelectMediaItem]) -> Void) {
self.needSelectVideo = needSelectVideo
self.uploadMediaBlock = completion
UIApplication.shared.yhKeyWindow()?.addSubview(self)
}
@objc func dismiss() {
self.removeFromSuperview()
}
}
// MARK: - TableView DataSource & Delegate
extension YHMediaUploadSheetView: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 2 // 拍照 + 从手机相册选择
} else {
return 1 // 取消
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: YHMediaUploadTypeCell.cellReuseIdentifier, for: indexPath) as? YHMediaUploadTypeCell else {
return UITableViewCell()
}
var itemIndex = 0
if indexPath.section == 0 {
itemIndex = indexPath.row
} else {
itemIndex = 2 // 取消按钮
}
if itemIndex < uploadTypeArr.count {
cell.item = uploadTypeArr[itemIndex]
// 第一个section的第一行显示分割线
if indexPath.section == 0 && indexPath.row == 0 {
cell.showSeparator = true
} else {
cell.showSeparator = false
}
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
var itemIndex = 0
if indexPath.section == 0 {
itemIndex = indexPath.row
} else {
itemIndex = 2 // 取消按钮
}
if itemIndex < uploadTypeArr.count {
let operationItem = uploadTypeArr[itemIndex]
if operationItem.type == .cancel {
dismiss()
} else if operationItem.type == .photoLibrary {
selectMediaFromPhotoLibrary()
} else if operationItem.type == .camera {
showCameraWithBothModes()
}
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 54.0
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? 0.01 : 8.0
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 0.01
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let view = UIView()
view.backgroundColor = nil
return view
} else {
let view = UIView()
view.backgroundColor = UIColor.brandGrayColor3
return view
}
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return UIView()
}
}
// MARK: - Camera & Photo Library
extension YHMediaUploadSheetView {
// 显示相机(拍照和录像在同一界面)
func showCameraWithBothModes() {
checkCameraPermission { [weak self] in
self?.presentCameraWithBothModes()
}
}
// 从相册选择
func selectMediaFromPhotoLibrary() {
checkPhotoLibraryPermission { [weak self] in
self?.presentPhotoLibraryPicker()
}
}
// 展示相机(同时支持拍照和录像)
func presentCameraWithBothModes() {
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
YHHUD.flash(message: "设备不支持相机功能")
return
}
let picker = UIImagePickerController()
picker.delegate = self
picker.sourceType = .camera
// 同时支持拍照和录像
var mediaTypes: [String] = []
// 检查是否支持拍照和录像
if let availableTypes = UIImagePickerController.availableMediaTypes(for: .camera) {
if availableTypes.contains(kUTTypeImage as String) {
mediaTypes.append(kUTTypeImage as String)
}
if needSelectVideo, availableTypes.contains(kUTTypeMovie as String) {
mediaTypes.append(kUTTypeMovie as String)
}
}
guard !mediaTypes.isEmpty else {
YHHUD.flash(message: "设备不支持拍照或录像功能")
return
}
picker.mediaTypes = mediaTypes
// 设置视频录制参数
picker.videoQuality = .typeMedium
picker.videoMaximumDuration = 60.0 // 60秒
// 默认是拍照模式,用户可以在界面中切换到录像
picker.cameraCaptureMode = .photo
picker.allowsEditing = true
UIViewController.current?.present(picker, animated: true)
}
// 展示相册选择器
func presentPhotoLibraryPicker() {
if #available(iOS 14.0, *) {
// iOS 14+ 使用 PHPickerViewController,支持多选
var configuration = PHPickerConfiguration()
configuration.selectionLimit = maxSelectCount
// 同时支持图片和视频
configuration.filter = needSelectVideo ? .any(of: [.images, .videos]) : .any(of: [.images])
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
UIViewController.current?.present(picker, animated: true)
} else {
// iOS 14以下使用 UIImagePickerController,只能单选
if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .photoLibrary
imagePicker.mediaTypes = needSelectVideo ? [kUTTypeImage as String, kUTTypeMovie as String] : [kUTTypeImage as String]
UIViewController.current?.present(imagePicker, animated: true)
}
}
}
}
// MARK: - 权限检查
extension YHMediaUploadSheetView {
func checkCameraPermission(completion: @escaping () -> Void) {
let cameraAuthStatus = AVCaptureDevice.authorizationStatus(for: .video)
switch cameraAuthStatus {
case .authorized:
completion()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
if granted {
completion()
} else {
YHHUD.flash(message: "需要相机权限才能拍照")
}
}
}
case .denied, .restricted:
YHHUD.flash(message: "请在设置中打开相机权限")
@unknown default:
YHHUD.flash(message: "无法获取相机权限")
}
}
func checkPhotoLibraryPermission(completion: @escaping () -> Void) {
let photoAuthStatus = PHPhotoLibrary.authorizationStatus()
switch photoAuthStatus {
case .authorized, .limited:
completion()
case .notDetermined:
PHPhotoLibrary.requestAuthorization { status in
DispatchQueue.main.async {
if status == .authorized || status == .limited {
completion()
} else {
YHHUD.flash(message: "需要相册权限才能选择照片")
}
}
}
case .denied, .restricted:
YHHUD.flash(message: "请在设置中打开相册权限")
@unknown default:
YHHUD.flash(message: "无法获取相册权限")
}
}
}
// MARK: - UIImagePickerControllerDelegate
extension YHMediaUploadSheetView: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
guard let mediaType = info[.mediaType] as? String else {
picker.dismiss(animated: true)
return
}
var mediaItems: [YHSelectMediaItem] = []
if mediaType == kUTTypeImage as String {
// 处理图片
if let image = info[.originalImage] as? UIImage {
var imageName = ""
if let imageUrl = info[.imageURL] as? URL {
imageName = imageUrl.lastPathComponent
}
if imageName.isEmpty {
let timestamp = Date().timeIntervalSince1970
imageName = "\(timestamp).jpg".replacingOccurrences(of: ".", with: "")
}
let item = YHSelectMediaItem(name: imageName, type: .image, image: image)
mediaItems.append(item)
}
} else if mediaType == kUTTypeMovie as String {
// 处理视频
if let videoURL = info[.mediaURL] as? URL {
let videoName = videoURL.lastPathComponent
let item = YHSelectMediaItem(name: videoName, type: .video, videoURL: videoURL)
mediaItems.append(item)
}
}
picker.dismiss(animated: true) {
self.uploadMediaBlock?(mediaItems)
self.dismiss()
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
}
// MARK: - PHPickerViewControllerDelegate
@available(iOS 14.0, *)
extension YHMediaUploadSheetView: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
self.dismiss()
if results.count <= 0 {
return
}
let group = DispatchGroup()
// 使用数组保持顺序,用可选类型处理失败情况
var mediaItems: [YHSelectMediaItem?] = Array(repeating: nil, count: results.count)
YHHUD.show(.progress(message: "处理中..."))
for (index, result) in results.enumerated() {
group.enter()
// 检查是否是图片
if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { url, _ in
defer { group.leave() }
if let url = url, let imageData = try? Data(contentsOf: url), let image = UIImage(data: imageData) {
let imageName = url.lastPathComponent.isEmpty ? "\(Date().timeIntervalSince1970).jpg" : url.lastPathComponent
let item = YHSelectMediaItem(name: imageName, type: .image, image: image)
mediaItems[index] = item
}
}
}
// 检查是否是视频
else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in
defer { group.leave() }
if let url = url {
// 复制视频文件到临时目录
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(url.lastPathComponent)
do {
if FileManager.default.fileExists(atPath: tempURL.path) {
try FileManager.default.removeItem(at: tempURL)
}
try FileManager.default.copyItem(at: url, to: tempURL)
let videoName = tempURL.lastPathComponent
let item = YHSelectMediaItem(name: videoName, type: .video, videoURL: tempURL)
mediaItems[index] = item
} catch {
printLog("视频文件复制失败: \(error)")
// 失败时 mediaItems[index] 保持为 nil
}
}
}
} else {
// 不支持的类型,直接 leave
group.leave()
}
}
// 等待所有任务完成
group.notify(queue: .main) {
YHHUD.hide()
// 过滤掉失败的项目,保持成功项目的相对顺序
let successfulItems = mediaItems.compactMap { $0 }
self.uploadMediaBlock?(successfulItems)
}
}
}
// MARK: - YHMediaUploadTypeCell
class YHMediaUploadTypeCell: UITableViewCell {
static let cellReuseIdentifier = "YHMediaUploadTypeCell"
var showSeparator: Bool = false {
didSet {
separatorView.isHidden = !showSeparator
}
}
var item: YHMediaUploadItem? {
didSet {
guard let item = item else { return }
titleLabel.text = item.title
// 根据类型设置样式
if item.type == .cancel {
titleLabel.font = UIFont.PFSC_R(ofSize: 15)
} else {
titleLabel.font = UIFont.PFSC_M(ofSize: 15)
}
}
}
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.PFSC_M(ofSize: 15)
label.textColor = UIColor.brandGrayColor8
label.textAlignment = .center
return label
}()
lazy var separatorView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.separator
view.isHidden = true
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
selectionStyle = .none
backgroundColor = .white
contentView.addSubview(titleLabel)
contentView.addSubview(separatorView)
titleLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
separatorView.snp.makeConstraints { make in
make.left.equalToSuperview()
make.right.equalToSuperview()
make.bottom.equalToSuperview()
make.height.equalTo(0.5)
}
}
}
......@@ -405,8 +405,31 @@ extension YHCertificateUploadSheetView: (UIImagePickerControllerDelegate & UINav
return false
}
private func getCaptureDeviceAuthorization(notDeterminedBlock: (() -> Void)?) -> Bool? {
let cameraAuthStatus = AVCaptureDevice.authorizationStatus(for: .video)
switch cameraAuthStatus {
case .authorized:
// 已授权,直接使用相机
return true
case .notDetermined:
// 未请求过权限,主动请求
AVCaptureDevice.requestAccess(for: .video) { _ in
notDeterminedBlock?()
}
return nil
case .denied, .restricted:
// 权限被拒绝或受限
break
@unknown default:
break
}
return false
}
func takePhoto() {
guard let authorization = getPhotoLibraryAuthorization(notDeterminedBlock: {
guard let authorization = getCaptureDeviceAuthorization(notDeterminedBlock: {
DispatchQueue.main.async {
self.takePhoto()
}
......@@ -415,7 +438,7 @@ extension YHCertificateUploadSheetView: (UIImagePickerControllerDelegate & UINav
}
if !authorization {
YHHUD.flash(message: "请在设置中打开相权限")
YHHUD.flash(message: "请在设置中打开相权限")
return
}
......
......@@ -310,8 +310,31 @@ extension YHDocumentUploadView: (UIImagePickerControllerDelegate & UINavigationC
return false
}
private func getCaptureDeviceAuthorization(notDeterminedBlock: (() -> Void)?) -> Bool? {
let cameraAuthStatus = AVCaptureDevice.authorizationStatus(for: .video)
switch cameraAuthStatus {
case .authorized:
// 已授权,直接使用相机
return true
case .notDetermined:
// 未请求过权限,主动请求
AVCaptureDevice.requestAccess(for: .video) { _ in
notDeterminedBlock?()
}
return nil
case .denied, .restricted:
// 权限被拒绝或受限
break
@unknown default:
break
}
return false
}
func takePhoto() {
guard let authorization = getPhotoLibraryAuthorization(notDeterminedBlock: {
guard let authorization = getCaptureDeviceAuthorization(notDeterminedBlock: {
DispatchQueue.main.async {
self.takePhoto()
}
......@@ -320,7 +343,7 @@ extension YHDocumentUploadView: (UIImagePickerControllerDelegate & UINavigationC
}
if !authorization {
YHHUD.flash(message: "请在设置中打开相权限")
YHHUD.flash(message: "请在设置中打开相权限")
return
}
......
//
// YHMediaBrowserViewController.swift
// galaxy
//
// Created by alexzzw on 2025/9/27.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import Kingfisher
import UIKit
import JXPhotoBrowser
import Photos
import PhotosUI
class YHMediaBrowserViewController: JXPhotoBrowser {
var deleteMediaBlock: ((Int) -> Void)?
var dismissBlock: (() -> Void)?
lazy var navBar: UIView = {
let v = UIView()
let backBtn = UIButton()
backBtn.setImage(UIImage(named: "nav_back_white"), for: .normal)
backBtn.addTarget(self, action: #selector(didBackBtnClicked), for: .touchUpInside)
v.addSubview(backBtn)
let deleteBtn = UIButton()
let img = UIImage(named: "media_brower_delete")
let templateImage = img?.withRenderingMode(.alwaysTemplate)
deleteBtn.setImage(templateImage, for: .normal)
deleteBtn.imageView?.tintColor = .white
deleteBtn.addTarget(self, action: #selector(didDeleteBtnClicked), for: .touchUpInside)
v.addSubview(deleteBtn)
backBtn.snp.makeConstraints { make in
make.width.height.equalTo(44)
make.left.equalToSuperview()
make.bottom.equalToSuperview()
}
deleteBtn.snp.makeConstraints { make in
make.width.height.equalTo(44)
make.right.equalToSuperview()
make.bottom.equalToSuperview()
}
return v
}()
@objc func didBackBtnClicked() {
dismiss()
}
@objc func didDeleteBtnClicked() {
let index = self.browserView.pageIndex
deleteMediaBlock?(index)
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(navBar)
navBar.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.height.equalTo(k_Height_NavigationtBarAndStatuBar)
}
}
deinit {
printLog("YHMediaBrowserViewController deinit")
}
override func dismiss() {
super.dismiss()
dismissBlock?()
}
}
......@@ -13,28 +13,28 @@ import SDWebImage
class YHPictureReviewManager: NSObject {
static let shared = YHPictureReviewManager()
override init() {
super.init()
}
private var curIndex: Int = 0
private var arrPics: [String] = []
private var arrPreviewItems: [YHPreviewMediaItem] = []
}
extension YHPictureReviewManager {
func showNetWorkPicturs(curIndex: Int, arrPicturs: [String]) {
guard curIndex > -1, arrPicturs.count > 0 else { return }
guard curIndex > -1, arrPicturs.count > 0 else { return }
self.curIndex = curIndex
self.arrPics = arrPicturs
arrPics = arrPicturs
let browser = YHPictureBrowserViewController()
browser.numberOfItems = {
self.arrPics.count
}
browser.reloadCellAtIndex = { context in
if context.index >= self.arrPics.count {
return
......@@ -44,16 +44,16 @@ extension YHPictureReviewManager {
let browserCell = context.cell as? JXPhotoBrowserImageCell
browserCell?.index = context.index
let placeholder = UIImage(named: "global_default_image")
browserCell?.imageView.sd_setImage(with: url, placeholderImage: placeholder, options: [], completed: { (_, _, _, _) in
browserCell?.imageView.sd_setImage(with: url, placeholderImage: placeholder, options: [], completed: { _, _, _, _ in
browserCell?.setNeedsLayout()
})
// 添加长按事件
browserCell?.longPressedAction = { cell, _ in
self.longPress(cell: cell)
}
}
browser.getImgUrlBlock = { [weak self] index in
guard let self = self else { return "" }
if 0 <= index, index < self.arrPics.count {
......@@ -61,22 +61,245 @@ extension YHPictureReviewManager {
}
return ""
}
// 数字样式的页码指示器
browser.pageIndicator = JXPhotoBrowserNumberPageIndicator()
browser.pageIndex = self.curIndex
browser.show()
}
}
extension YHPictureReviewManager {
private func longPress(cell: JXPhotoBrowserImageCell) {
let index = cell.index
if index < self.arrPics.count, index > -1 {
if index < arrPics.count, index > -1 {
let view = YHLongtapPictureSheetView.sheetView()
view.myUrl = self.arrPics[index]
view.myUrl = arrPics[index]
view.show()
}
}
private func longPressMedia(cell: JXPhotoBrowserImageCell) {
let index = cell.index
if index < arrPreviewItems.count, index > -1 {
let view = YHLongtapPictureSheetView.sheetView()
let item = arrPreviewItems[index]
if item.type == .image {
switch item.source {
case let .remoteURL(url):
view.myUrl = url
view.show()
case .localImage:
break
case .localVideoURL:
break
}
}
}
}
}
// MARK: - 新的混合媒体预览方法
extension YHPictureReviewManager {
/// 显示本地媒体预览(从 YHSelectMediaItem 数组)
/// - Parameters:
/// - curIndex: 当前索引
/// - selectMediaItems: 本地媒体项数组
/// - deleteCallback: 删除回调 (deletedIndex) -> Void
func showLocalMedia(curIndex: Int,
selectMediaItems: [YHSelectMediaItem],
deleteCallback: ((_ deletedIndex: Int) -> Void)? = nil) {
let previewItems = selectMediaItems.map { YHPreviewMediaItem(from: $0) }
showPreviewMedia(curIndex: curIndex,
previewItems: previewItems,
deleteCallback: deleteCallback)
}
/// 显示远程媒体预览(从URL字符串数组,自动检测类型)
/// - Parameters:
/// - curIndex: 当前索引
/// - urlStrings: URL字符串数组
/// - deleteCallback: 删除回调 (deletedIndex) -> Void
func showRemoteMedia(curIndex: Int,
urlStrings: [String],
deleteCallback: ((_ deletedIndex: Int) -> Void)? = nil) {
let previewItems = urlStrings.map { urlString -> YHPreviewMediaItem in
let type = detectMediaType(from: urlString)
return YHPreviewMediaItem(remoteURL: urlString, type: type)
}
showPreviewMedia(curIndex: curIndex,
previewItems: previewItems,
deleteCallback: deleteCallback)
}
/// 显示混合媒体预览(手动指定每个项目)
/// - Parameters:
/// - curIndex: 当前索引
/// - previewItems: 预览媒体项数组
/// - deleteCallback: 删除回调 (deletedIndex) -> Void
func showPreviewMedia(curIndex: Int,
previewItems: [YHPreviewMediaItem],
deleteCallback: ((_ deletedIndex: Int) -> Void)? = nil) {
guard curIndex > -1, previewItems.count > 0 else { return }
self.curIndex = curIndex
arrPreviewItems = previewItems
let browser = YHMediaBrowserViewController()
browser.numberOfItems = { [weak self] in
guard let self = self else {
return 0
}
return self.arrPreviewItems.count
}
// 根据媒体类型返回对应的Cell类
browser.cellClassAtIndex = { [weak self] index in
guard let self = self else {
return JXPhotoBrowserImageCell.self
}
guard index < self.arrPreviewItems.count else { return JXPhotoBrowserImageCell.self }
let previewItem = self.arrPreviewItems[index]
return previewItem.type == .video ? YHVideoCell.self : JXPhotoBrowserImageCell.self
}
browser.reloadCellAtIndex = { [weak self] context in
guard let self = self else {
return
}
if context.index >= self.arrPreviewItems.count {
return
}
let previewItem = self.arrPreviewItems[context.index]
if previewItem.type == .video {
// 视频Cell
let browserCell = context.cell as? YHVideoCell
switch previewItem.source {
case let .remoteURL(urlString):
if let url = URL(string: urlString) {
browserCell?.loadVideo(from: url)
}
case let .localVideoURL(url):
browserCell?.loadVideo(from: url)
case .localImage:
break // 视频不应该是本地图片
}
} else {
// 图片Cell
let browserCell = context.cell as? JXPhotoBrowserImageCell
browserCell?.index = context.index
let placeholder = UIImage(named: "global_default_image")
switch previewItem.source {
case let .remoteURL(urlString):
if let url = URL(string: urlString) {
browserCell?.imageView.sd_setImage(with: url, placeholderImage: placeholder, options: [], completed: { _, _, _, _ in
browserCell?.setNeedsLayout()
})
}
case let .localImage(image):
browserCell?.imageView.image = image
browserCell?.setNeedsLayout()
case .localVideoURL:
break // 图片不应该是视频URL
}
// 添加长按事件(仅图片)
browserCell?.longPressedAction = { [weak self] cell, _ in
guard let self = self else {
return
}
self.longPressMedia(cell: cell)
}
}
}
// 视频播放控制
browser.cellWillAppear = { [weak self] cell, index in
guard let self = self else {
return
}
if index < self.arrPreviewItems.count {
let previewItem = self.arrPreviewItems[index]
if previewItem.type == .video {
(cell as? YHVideoCell)?.playVideo()
}
}
}
browser.cellWillDisappear = { [weak self] cell, index in
guard let self = self else {
return
}
if index < self.arrPreviewItems.count {
let previewItem = self.arrPreviewItems[index]
if previewItem.type == .video {
(cell as? YHVideoCell)?.stopVideo()
}
}
}
// 完善的删除逻辑
browser.deleteMediaBlock = { [weak self, weak browser] index in
guard let self = self else {
return
}
// 删除数据
self.arrPreviewItems.remove(at: index)
deleteCallback?(index)
// 重新显示浏览器
if !self.arrPreviewItems.isEmpty {
browser?.dismiss(animated: false) { [weak self] in
guard let self = self else {
return
}
let newIndex = index < self.arrPreviewItems.count ? index : self.arrPreviewItems.count - 1
self.showPreviewMedia(curIndex: newIndex, previewItems: self.arrPreviewItems, deleteCallback: deleteCallback)
}
} else {
browser?.dismiss(animated: true)
self.clearData()
}
}
browser.dismissBlock = { [weak self] in
guard let self = self else {
return
}
self.clearData()
}
// 数字样式的页码指示器
browser.pageIndicator = JXPhotoBrowserNumberPageIndicator()
browser.pageIndex = self.curIndex
browser.show()
}
private func clearData() {
arrPreviewItems.removeAll()
}
/// 根据URL检测媒体类型
/// - Parameter urlString: URL字符串
/// - Returns: 媒体类型
private func detectMediaType(from urlString: String) -> YHMediaType {
let lowercasedUrl = urlString.lowercased()
// 视频文件扩展名
let videoExtensions = ["mp4", "mov", "avi", "mkv", "wmv", "flv", "webm", "m4v"]
// 检查URL是否包含视频扩展名
for ext in videoExtensions {
if lowercasedUrl.contains(".\(ext)") {
return .video
}
}
// 默认为图片
return .image
}
}
//
// YHPreviewMediaItem.swift
// galaxy
//
// Created by alexzzw on 2025/9/27.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
// 预览媒体项 - 专门用于预览的数据模型
struct YHPreviewMediaItem {
let type: YHMediaType
let source: YHMediaSource
enum YHMediaSource {
case remoteURL(String) // 远程URL
case localImage(UIImage) // 本地图片
case localVideoURL(URL) // 本地视频文件URL
var displayURL: String {
switch self {
case .remoteURL(let url):
return url
case .localImage:
return "" // 本地图片不需要URL
case .localVideoURL(let url):
return url.absoluteString
}
}
}
// 从远程URL创建
init(remoteURL: String, type: YHMediaType = .image) {
self.type = type
self.source = .remoteURL(remoteURL)
}
// 从本地图片创建
init(localImage: UIImage) {
self.type = .image
self.source = .localImage(localImage)
}
// 从本地视频URL创建
init(localVideoURL: URL) {
self.type = .video
self.source = .localVideoURL(localVideoURL)
}
// 从 YHSelectMediaItem 创建
init(from selectMediaItem: YHSelectMediaItem) {
self.type = selectMediaItem.type
switch selectMediaItem.type {
case .image:
if let image = selectMediaItem.image {
self.source = .localImage(image)
} else {
// 如果没有本地图片,使用占位符
self.source = .localImage(UIImage(named: "global_default_image") ?? UIImage())
}
case .video:
if let videoURL = selectMediaItem.videoURL {
self.source = .localVideoURL(videoURL)
} else {
// 异常情况,创建一个空的远程URL
self.source = .remoteURL("")
}
}
}
}
//
// YHSelectMediaItem.swift
// galaxy
//
// Created by alexzzw on 2025/9/27.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
class YHSelectMediaItem {
var name: String
var type: YHMediaType
var image: UIImage?
var videoURL: URL?
var duration: TimeInterval?
init(name: String = "", type: YHMediaType = .image, image: UIImage? = nil, videoURL: URL? = nil, duration: TimeInterval? = nil) {
self.name = name
self.type = type
self.image = image
self.videoURL = videoURL
self.duration = duration
}
}
enum YHMediaType {
case image
case video
}
//
// YHVideoCell.swift
// galaxy
//
// Created by alexzzw on 2025/9/27.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import Foundation
import UIKit
import AVFoundation
import JXPhotoBrowser
import SnapKit
class YHVideoCell: UIView, JXPhotoBrowserCell {
weak var photoBrowser: JXPhotoBrowser?
lazy var player = AVPlayer()
lazy var playerLayer = AVPlayerLayer(player: player)
// 播放状态图标
lazy var playImageView: UIImageView = {
let imageView = UIImageView()
let playImage = UIImage(named: "circle_play_icon")?.withRenderingMode(.alwaysOriginal)
imageView.image = playImage
return imageView
}()
// 进度滑块
lazy var progressSlider: UISlider = {
let slider = UISlider()
slider.minimumTrackTintColor = .white
slider.maximumTrackTintColor = UIColor.white.withAlphaComponent(0.3)
slider.thumbTintColor = .white
slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
slider.addTarget(self, action: #selector(sliderTouchDown), for: .touchDown)
slider.addTarget(self, action: #selector(sliderTouchUp), for: .touchUpInside)
slider.addTarget(self, action: #selector(sliderTouchUp), for: .touchUpOutside)
return slider
}()
// 时间标签
lazy var timeLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12)
label.textColor = .white
label.text = "00:00 / 00:00"
label.textAlignment = .right
return label
}()
private var timeObserver: Any?
private var isPlaying = false
private var isSeeking = false
static func generate(with browser: JXPhotoBrowser) -> Self {
let instance = Self.init(frame: .zero)
instance.photoBrowser = browser
return instance
}
required override init(frame: CGRect) {
super.init(frame: .zero)
setupUI()
setupGestures()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
removeTimeObserver()
NotificationCenter.default.removeObserver(self)
}
private func setupUI() {
backgroundColor = .black
layer.addSublayer(playerLayer)
addSubview(playImageView)
addSubview(progressSlider)
addSubview(timeLabel)
setupConstraints()
setupNotifications()
}
private func setupConstraints() {
playImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(48)
}
progressSlider.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(16)
make.bottom.equalToSuperview().offset(-60)
make.height.equalTo(30)
}
timeLabel.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-16)
make.top.equalTo(progressSlider.snp.bottom).offset(8)
}
}
private func setupGestures() {
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tap)
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap))
doubleTap.numberOfTapsRequired = 2
addGestureRecognizer(doubleTap)
tap.require(toFail: doubleTap)
}
private func setupNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(playerDidFinishPlaying),
name: .AVPlayerItemDidPlayToEndTime,
object: nil
)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
// MARK: - Actions
@objc private func handleTap() {
// 点击整个页面播放或暂停
if isPlaying {
pauseVideo()
} else {
playVideo()
}
// 显示/隐藏控制器
toggleControlsVisibility()
}
@objc private func handleDoubleTap() {
photoBrowser?.dismiss()
}
@objc private func sliderValueChanged(_ sender: UISlider) {
guard let item = player.currentItem else { return }
let duration = CMTimeGetSeconds(item.duration)
if duration.isFinite && duration > 0 {
let targetTime = Double(sender.value) * duration
let currentTimeString = formatTime(targetTime)
let durationString = formatTime(duration)
timeLabel.text = "\(currentTimeString) / \(durationString)"
}
}
@objc private func sliderTouchDown() {
isSeeking = true
}
@objc private func sliderTouchUp() {
guard let item = player.currentItem else { return }
let duration = CMTimeGetSeconds(item.duration)
if duration.isFinite && duration > 0 {
let targetTime = Double(progressSlider.value) * duration
let seekTime = CMTime(seconds: targetTime, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
player.seek(to: seekTime) { [weak self] _ in
self?.isSeeking = false
}
} else {
isSeeking = false
}
}
@objc private func playerDidFinishPlaying() {
isPlaying = false
playImageView.isHidden = false
player.seek(to: CMTime.zero)
progressSlider.value = 0
updatePlayButtonImage()
}
// MARK: - Video Control
func loadVideo(from url: URL) {
let playerItem = AVPlayerItem(url: url)
player.replaceCurrentItem(with: playerItem)
addTimeObserver()
updatePlayButtonImage()
}
func loadVideo(from urlString: String) {
guard let url = URL(string: urlString) else { return }
loadVideo(from: url)
}
func playVideo() {
player.play()
isPlaying = true
playImageView.isHidden = true
updatePlayButtonImage()
}
private func pauseVideo() {
player.pause()
isPlaying = false
playImageView.isHidden = false
updatePlayButtonImage()
}
func stopVideo() {
player.pause()
player.seek(to: CMTime.zero)
isPlaying = false
playImageView.isHidden = false
progressSlider.value = 0
updatePlayButtonImage()
}
private func updatePlayButtonImage() {
let imageName = isPlaying ? "circle_pause_icon" : "circle_play_icon"
let image = UIImage(named: imageName)?.withRenderingMode(.alwaysOriginal)
playImageView.image = image
}
private func toggleControlsVisibility() {
let isHidden = playImageView.alpha == 0
UIView.animate(withDuration: 0.3) {
self.playImageView.alpha = isHidden ? 1.0 : 0.0
self.progressSlider.alpha = isHidden ? 1.0 : 0.0
self.timeLabel.alpha = isHidden ? 1.0 : 0.0
} completion: { _ in
self.playImageView.isHidden = !isHidden && self.isPlaying
self.progressSlider.isHidden = !isHidden
self.timeLabel.isHidden = !isHidden
}
}
// MARK: - Time Observer
private func addTimeObserver() {
let timeInterval = CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserver = player.addPeriodicTimeObserver(forInterval: timeInterval, queue: .main) { [weak self] _ in
self?.updateProgress()
}
}
private func removeTimeObserver() {
if let timeObserver = timeObserver {
player.removeTimeObserver(timeObserver)
self.timeObserver = nil
}
}
private func updateProgress() {
guard let item = player.currentItem, !isSeeking else { return }
let currentTime = CMTimeGetSeconds(player.currentTime())
let duration = CMTimeGetSeconds(item.duration)
if duration.isFinite && duration > 0 {
progressSlider.value = Float(currentTime / duration)
let currentTimeString = formatTime(currentTime)
let durationString = formatTime(duration)
timeLabel.text = "\(currentTimeString) / \(durationString)"
}
}
private func formatTime(_ time: Double) -> String {
if time.isInfinite || time.isNaN {
return "00:00"
}
let minutes = Int(time) / 60
let seconds = Int(time) % 60
return String(format: "%02d:%02d", minutes, seconds)
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "circle_pause_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "circle_pause_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "circle_play_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "circle_play_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "circle_plus_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "circle_plus_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "media_brower_delete@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "media_brower_delete@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment