582 lines
22 KiB
Swift
582 lines
22 KiB
Swift
//
|
||
// GroupChatVC.swift
|
||
// QuickLocation
|
||
//
|
||
// Created by 八条 on 2026/6/4.
|
||
//
|
||
|
||
import UIKit
|
||
import RxSwift
|
||
import RxCocoa
|
||
import RxDataSources
|
||
import OpenIMSDK
|
||
import AVFoundation
|
||
import AudioToolbox
|
||
import HXPHPicker
|
||
import IQKeyboardManagerSwift
|
||
|
||
final class GroupChatVC: BaseViewController {
|
||
|
||
override var isNavigationBarHidden: Bool { true }
|
||
|
||
fileprivate var rootView: GroupChatView!
|
||
private let viewModel = GroupChatViewModel()
|
||
private var msgListener: MessageListenerProxy?
|
||
|
||
// MARK: - Init
|
||
init(groupId: String) {
|
||
viewModel.groupId = groupId
|
||
super.init(nibName: nil, bundle: nil)
|
||
}
|
||
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func loadView() {
|
||
rootView = GroupChatView(frame: UIScreen.main.bounds)
|
||
view = rootView
|
||
}
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
fd_interactivePopDisabled = true
|
||
bindViewModel()
|
||
requestGroupInfoByKey()
|
||
reactiveAction()
|
||
setupMessageListener()
|
||
setupVoiceRecording()
|
||
setupPanelDismiss()
|
||
setupKeyboard()
|
||
}
|
||
|
||
override func viewWillAppear(_ animated: Bool) {
|
||
super.viewWillAppear(animated)
|
||
IQKeyboardManager.shared.isEnabled = false
|
||
IQKeyboardManager.shared.resignOnTouchOutside = false
|
||
}
|
||
|
||
override func viewDidAppear(_ animated: Bool) {
|
||
super.viewDidAppear(animated)
|
||
// if !hasScrolledToBottom {
|
||
// hasScrolledToBottom = true
|
||
// scrollToBottom()
|
||
// }
|
||
}
|
||
|
||
override func viewWillDisappear(_ animated: Bool) {
|
||
super.viewWillDisappear(animated)
|
||
VoicePlayerManager.shared.stop()
|
||
IQKeyboardManager.shared.isEnabled = true
|
||
IQKeyboardManager.shared.resignOnTouchOutside = true
|
||
}
|
||
|
||
// MARK: - Keyboard
|
||
private func setupKeyboard() {
|
||
// 键盘升起
|
||
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
|
||
.subscribe(onNext: { [weak self] noti in
|
||
guard let self = self,
|
||
let userInfo = noti.userInfo,
|
||
let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
|
||
else { return }
|
||
let height = frame.height
|
||
let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
|
||
// 收起表情/语音面板
|
||
self.rootView.dismissAllPanels(excludeTextField: true)
|
||
UIView.animate(withDuration: duration) {
|
||
self.rootView.bottomBar.layoutChain.bottom(height + kSafeBottomMargin + 20)
|
||
}
|
||
self.scrollToBottom()
|
||
})
|
||
.disposed(by: disposeBag)
|
||
}
|
||
|
||
private func setupPanelDismiss() {
|
||
// 用户开始拖拽 tableview 时收起面板
|
||
rootView.tableView.panGestureRecognizer.rx.event
|
||
.filter { $0.state == .began }
|
||
.subscribe(onNext: { [weak self] _ in
|
||
self?.rootView.dismissAllPanels()
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
// 点击 cell 收起面板
|
||
rootView.tableView.rx.itemSelected
|
||
.subscribe(onNext: { [weak self] _ in
|
||
self?.rootView.dismissAllPanels()
|
||
})
|
||
.disposed(by: disposeBag)
|
||
}
|
||
|
||
// MARK: - Bindings
|
||
private func bindViewModel() {
|
||
viewModel.output.messages
|
||
.skip(1)
|
||
.map { [ChatSectionModel(model: "", items: $0)] }
|
||
.observe(on: MainScheduler.asyncInstance)
|
||
.bind(to: rootView.tableView.rx.items(dataSource: dataSource))
|
||
.disposed(by: disposeBag)
|
||
|
||
viewModel.output.messages
|
||
.skip(1)
|
||
.observe(on: MainScheduler.asyncInstance)
|
||
.subscribe(onNext: { [weak self] _ in
|
||
self?.scrollToBottom()
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
let emojiItems = UITableViewCell.emojiFileNames.map { $0 }
|
||
Observable.just([SectionModel(model: "", items: emojiItems)])
|
||
.bind(to: rootView.emojiCollectionView.rx.items(dataSource: emojiDataSource))
|
||
.disposed(by: disposeBag)
|
||
}
|
||
|
||
private var hasScrolledToBottom = false
|
||
|
||
private func scrollToBottom() {
|
||
let count = dataSource.sectionModels.first?.items.count ?? 0
|
||
guard count > 0 else { return }
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||
self.rootView.tableView.layoutIfNeeded()
|
||
let indexPath = IndexPath(row: count - 1, section: 0)
|
||
self.rootView.tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)
|
||
}
|
||
}
|
||
|
||
private lazy var emojiDataSource: RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> = {
|
||
RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> { _, collectionView, indexPath, name in
|
||
let cell: EmojiPanelCell = collectionView.dequeueReusableCell(for: indexPath)
|
||
if let path = Bundle.main.path(forResource: name, ofType: "json") {
|
||
cell.configure(path: path)
|
||
}
|
||
return cell
|
||
}
|
||
}()
|
||
|
||
// MARK: - Message Listener
|
||
private func setupMessageListener() {
|
||
msgListener = MessageListenerProxy { [weak self] msg in
|
||
guard let self = self, msg.groupID == self.viewModel.groupId else { return }
|
||
self.viewModel.onReceiveMessage(msg)
|
||
}
|
||
OIMManager.callbacker.addAdvancedMsgListener(listener: msgListener!)
|
||
}
|
||
|
||
// MARK: - Actions
|
||
private func reactiveAction() {
|
||
rootView.backBtn.rx.tap
|
||
.subscribe(onNext: { _ in
|
||
AppRouter.shared.popOrDismiss()
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
// 语音按钮
|
||
rootView.voiceBtn.rx.tap.subscribe(onNext: { [weak self] _ in
|
||
guard let self = self else { return }
|
||
let status = AVCaptureDevice.authorizationStatus(for: .audio)
|
||
switch status {
|
||
case .authorized:
|
||
break
|
||
case .notDetermined:
|
||
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
||
guard granted else { return }
|
||
DispatchQueue.main.async {
|
||
self.rootView.dismissAllPanels()
|
||
self.showSpeakPanel()
|
||
}
|
||
}
|
||
return
|
||
default:
|
||
Permission.openAppSetting(title: "请开启麦克风权限",
|
||
message: "请在iPhone的“设置-隐私-麦克风”选项中允许\(kAppName)访问你的麦克风。")
|
||
return
|
||
}
|
||
self.rootView.dismissAllPanels()
|
||
self.showSpeakPanel()
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
// 键盘按钮
|
||
rootView.voiceRecordView.keyboardBtn.rx.tap
|
||
.subscribe(onNext: { [weak self] _ in
|
||
guard let self = self else { return }
|
||
self.rootView.dismissAllPanels()
|
||
self.rootView.textField.becomeFirstResponder()
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
// 表情按钮
|
||
rootView.emojiBtn.rx.tap
|
||
.subscribe(onNext: { [weak self] _ in
|
||
guard let self = self else { return }
|
||
self.showEmojiPanel()
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
rootView.voiceRecordView.emojiBtn.rx.tap
|
||
.subscribe(onNext: { [weak self] _ in
|
||
guard let self = self else { return }
|
||
self.rootView.dismissAllPanels()
|
||
self.showEmojiPanel()
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
// 表情面板点击
|
||
rootView.emojiCollectionView.rx.modelSelected(String.self)
|
||
.subscribe(onNext: { [weak self] name in
|
||
guard let self = self, let idx = UITableViewCell.emojiFileNames.firstIndex(of: name) else { return }
|
||
self.viewModel.input.sendMessage.onNext("js_emoji:\(idx)")
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
// ➕按钮
|
||
Observable.merge(
|
||
rootView.addBtn.rx.tap.asObservable(),
|
||
rootView.voiceRecordView.addBtn.rx.tap.asObservable()
|
||
)
|
||
.subscribe(onNext: { [weak self] in
|
||
guard let self = self else { return }
|
||
self.rootView.dismissAllPanels()
|
||
self.showAlbum()
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
let sendText = Observable.merge(
|
||
rootView.sendBtn.rx.tap.map { [weak self] _ in self?.rootView.textField.text ?? "" },
|
||
rootView.textField.rx.controlEvent(.editingDidEndOnExit)
|
||
.map { [weak self] _ in self?.rootView.textField.text ?? "" }
|
||
)
|
||
.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||
.do(onNext: { [weak self] _ in self?.rootView.textField.text = "" })
|
||
|
||
sendText
|
||
.bind(to: viewModel.input.sendMessage)
|
||
.disposed(by: disposeBag)
|
||
|
||
// 设置
|
||
rootView.settingBtn.rx.tap
|
||
.subscribe(onNext: { [weak self] _ in
|
||
guard let self = self else { return }
|
||
AppRouter.push(Route.groupSetting, userInfo: ["groupId": self.viewModel.groupId])
|
||
})
|
||
.disposed(by: disposeBag)
|
||
}
|
||
|
||
// MARK: - 显示语音面板
|
||
private func showSpeakPanel() {
|
||
let show = rootView.voiceRecordView.isHidden
|
||
rootView.voiceRecordView.isHidden = !show
|
||
let offset: CGFloat = show ? 252 : 0
|
||
UIView.animate(withDuration: 0.25) {
|
||
self.rootView.bottomBar.layoutChain.bottom(show ? offset - self.rootView.bottomBar.dl.height : kSafeBottomMargin + 20)
|
||
self.rootView.voiceRecordView.layoutChain.bottom(offset - 252 + kSafeBottomMargin)
|
||
}
|
||
scrollToBottom()
|
||
}
|
||
|
||
// MARK: - 显示表情面板
|
||
private func showEmojiPanel() {
|
||
let show = self.rootView.emojiPanelView.isHidden
|
||
self.rootView.emojiPanelView.isHidden = !show
|
||
let offset: CGFloat = show ? 220 : 0
|
||
UIView.animate(withDuration: 0.25) {
|
||
self.rootView.bottomBar.layoutChain.bottom(kSafeBottomMargin + 20 + offset)
|
||
}
|
||
// completion: { _ in
|
||
// let offset: CGFloat = self.rootView.tableView.contentSize.height
|
||
// self.rootView.tableView.setContentOffset(CGPointMake(0, offset), animated: false)
|
||
// }
|
||
scrollToBottom()
|
||
|
||
if show {
|
||
self.rootView.textField.resignFirstResponder()
|
||
EmojiPanelCell.preloadAnimations()
|
||
}
|
||
}
|
||
|
||
// MARK: - 显示相册
|
||
private func showAlbum() {
|
||
// 设置与微信主题一致的配置
|
||
let config = PhotoTools.getWXPickerConfig()
|
||
// 最多可以选择的资源数,如果为0则不限制
|
||
config.selectOptions = [.photo]
|
||
config.selectMode = .multiple
|
||
config.maximumSelectedCount = 9
|
||
// config.maximumSelectedPhotoFileSize = 5242880
|
||
config.allowSyncICloudWhenSelectPhoto = false
|
||
config.previewView.bottomView.editButtonHidden = true
|
||
config.photoList.allowAddCamera = true
|
||
config.photoList.camera.allowsEditing = false
|
||
let pickerController = PhotoPickerController(picker: config)
|
||
pickerController.pickerDelegate = self
|
||
pickerController.modalPresentationStyle = .fullScreen
|
||
self.present(pickerController, animated: true, completion: nil)
|
||
}
|
||
|
||
// MARK: - Voice Recording
|
||
private var audioRecorder: AVAudioRecorder?
|
||
private var recordFileURL: URL?
|
||
private var recordTimer: Timer?
|
||
private var recordDuration: Int = 0
|
||
private func setupVoiceRecording() {
|
||
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleVoiceLongPress(_:)))
|
||
longPress.minimumPressDuration = 0.2
|
||
rootView.voiceRecordView.speakBtn.addGestureRecognizer(longPress)
|
||
}
|
||
|
||
@objc private func handleVoiceLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||
let location = gesture.location(in: rootView.voiceRecordView)
|
||
let btnCenterX = rootView.voiceRecordView.speakBtn.center.x
|
||
let isCancel = location.x < btnCenterX - 100
|
||
|
||
switch gesture.state {
|
||
case .began:
|
||
AudioServicesPlaySystemSound(1519)
|
||
rootView.voiceRecordView.state = .recording
|
||
startRecording()
|
||
case .changed:
|
||
let wasRecording = rootView.voiceRecordView.state == .recording
|
||
rootView.voiceRecordView.state = isCancel ? .canceling : .recording
|
||
rootView.voiceRecordView.cancelBtn.isSelected = isCancel
|
||
if isCancel && wasRecording {
|
||
AudioServicesPlaySystemSound(1519)
|
||
}
|
||
case .ended:
|
||
stopRecording(cancel: isCancel)
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
|
||
private func startRecording() {
|
||
let session = AVAudioSession.sharedInstance()
|
||
try? session.setCategory(.playAndRecord, mode: .default)
|
||
try? session.setActive(true)
|
||
|
||
let dir = NSTemporaryDirectory()
|
||
let filename = "voice_\(Int(Date().timeIntervalSince1970)).wav"
|
||
recordFileURL = URL(fileURLWithPath: dir + filename)
|
||
|
||
let settings: [String: Any] = [
|
||
AVFormatIDKey: kAudioFormatLinearPCM,
|
||
AVSampleRateKey: 8000,
|
||
AVNumberOfChannelsKey: 1,
|
||
AVLinearPCMBitDepthKey: 16,
|
||
AVLinearPCMIsFloatKey: false
|
||
]
|
||
|
||
guard let url = recordFileURL else { return }
|
||
audioRecorder = try? AVAudioRecorder(url: url, settings: settings)
|
||
audioRecorder?.record()
|
||
|
||
recordDuration = 0
|
||
recordTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||
self?.recordDuration += 1
|
||
}
|
||
}
|
||
|
||
private func stopRecording(cancel: Bool) {
|
||
audioRecorder?.stop()
|
||
recordTimer?.invalidate()
|
||
recordTimer = nil
|
||
rootView.voiceRecordView.stopRotating()
|
||
|
||
try? AVAudioSession.sharedInstance().setActive(false)
|
||
|
||
if cancel {
|
||
self.rootView.voiceRecordView.cancelBtn.isSelected = false
|
||
if let url = recordFileURL { try? FileManager.default.removeItem(at: url) }
|
||
return
|
||
}
|
||
|
||
guard let url = recordFileURL, recordDuration >= 1 else {
|
||
DLToast.show(text: "说话时间太短")
|
||
if let url = recordFileURL { try? FileManager.default.removeItem(at: url) }
|
||
return
|
||
}
|
||
|
||
// Send voice message via OpenIM
|
||
let msg = OIMMessageInfo.createSoundMessage(fromFullPath: url.path, duration: recordDuration * 1000)
|
||
OIMManager.manager.sendMessage(msg,
|
||
recvID: "",
|
||
groupID: viewModel.groupId,
|
||
offlinePushInfo: nil,
|
||
onSuccess: { [weak self] _ in
|
||
self?.viewModel.onReceiveMessage(msg)
|
||
},
|
||
onProgress: nil as OIMNumberCallback?,
|
||
onFailure: { code, errMsg in
|
||
print("Voice send failed: \(code) \(errMsg ?? "")")
|
||
})
|
||
}
|
||
|
||
// MARK: - API
|
||
private func requestGroupInfoByKey() {
|
||
GroupService.groupInfoByKey(viewModel.groupId).subscribe { response in
|
||
guard let model = response.model else { return }
|
||
self.viewModel.memberList = response.list
|
||
self.rootView.groupNameLabel.text = model.name
|
||
self.rootView.groupAvatarView.image = model.groupIcon
|
||
self.viewModel.loadMessages()
|
||
}.disposed(by: disposeBag)
|
||
}
|
||
|
||
// MARK: - dataSource
|
||
private lazy var dataSource: RxTableViewSectionedReloadDataSource<ChatSectionModel> = {
|
||
RxTableViewSectionedReloadDataSource<ChatSectionModel> { _, tableView, indexPath, item in
|
||
switch item {
|
||
case let .send(msg):
|
||
let cell: TextSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||
cell.configure(msg)
|
||
return cell
|
||
case let .received(msg):
|
||
let cell: TextReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||
cell.configure(msg)
|
||
return cell
|
||
case let .emojiSend(msg):
|
||
let cell: EmojiSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||
cell.configure(msg)
|
||
return cell
|
||
case let .emojiReceived(msg):
|
||
let cell: EmojiReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||
cell.configure(msg)
|
||
return cell
|
||
case let .voiceSend(msg):
|
||
let cell: VoiceSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||
cell.configure(msg)
|
||
return cell
|
||
case let .voiceReceived(msg):
|
||
let cell: VoiceReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||
cell.configure(msg)
|
||
return cell
|
||
case let .imageSend(msg):
|
||
let cell: ImageSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||
cell.configure(msg)
|
||
cell.onImageTap = { [weak self] in
|
||
self?.showBigImage(imgUrlList: [msg.imageUrl], currentPage: 0, projectiveView: cell.photoView)
|
||
}
|
||
return cell
|
||
case let .imageReceived(msg):
|
||
let cell: ImageReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||
cell.configure(msg)
|
||
cell.onImageTap = { [weak self] in
|
||
self?.showBigImage(imgUrlList: [msg.imageUrl], currentPage: 0, projectiveView: cell.photoView)
|
||
}
|
||
return cell
|
||
case let .notification(text):
|
||
let cell: NotificationMsgCell = tableView.dequeueReusableCell(for: indexPath)
|
||
cell.configure(text)
|
||
return cell
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
// MARK: - MessageListenerProxy
|
||
private class MessageListenerProxy: NSObject, OIMAdvancedMsgListener {
|
||
private let handler: (OIMMessageInfo) -> Void
|
||
|
||
init(handler: @escaping (OIMMessageInfo) -> Void) {
|
||
self.handler = handler
|
||
}
|
||
|
||
func onRecvNewMessage(_ msg: OIMMessageInfo) {
|
||
handler(msg)
|
||
}
|
||
}
|
||
|
||
// MARK: - PhotoPickerControllerDelegate
|
||
extension GroupChatVC: PhotoPickerControllerDelegate {
|
||
/// 选择完成之后调用
|
||
/// - Parameters:
|
||
/// - pickerController: 对应的 PhotoPickerController
|
||
/// - result: 选择的结果
|
||
/// result.photoAssets 选择的资源数组
|
||
/// result.isOriginal 是否选中原图
|
||
func pickerController(_ pickerController: PhotoPickerController,
|
||
didFinishSelection result: PickerResult) {
|
||
result.getImage { (image, photoAsset, index) in
|
||
} completionHandler: { [weak self] (images) in
|
||
guard let self = self else { return }
|
||
for img in images {
|
||
self.sendImageMessage(img)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func sendImageMessage(_ image: UIImage) {
|
||
guard let data = image.jpegData(compressionQuality: 0.8) else { return }
|
||
let dir = NSTemporaryDirectory()
|
||
let filename = "img_\(Int(Date().timeIntervalSince1970)).jpg"
|
||
let fileURL = URL(fileURLWithPath: dir + filename)
|
||
try? data.write(to: fileURL)
|
||
|
||
let displaySize = Self.imageDisplaySize(w: image.size.width, h: image.size.height)
|
||
|
||
let msg = OIMMessageInfo.createImageMessage(fromFullPath: fileURL.path)
|
||
// 使用 SDK 的 clientMsgID 作为本地消息 ID,方便后续与监听器去重
|
||
let localId = msg.clientMsgID ?? UUID().uuidString
|
||
|
||
// 立即显示本地图片(带 loading)
|
||
let localMsg = ChatMessage(
|
||
id: localId,
|
||
isSelf: true,
|
||
avatar: viewModel.getUserAvatar(id: AppContextManager.shared.userId),
|
||
senderName: AppContextManager.shared.name,
|
||
content: "",
|
||
voiceUrl: "",
|
||
imageUrl: fileURL.path,
|
||
imageWidth: displaySize.width,
|
||
imageHeight: displaySize.height,
|
||
timestamp: Date().timeIntervalSince1970,
|
||
showTime: false,
|
||
isUploading: true
|
||
)
|
||
viewModel.appendLocalMessage(.imageSend(localMsg))
|
||
OIMManager.manager.sendMessage(msg,
|
||
recvID: "",
|
||
groupID: viewModel.groupId,
|
||
offlinePushInfo: nil,
|
||
onSuccess: { [weak self] returnedMsg in
|
||
// 服务端返回后,更新本地消息为服务端图片URL,去掉loading
|
||
// 注意:returnedMsg 是 SDK 回填服务端数据后的新 OIMMessageInfo,包含完整 URL
|
||
let networkUrl = returnedMsg?.pictureElem?.sourcePicture?.url
|
||
?? returnedMsg?.pictureElem?.bigPicture?.url
|
||
?? returnedMsg?.pictureElem?.sourcePath
|
||
?? ""
|
||
self?.viewModel.updateLocalMessage(id: localId) { chatMsg in
|
||
// 仅在服务端有URL时才替换,否则保留本地路径让图片仍可见
|
||
if !networkUrl.isEmpty {
|
||
chatMsg.imageUrl = networkUrl
|
||
}
|
||
chatMsg.isUploading = false
|
||
}
|
||
try? FileManager.default.removeItem(at: fileURL)
|
||
},
|
||
onProgress: nil as OIMNumberCallback?,
|
||
onFailure: { [weak self] code, errMsg in
|
||
print("Image send failed: \(code) \(errMsg ?? "")")
|
||
// 发送失败,隐藏 loading
|
||
self?.viewModel.updateLocalMessage(id: localId) { chatMsg in
|
||
chatMsg.isUploading = false
|
||
}
|
||
})
|
||
}
|
||
|
||
private static func imageDisplaySize(w: CGFloat, h: CGFloat) -> CGSize {
|
||
guard w > 0, h > 0 else { return CGSize(width: 160, height: 160) }
|
||
let maxW: CGFloat = 200, maxH: CGFloat = 250, minW: CGFloat = 80
|
||
var dw = maxW, dh = dw * (h / w)
|
||
if dh > maxH { dh = maxH; dw = dh * (w / h) }
|
||
if dw < minW { dw = minW; dh = dw * (h / w) }
|
||
return CGSize(width: dw, height: dh)
|
||
}
|
||
|
||
/// 点击取消时调用
|
||
/// - Parameter pickerController: 对应的 PhotoPickerController
|
||
func pickerController(didCancel pickerController: PhotoPickerController) {
|
||
|
||
}
|
||
}
|