假设一个场景,我们的app需要图片,这个图片的来源是相册或者通过拍照获取的,并且要将图片保留在app中,并且希望app打开时能遍历到某个目录下的所有图片,并且加载,以供我们的app使用。
构建一个View,在这个View中选择相册中的图片,或者通过相机拍照
选中的图片保存到app中,并且在app打开时加载指定目录的图片
构造一个外层容器用来装载这个View
计划完成,开始动手写代码
SwiftUI没有这个样的控件,需要使用UIKit中的UIImage这个控件,那么这里需要一个技术,SwifiUI桥接UIKit控件。那么现在不讲原理,只讲如何实现。
定义一个Struct继承UIViewControllerRepresentable,在继承时,需要实现2个func,makeUIViewController和updateUIViewController一个是在构造时调用一个是在刷新时被调用.在这个Struct中可以打开相册/相机 (使用摄像头需要在info.plist中添加一个Privacy - Camera Usage Description 的授权描述)
struct ImagePicker: UIViewControllerRepresentable {
@Binding var sourceType:UIImagePickerController.SourceType
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker=UIImagePickerController()
picker.allowsEditing=false
picker.sourceType=sourceType
print("picker source \(sourceType.rawValue)")
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
}
}
在这段代码中定义了一个ImagePicker的View,继承了UIViewControllerRepresentable,添加了一个sourceType的属性,用来控制图片来源(相册还是相机)。在makeUIViewController函数中,创建UIImagePickerController的实例,并且设置图片是不可编辑的,设置了来源。
我们完成了一个打开相册或者相机的View。但打开之后,选择图片是没有任何反应的,这个就需要我们做另一个事情,添加一个代理处理选照片的这个事件。(具体原理需可查阅UIKit相关文档。这里只实现功能),定义一个类继承NSObject,UINavigationControllerDelegate,UIImagePickerControllerDelegate。
struct ImagePicker: UIViewControllerRepresentable {
@Binding var sourceType:UIImagePickerController.SourceType
let handlerImage:(_ image:UIImage)->Void
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker=UIImagePickerController()
picker.allowsEditing=false
picker.delegate=context.coordinator
picker.sourceType=sourceType
print("picker source \(sourceType.rawValue)")
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
}
class Coordinator:NSObject,UINavigationControllerDelegate,UIImagePickerControllerDelegate{
let parent:ImagePicker
init(_ parent:ImagePicker) {
self.parent=parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]){
if let image=info[.originalImage] as? UIImage{
parent.handlerImage(image)
if let p=info[.imageURL] {
print("image = \(p)")
}
}
}
}
}
代码中定义了Coordinator这个类,继承NSObject,UINavigationControllerDelegate,UIImagePickerControllerDelegate,这个类是ImagePicker的内部类,ImagePicker选中图片时会回调imagePickerController这个函数,info这个参数代表选中的图片,可以将info[.originalImage]转换成UIImage。保存图片到app中是调用了ImagePicker中的一个函数处理的,代码体现在parent.handlerImage(image)这里。
将Coordinator和ImagePicker关联起来的方法,在Coordinator添加一个指向ImagePicker 属性,并且在初始化时赋值;在ImagePicker中增加makeCoordinator这个函数,这个函数中创建Coordinator对象,并且将自己赋值给Coordinator的ImagePicker属性
这样我们完成了一个从相册/相机选择图片的View。下一步我们将选择的图片保存到app中
先了解ios的关于文件概念。在ios系统中有一个沙箱(sandbox)的概念,app自身使用的文件都是在这个沙箱中,我们只需要打开沙箱,找到目录保存文件或者找到文件加载进来。沙箱中有4个目录Documents,Library,SystemData.tmp。我们需要将图片保存在documents这个目录下。
先找到沙箱目录
let filePath=NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last!
文件管理工具
let filemanager=FileManager.default
图片写入
try! filemanager.createDirectory(atPath: dir, withIntermediateDirectories: true, attributes: nil)
let data:Data=image.pngData()!
try! data.write(to: URL(fileURLWithPath: filename))
第一步创建目录,第二步从UIImage对象获取NSData对象,第三步写入文件
完整的代码
static func saveImage(image:UIImage)->String{
let filePath=NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last!
let dir=filePath+"/images/"+BuildingPropertyDir+"/"
let filemanager=FileManager.default
try! filemanager.createDirectory(atPath: dir, withIntermediateDirectories: true, attributes: nil)
let now=Date()
let formatter=DateFormatter()
formatter.dateFormat="YYYYMMddHHmmss"
let time=formatter.string(from: now)
let fn=time+".png"
let filename=dir+fn
let data:Data=image.pngData()!
try! data.write(to: URL(fileURLWithPath: filename))
// print("filename \(filename)")
return fn
}
参数是UIImage,在Documents 目录下创建一个images+BuildingPropertyDir 的目录,按照时间生成文件名,将图片转成png文件,写入沙盒中,返回文件名。
static func loadImages()->[(String,UIImage)]{
var images:[(String,UIImage)]=[]
let filePath=NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last!
let dir=filePath+"/images/"+BuildingPropertyDir
let manager=FileManager.default
let files=try! manager.contentsOfDirectory(atPath: dir)
for f in files{
let filename=dir+"/"+f
if let image=UIImage(contentsOfFile: filename){
let i=(f,image)
images.append(i)
}
}
return images
}
第一步先找到沙盒目录。拼接需要加在的目录,第二步获取文件管理器FileManager.default,获取所有的文件名称,第三步,通过文件名称加载图片image=UIImage(contentsOfFile: filename),返回 (文件名称,UIImage) 的元数据数组。
@State private var showPic=false
@State private var sourceType:UIImagePickerController.SourceType=UIImagePickerController.SourceType.camera
var body: some View {
VStack(){
Button("添加图片"){
sourceType=UIImagePickerController.SourceType.photoLibrary
self.showPic.toggle()
}
Button("拍照"){
sourceType=UIImagePickerController.SourceType.camera
self.showPic.toggle()
}
}.frame(width: 300, height: 400, alignment: .center).sheet(isPresented: $showPic,onDismiss:{
print("dimiss")
}){
HStack(){
ImagePicker(sourceType:$sourceType){
image in
let id=saveImage(image: image)
showPic=false
}
}
}
}
首先定义一个View,中间有2个button,一个是添加图片,一个是拍照。当我们点击其中一个按钮时,弹出一个sheet,相册或者是相机(ImagePicker)被放置在这个sheet中,sheet的出现和消失是通过showPic这个变量控制**(isPresented: $showPic),ImagePicker是相册还是相机是通过@State private var sourceType** 控制,这2个变量在button点击事件中被设置成对应的数值。在ImagePicker构造是传入了一个闭函数赋值给ImagePicker的handlerImage变量,用来处理选中的图片既保存图片到沙盒中,这里调用了saveImage这个函数。并且设置showPic为false,sheet就dismiss。