当前位置: 首页 > 工具软件 > Swift Sandbox > 使用案例 >

SwiftUI 使用相册的实践

林英武
2023-12-01

目标与计划

目标

假设一个场景,我们的app需要图片,这个图片的来源是相册或者通过拍照获取的,并且要将图片保留在app中,并且希望app打开时能遍历到某个目录下的所有图片,并且加载,以供我们的app使用。

计划

构建一个View,在这个View中选择相册中的图片,或者通过相机拍照
选中的图片保存到app中,并且在app打开时加载指定目录的图片
构造一个外层容器用来装载这个View
计划完成,开始动手写代码

实现

构建从相册/相机选择图片的View

SwiftUI没有这个样的控件,需要使用UIKit中的UIImage这个控件,那么这里需要一个技术,SwifiUI桥接UIKit控件。那么现在不讲原理,只讲如何实现。
定义一个Struct继承UIViewControllerRepresentable,在继承时,需要实现2个func,makeUIViewControllerupdateUIViewController一个是在构造时调用一个是在刷新时被调用.在这个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中

将图片保存到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。

总结

  1. 在这个实践中有以下几个知识点需要了解
  2. SwiftUI如何与UIKit桥接,这个可以apple developer的官方文档和实例。
  3. UIImagePickerController 的使用
  4. ios沙箱文件的概念,以及如何操作。UIImage获取png,可以很简单的通过image.pngData()!获取
  5. sheet的使用
 类似资料: