现在要写这个部分,新开一页去放。
思路是利用for...in来逐个打印组件
设置两个数组,分别储存图片字符串和对应的文本字符串
记得数组取名要取复数,表示里面不止一个元素
新建"+"按钮的功能栏,并将其添加到视图
只做这三个:照片、拍摄、位置。
swift
class MoreView: UIView {
let imgs = ["photo.fill", "camera.fill", "location.fill"]
let texts = ["照片", "相机", "位置"]
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
}
然后再setupUI()函数里设置图片按钮和文字标签,并为其添加事件
使用for...in来逐个打印组件
按钮
swift
func setupUI(){
let y = 16
for idx in 0..<imgs.count {
let x = 16 + (24 + 66) * idx //同行x是递增的
let btn = UIButton(frame: .init(x: x, y: y, width: 66, height: 66))
btn.setImage(UIImage(systemName: imgs[idx]), for: .normal)
btn.backgroundColor = .white
btn.layer.cornerRadius = 12
btn.layer.masksToBounds = true
btn.tag = 1000 + idx
//为了防止tag重复,要在idx的基础上加100,1000等
btn.tintColor = .black.withAlphaComponent(0.7)
self.addSubview(btn)
文本
swift
let lbl = UILabel(frame: .init(x: x, y: y, width: 66, height: 66))
lbl.text = texts[idx]
lbl.textColor = .black.withAlphaComponent(0.7)
lbl.font = .systemFont(ofSize: 16)
lbl.textAlignment = .center
self.addSubview(lbl)
使用回调,把索引以tag传出去。
swift
typealias BtnClick = ((Int) -> ())
class MoreView: UIView {
var btnClick: BtnClick? }
swift
@objc func handleAction(btn: UIButton) {
let tag = btn.tag - 1000
btnClick?(tag)
}
写一个函数,控制闭包被传出?
获取底部栏高度:写在Public里
swift
let bottomSafeArea = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0
在ChatViewController页面写一个moreView,这是创建界面实例,并控制点击对应按钮,然后操作相关功能的。
swift
lazy var moreView: MoreView = {
let moreView = MoreView(frame: .init(x: 0, y: SCREENHEIGHT - 98 - bottomSafeArea, width: SCREENWIDTH, height: 98 + bottomSafeArea))
moreView.backgroundColor = .lightGray
moreView.btnClick = { [weak self] idx in
print(idx)
if idx == 0 {
//验证相册权限,并打开相册
self?.openPhotoLib()
} else if idx == 1 {
//验证相机权限,并打开相机
self?.openCamera()
} else {
//打开定位
}
}
把相关绑定到点击按钮MoreView的对应索引处
0对应的是第一个按钮,以此类推(openPhotoLib函数,openCamera函数待编写),使用self?调用是因为闭包会捕获内外参数,必须弱引用。weak self和?应该是一起用的。
然后写一个函数popMoreView,点击加号按钮时展开界面。
先为加号按钮添加事件。
swift
chatInputView.plusButton.addTarget(self, action: #selector(popMoreView), for: .touchUpInside)
为了实现展开时注销键盘、收起时展开键盘的效果,所以用一个私有变量定义本页的状态。
聊天页面的默认状态是收起界面,在展开页面时将moreView添加到页面中,并将对话框顶上去。
swift
private var isMore = false
swift
@objc func popMoreView(){
if isMore {
} else {
}
isMore = !isMore
}
用isMore = !isMore来切换页面的状态,每次点击时切换一次
swift
@objc func popMoreView(){
if isMore {
self.moreView.removeFromSuperview()
self.chatInputView.chattextField.becomeFirstResponder()
} else {
self.view.addSubview(self.moreView)
self.chatInputView.chattextField.resignFirstResponder()
UIView.animate(withDuration: 0.5) {
self.chatInputView.frame = .init(origin: .init(x: 0, y: SCREENHEIGHT - 100 - 98), size: self.chatInputView.frame.size)
self.chat_scrollView.frame = .init(origin: .init(x: 0, y: 100 - 98), size: self.chat_scrollView.frame.size)
}
}
isMore = !isMore
}
self.moreView.removeFromSuperview()
self.chatInputView.chattextField.becomeFirstResponder()
默认状态下,移除功能栏,并将输入框设置为第一响应器。
功能栏展开时,添加功能栏视图到界面,并重新设置输入框视图与滚动视图。
设置照片按钮功能
设置照片按钮功能,需要:
- 请求相册权限,检查相册权限,打开选择器获取照片,并关闭选择器。
- 设置照片的Cell,将照片转换成png并写入沙盒中。
- 根据照片比例重新渲染照片
- 在页面内创建照片的Cell,并将其发送到消息记录内
- 练习:点击照片消息时放大照片,再次点击则回到原来的页面
请求相册权限,检查相册权限,打开选择器获取照片,并关闭选择器
像请求语音权限一样设置请求与请求信息
Privacy - Photo Library Usage Description
我们需要您的允许来访问相册,以便选择照片。
使用相册/相机需要导入photos库
swift
import Photos
检查相册权限时,有三种情况:
- 已授权相册权限:获取照片
- 未授权相册权限:请求权限,成功则获取照片,否则提示无权限
- 拒绝授权:提示无权限
获取相册权限
swift
func openPhotoLib(){
let status = PHPhotoLibrary.authorizationStatus()
}
编写包括的所有情况
swift
let status = PHPhotoLibrary.authorizationStatus()
if status == .authorized {
//有权限时
DispatchQueue.main.async {
}
} else if status == .denied {
//拒绝(了)授权时
} else {
//无权限时,请求授权
PHPhotoLibrary.requestAuthorization { [weak self] status in
if status == .authorized {
} else {
}
}
}
}
- 使用
[weak self]:防止强引用循环,从而避免内存泄漏。 - 这个
status是闭包的参数,这里是要捕获status的状态才能继续操作,所以要加上参数。 - 安全访问 :使用
self?安全地访问self,防止在self已被释放时触发崩溃。
有权限时,打开选择器获取照片,编写**presentImagePickerController函数,设置获取源为相册**
swift
func presentImagePickerController(){
let imagePickerController = UIImagePickerController()
imagePickerController.delegate = self
imagePickerController.sourceType = .photoLibrary //设置源为相册
present(imagePickerController, animated: true, completion: nil)
}
let imagePickerController = UIImagePickerController()创建一个照片选择器
imagePickerController.delegate = self将delegate属性设置为当前类的实例,实现这些协议,可以处理用户选择的图片或视频,并响应选择器中的操作(例如取消选择或完成选择)
系统提示,为遵循协议,这意味着当前的视图控制器需要遵循 UIImagePickerControllerDelegate 和 UINavigationControllerDelegate 协议
imagePickerController.sourceType = .photoLibrary设置源为相册
present(imagePickerController, animated: true, completion: nil)将该选择器推入界面
无权限时,提示无权限,编写对应函数。
这个提示本质是一个提示框
swift
func showPermissionAlert(){
let alert = UIAlertController(title: "访问相册", message: "请在设置中允许访问相册", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "好的", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
let alert = UIAlertController(title: "访问相册", message: "请在设置中允许访问相册", preferredStyle: .alert)
创建一个 UIAlertController 的实例,命名为 alert。这个控制器是用于显示警告和提示对话框的
alert.addAction(UIAlertAction(title: "好的", style: .default, handler: nil))
向 alert 中添加一个操作按钮。这是 UIAlertAction 的实例
alert: 这是一个已经创建好的 UIAlertController 实例,用于显示警告或提示信息。
title: "好的": 这是按钮上显示的文本,当用户看到弹出框时,会看到这个中文"好的"按钮。
style: .default: 这是按钮的样式。.default 表示这是一个普通的按钮,用户点击后将触发相应的操作。还有其他样式,比如 .cancel 和 .destructive。
handler: nil: 这是一个闭包,在用户点击这个按钮时会被调用。由于这里设置为 nil,表示点击该按钮时不会执行任何操作。如果您需要在点击时执行特定的功能,可以传入一个闭包。
然后继续编写openPhotoLib(检查权限方法)
请求权限之后会进入异步线程,所以要回到主线程才能执行其他操作。
swift
func openPhotoLib(){
let status = PHPhotoLibrary.authorizationStatus()
if status == .authorized {
self.presentImagePickerController()
} else if status == .denied {
//拒绝
self.showPermissionAlert()
} else {
//未授权时,请求授权
PHPhotoLibrary.requestAuthorization { [weak self] status in
if status == .authorized {
DispatchQueue.main.async {
self?.presentImagePickerController()
}
} else {
DispatchQueue.main.async {
self?.showPermissionAlert()
}
}
}
}
}
选择完毕之后关闭选择器,输入didcancel即可弹出该函数。
swift
//关闭选择器
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
设置照片的Cell,将照片转换成png、修改尺寸。
照片消息由头像和照片按钮组成。
头像不变
swift
lazy var user_head: UIImageView = {
var user_head = UIImageView(frame: .init(origin: .init(x: SCREENWIDTH - 16 - 50, y: 0), size: .init(width: 50, height: 50)))
user_head.layer.cornerRadius = 25
user_head.layer.masksToBounds = true
return user_head
}()
图片按钮
swift
lazy var imgBtn: UIButton = {
var imgBtn = UIButton(frame: .init(origin: .init(x: SCREENWIDTH - (16 + 8 + 30), y: 0), size: .init(width: 30, height: 30)))
imgBtn.layer.cornerRadius = 8
imgBtn.layer.masksToBounds = true
imgBtn.backgroundColor = .systemGreen
//imgBtn.addTarget(self, action: #selector(expandThisImg), for: .touchUpInside)
return imgBtn
}()
16是边距,8是与消息框的距离,30是头像宽度
获取选择的图片地址,并用使用 Data(contentsOf:) 方法来获取图片,并对其重新渲染。
获取图片地址的方式同录音一致。该方法需要用到Model中存放的数据,所以把Model作为参数传进来。
swift
func reDraw(with chatModel: ChatTextModel){
}
获取资源名的函数非常常用,将其写在Public中。
swift
static func getDocumentsDirectory() -> URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
//获取存放文档的沙盒位置
static func fullFilePath(file_name: String) -> URL {
return getDocumentsDirectory().appendingPathComponent(file_name)
}
//获取文件名(URL)
那么我们需要先获取存储文档的沙盒地址,再拼起来获取图片文件名(当然,此时还未设置,但我们打算将文件名存在Model里的filePath中,就像语音一样,这里先写)。
Data(contentsOf:) 从指定的 URL 加载数据的一种方法。它非常适合快速读取文件内容,例如本地文件或网络资源。所以,contentsOf后面要传入的是一个URL。
顺手设置一下头像的图片,当然也可以在外面设。
swift
user_head.image = UIImage(named: chatModel.mineHead)
let data = try? Data(contentsOf: URL)
swift
let data = try? Data(contentsOf: Public.fullFilePath(file_name: chatModel.filePath))
使用try?避免获取文件失败。
现在获取了图片,使用从上一步加载的数据 (data),创建一个UIImage的实例。如果获取成功,则执行闭包内容。闭包就是重新设置图片属性的内容,暂不编写。
swift
if let image = UIImage(data: data ?? Data()){
}
data ?? Data() 确保如果 data 为 nil,则提供一个空的数据对象。避免了 UIImage(data:) 的崩溃,因为提供一个空的 Data 对象以防数据加载不成功。
根据图片比例重新设置图片宽高
内容比较复杂,额外开一个函数getSize来操作。既然是要根据图片重新设置宽高,需要用到的参数是图片img: UIImage返回的就是CGSize
使用static修饰,这样就可以在没有该类(ChatImageCell)实例的情况下,随意使用该方法。这里去掉static就不能在其他函数里随意调用它了,因为现在其实还没有ChatImageCell的实例。
swift
static func getSize(with img: UIImage) -> CGSize {
}
图片不可能无限大,先规定其最长的长度。使用static修饰因为这是一个不能被改变的量。
swift
static let maxLength = 186.0
重新设置宽高思路:
如果宽(width)大于高(height),最大值为宽,高用宽高比计算。
如果高(height)大于宽(width),最大值为高,宽用宽高比计算。
返回reDraw方法继续渲染图片
swift
if let image = UIImage(data: data ?? Data()){
let size = ChatImageCell.getSize(with: image)
self.imgBtn.frame = .init(origin: .init(x: SCREENWIDTH - 50 - 16 - 8 - size.width, y: 0), size: size)
self.imgBtn.setImage(image, for: .normal)
}
50是头像的宽,16是边距,8是头像与对话框的边距,size.width是自己的宽
返回ChatViewController,调用被用户选择的图片。
使用imagePickerController...didFinishPickingMediaWithInfo函数来调用这张图片。输入didFinish就能出来。
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any])该方法在用户选择图片后调用,主要就处理这张图片。
swift
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
picker.dismiss(animated: true)
}
使用picker.dismiss(animated: true)把选择框关掉。
获取原始图片
获取成功则执行闭包内操作
swift
if let image = info[.originalImage] as? UIImage { }
info: 这是一个字典,包含用户在图像选择器中所选择的媒体的信息。其键为UIImagePickerController.InfoKey类型,值为任意类型(Any)。info[.originalImage]:- 这里使用了
info字典的键UIImagePickerController.InfoKey.originalImage来访问用户所选的原始图像。 info[.originalImage]将返回一个可选值(Optional),它可能包含用户选择的图像(如果用户选择了图像)或者为nil(如果没有选择图像)。
- 这里使用了
as? UIImage:- 这一部分尝试将
info[.originalImage]的值转换为UIImage类型。 as?是一个"条件类型转换"的操作符,表示如果转换成功,它将返回一个非可选的UIImage;如果转换失败(即info[.originalImage]的值不是UIImage类型或者为nil),则image将为nil。
- 这一部分尝试将
将图片转换为PNG格式
let data = image.pngData() 用于将 UIImage 类型的图像转换为 PNG 格式的数据
swift
if let image = info[.originalImage] as? UIImage {
let data = image.pngData()
命名图片
前面Cell里我们需要获取这张图片的名字及URL,所以这里像命名语音一样命名用户选择的图片。
并将其拼成对应的URL。
swift
let img_name = Public.generateUniqueTimestamp() + ".png"
let url = Public.getDocumentsDirectory().appendingPathComponent(img_name)
将其写入沙盒中
write(to: url):是 Data 类型的方法,试图将该数据写入指定的文件路径(url),创建或覆盖目标文件。
考虑到写入失败,或获取不到图片的情况,使用try?和data?
swift
try? data?.write(to: url)
将图片添加到存储数据的Model中
将目标图片添加到Model中。注意其中type属性为"image",filePath是文件名,为计算得出的名字。
注:这里处理图片,并得出filePath名称之后,ChatImageCell中的reDraw方法就可以对图片进行重新渲染了。
swift
let messageID = "\(self.chatModels.count + 1)"
let model = chatModels[0]
let newmodel = ChatTextModel(chatID: self.messCellmodel.chatID, messageID: messageID, content: "", target: "mine", mineHead: model.mineHead, otherHead: model.otherHead, type: "image", filePath: img_name)
图片消息之间也要设置高度,到heightForRowAt函数设置cell之间的间距。
要设置间距,必须获得图片的高,这里图片的高度不是固定的,而是随着图片尺寸变化的。
1.用Data(contentsOf:)来获取路径下的图片→
swift
let data = try? Data(contentsOf: Public.fullFilePath(file_name: model.filePath)
2.创建为UIImage实例,这里使用if let值绑定,在闭包内处理两种情况(无图片和有图片时)
swift
if let image = UIImage(data: data ?? Data()) {}
3.获取图片高度,在图片高度加上间距(36)
swift
let size = ChatImageCell.getSize(with: image)
return size.height + 36
4.处理图片崩溃的情况
swift
if model.type == "image" {
let data = try? Data(contentsOf: Public.fullFilePath(file_name: model.filePath))
if let image = UIImage(data: data ?? Data()) {
let size = ChatImageCell.getSize(with: image)
return size.height + 36
} else {
return 30 + 36
}
创建每个图片信息Cell
swift
else {
let cell: ChatImageCell = tableView.cellForRow(at: indexPath) as? ChatImageCell ?? ChatImageCell(style: .default, reuseIdentifier: "ChatImageCell")
cell.reDraw(with: model)
cell.selectionStyle = .none
cell.backgroundColor = .clear
return cell
}
最终效果:


设置相机按钮功能
在系统处添加权限请求
Privacy - Camera Usage Description
检查相机权限,如无则请求相机权限openCamera
swift
func openCamera(){
let status = PHPhotoLibrary.authorizationStatus()
if status == .authorized {
self.camera_presentImagePickerController()
} else if status == .denied {
self.showPermissionAlert()
} else {
PHPhotoLibrary.requestAuthorization{ [weak self] status in
if status == .authorized {
DispatchQueue.main.async {
self?.camera_presentImagePickerController()
}
} else {
DispatchQueue.main.async {
self?.camera_presentImagePickerController()
}
}
}
}
}
有权限时,打开选择器获取照片,编写**camera_presentImagePickerController函数,设置获取源为相机**
从照相机获取图片
swift
func camera_presentImagePickerController(){
let camera_presentImagePickerController = UIImagePickerController()
camera_presentImagePickerController.delegate = self
camera_presentImagePickerController.sourceType = .camera //设置源为相机
present(camera_presentImagePickerController, animated: true, completion: nil)
}
由于通过相机选择的也是图片,所以这张图片同样会被didFinish...函数捕获,然后走和相册选择一样的流程。
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {}
把该功能绑定到点击按钮MoreView的索引处,确保点击图标时功能被调用。
swift
lazy var moreView: MoreView = {
let moreView = MoreView(frame: .init(x: 0, y: SCREENHEIGHT - 98 - bottomSafeArea, width: SCREENWIDTH, height: 98 + bottomSafeArea))
moreView.backgroundColor = .lightGray
moreView.btnClick = { [weak self] idx in
print(idx)
if idx == 0 {
self?.openPhotoLib()
} else if idx == 1 {
self?.openCamera()
} else {
}
}
效果图:

练习:点击发送的图片,放大
图片本身是按钮,点击时添加一个全屏的UIControl,并显示图片,再次点击时将其从父视图移除。
先新开一页设置一下这个自定义的UIControl,背景为纯黑,点击时从父视图移除。
objectivec
import UIKit
class EnlargeImg: UIControl {
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
func setupUI(){
self.backgroundColor = .black
self.addTarget(self, action: #selector(removeEnlargeImg), for: .touchUpInside)
}
@objc func removeEnlargeImg(){
self.removeFromSuperview()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
回到ChatViewController即为聊天主页面,使用参数为file_name的方法,执行放大图片与添加图片进UIControl的操作。
之所以把file_name设为参数,是因为使用cell中cilckblock进行回调的时候,也可以传递一个参数,而这个参数是用来读取文件名的,所以类型是String。
swift
func enlargeImage(file_name: String) {
}
通过图片地址获取图片,获取到图片之后,进行一些操作
swift
func enlargeImage(file_name: String) {
let data = try? Data(contentsOf: Public.fullFilePath(file_name: file_name))
if let image = UIImage(data: data ?? Data()){
//获取到图片之后,进行一些操作
}
}
现在获取到了图片,将其创建为UIImageView,图片设置为这张图
objectivec
let imgView = UIImageView(frame: .init(origin: .zero, size: .zero))
imgView.image = image
观察需求,图片放大后的宽度等于屏幕宽度SCREENWIDTH,而长度随比例适应变化。
也就是说放大后的宽度乘以比例就可以得出图片放大后的高度。
先取原来图片的宽高计算比例。
swift
let originalWidth = image.size.width
let originalHeight = image.size.height
let newHeight = (SCREENWIDTH / originalWidth) * originalHeight
再重新设置ImageView的size
swift
imgView.frame = .init(origin: .zero, size: .init(width: SCREENWIDTH, height: newHeight))
创建自定义UIControl**EnlargeImg的实例,将设置好的imgView添加进去,并居中放置(中心相同)**
swift
let bgControl = EnlargeImg(frame: .init(origin: .zero, size: .init(width: SCREENWIDTH, height: SCREENHEIGHT)))
bgControl.addSubview(imgView)
imgView.center = bgControl.center
最后将这个实例添加到主视图中
swift
func enlargeImage(file_name: String) {
let data = try? Data(contentsOf: Public.fullFilePath(file_name: file_name))
if let image = UIImage(data: data ?? Data()){
let imgView = UIImageView(frame: .init(origin: .zero, size: .zero))
imgView.image = image
let originalWidth = image.size.width
let originalHeight = image.size.height
let newHeight = (SCREENWIDTH / originalWidth) * originalHeight
imgView.frame = .init(origin: .zero, size: .init(width: SCREENWIDTH, height: newHeight))
let bgControl = EnlargeImg(frame: .init(origin: .zero, size: .init(width: SCREENWIDTH, height: SCREENHEIGHT)))
bgControl.addSubview(imgView)
imgView.center = bgControl.center
self.view.addSubview(bgControl)
}
}
在cell中使用ImageCell里设置好的空闭包clickBlock(同时也作为imgBtn的点击事件)。
然后调用刚刚设置好的函数传进这个空闭包里,并利用model将filePath作为参数传进函数中。
swift
else {
let cell: ChatImageCell = tableView.cellForRow(at: indexPath) as? ChatImageCell ?? ChatImageCell(style: .default, reuseIdentifier: "ChatImageCell")
cell.reDraw(with: model)
cell.clickBlock = { [weak self] in
self?.enlargeImage(file_name: model.filePath)
//把图片放大到全屏,点击消失,无需动画和其它功能
}
cell.selectionStyle = .none
cell.backgroundColor = .clear
return cell
}