SwiftUI使用PhotoUI

688 阅读1分钟

SwiftUI使用PhotoUI

在wwdc2022上,苹果让SwiftUI支持了PhotoUI。PhotoUI是属于PhotoKit的一个组件,使用PhotoUI,可以很方便的实现选择图片的功能。

使用PhotoUI选择文件需要处理三个步骤:

步骤一: 使用PhotoPicker打开图片选择视窗。

@State var photoPickerItem: PhotosPickerItem? = nil

var body: some View {
  PhotoPicker(selection: $photoPickerItem) {
    Text("tap to pick")
  }
}

PhotoPicker有许多的构造器,可以设置单选还是多选,可以设置允许选择的图片数量,可以设置图片(或视频)类型。

具体的设置可以查看官方文档

PhotoPicker实质上是一个按钮,所以可以使用buttonStyle对PhotoPicker进行样式设置。

Text部分则是按钮的Label。

步骤二: 创建一个Transferable对象用于处理图片转换。

因为PhotoPicker获取到的仅是图片的索引(官方称之为Placeholder),实际获取到图片还需要将图片加载到应用中,有时候由于图片尺寸过大或者图片位于iCloud上需要等待下载,或者是请求到视频文件(需要做类型适配)等等原因,所以需要一个转换器对索引进行转换。

struct ImageTransfer: Transferable {
  let image: Image
  enum TransferError: Error {
    case importFailed
  }
  
  static var transferRepresentation: some TransferRepresentation {
    DataRepresentation(importedContentType: .image) { data in
      #if canImport(AppKit)
        guard let nsImage = NSImage(data: data) else {
          throw TransferError.importFailed
        }
        let image = Image(nsImage: nsImage)
        return TransferImage(image: image)
      #elseif canImport(UIKit)
        guard let uiImage = UIImage(data: data) else {
          throw TransferError.importFailed
        }
        let image = Image(uiImage: uiImage)
        return TransferImage(image: image)
      #else
        throw TransferError.importFailed
      #endif
    }
  }
}

这里使用了宏编程的方法处理各个平台的图片

步骤三: 在PhotoPicker获取到图片索引后解析图片

enum ImageState {
  case empty
  case loading(Progress)
  case success(Image)
  case failure(Error)
}

@State
var imageState: ImageState = .empty

首先创建一个imageState来存放解析过程,后面会通过这个imageState的不同阶段显示不同的视图。

private func loadTransferable(from photoPickerItem: PhotoPickerItem) -> Progress {
  return photoPickerItem.loadTransferable(type: ImageTransfer.self) { res in
    DispatchQueue.main.async {
      guard let photoPickerItem = self.photoPickerItem else {
        print("Failed to get the selected item.")
        return 
      }
      
      switch res {
        case .success(let transferImage?):
          self.imageState = .success(transferImage.image)
        case .success(nil):
          self.imageState = .empty
        case .failure(let error):
          self.imageState = .failure(error)
      }
    }
  }
}

然后创建一个转换函数用于处理转换。这里使用了异步方法来处理转换,以免阻塞视图渲染。

PhotoPicker(selection: $photoPickerItem) {
  Text("tap to pick")
}
  .onChange(of: photoPickerItem) {
    if let photoPickerItem = $0 {
      let progress = loadTransferable(from: photoPickerItem)
      imageState = .loading(progress)
    } else {
      imageState = .empty
    }
  }

最后在选择图片后调用转换函数处理图片。

可以看到,我们是通过photoPickerItem的属性方法来触发转换。然后将imageState置为loading,等待转换处理完成后,根据处理的结果再将imageState切换为对应的状态。

最后一步: 根据imageState显示对应的视图

VStack {
  PhotoPicker(selection: $photoPickerItem) {
    Text("tap to pick")
  }
    .onChange(of: photoPickerItem) {
      if let photoPickerItem = $0 {
        let progress = loadTransferable(from: photoPickerItem)
        imageState = .loading(progress)
      } else {
        imageState = .empty
      }
    }
    
  switch imageState {
    case .success(let image):
      image.resizable()
           .aspectRatio(contentMode: .fit)
    case .loading:
      ProgressView()
    case .empth:
      Image(systemName: "photo.fill")
          .font(.system(size: 40)
          .foregroundColor(.gray)
    case .failure:
      Image(systemName: "rectangle.slash.fill")
          .font(.system(size: 40)
          .foregroundColor(.gray)
  }
}

这部分就比较简单了,就不展开了。

完整代码:

struct ContentView: View {
    @State
    var photoPickerItem: PhotosPickerItem? = nil
    
    @State
    var imageState: ImageState = .empty {
        didSet {
            print("imageState: ", imageState)
        }
    }
    
    var body: some View {
        VStack {
            PhotosPicker(selection: $photoPickerItem) {
                Text("tap to pick")
            }
            .onChange(of: photoPickerItem) { newValue in
                if let photoPickerItem {
                    let progress = loadTransferable(from: photoPickerItem)
                    imageState = .loading(progress)
                } else {
                    imageState = .empty
                }
            }
            
            switch imageState {
            case .success(let image):
                image.resizable()
                    .aspectRatio(contentMode: .fit)
            case .loading:
                ProgressView()
            case .empty:
                Image(systemName: "photo.fill")
                    .font(.system(size: 40))
                    .foregroundColor(.blue)
            case .failure:
                Image(systemName: "rectangle.slash.fill")
                    .font(.system(size: 40))
                    .foregroundColor(.blue)
            }
            
        }
    }
    
    enum ImageState {
        case empty
        case loading(Progress)
        case success(Image)
        case failure(Error)
    }
    
    struct ImageTransfer: Transferable {
        let image: Image
        
        enum TransferError: Error {
            case importFailed
        }
        
        static var transferRepresentation: some TransferRepresentation {
            DataRepresentation(importedContentType: .image) { data in
                #if canImport(AppKit)
                guard let nsImage = NSImage(data: data) else {
                    throw TransferError.importFailed
                }
                
                let image = Image(nsImage: nsImage)
                return TransferImage(image: image)
                #elseif canImport(UIKit)
                guard let uiImage = UIImage(data: data) else {
                    throw TransferError.importFailed
                }
                
                let image = Image(uiImage: uiImage)
                return ImageTransfer(image: image)
                #else
                throw TransferError.importFailed
                #endif
            }
        }
    }
    
    private func loadTransferable(from photoPickerItem: PhotosPickerItem) -> Progress {
        print("loadTransferable")
        return photoPickerItem.loadTransferable(type: ImageTransfer.self) { res in
            DispatchQueue.main.async {
                guard let photoPickerItem = self.photoPickerItem else {
                    print("Failed to get the selected item.")
                    return
                }
                
                switch res {
                case .success(let transferImage?):
                    self.imageState = .success(transferImage.image)
                case .success(nil):
                    self.imageState = .empty
                case .failure(let error):
                    self.imageState = .failure(error)
                }
            }
        }
    }
}