如何在Apple的“照片”应用中实现UIScrollView / UICollectionView过滤器以使其打开得如此之快?

时间:2018-08-02 14:47:44

标签: ios swift xcode uiimage core-image

我不是在询问确切的代码,而是整体的想法。

这是我的问题:我正在尝试创建与“照片”应用中的过滤器选择UI相似的内容。我尝试了多种方法,但所有方法都有其缺点。

1)我尝试将OperationOperationQueue与集合视图一起使用,该视图启用了预取。这样可以快速加载viewController,但在滚动时会掉帧。

2)现在我正在使用滚动视图和GCD,但是它加载viewController的时间过长(因为它将所有过滤器同时应用到其内部的所有按钮),但是随后它平滑地滚动。

注意:要回答这个问题,无需阅读下面的部分(我相信),但是,如果您对我如何尝试实现功能感兴趣,欢迎阅读它。

对于所有过滤器的实现,我使用称为Filters的结构,该结构负责启动每个过滤器并将其附加到数组。

struct Filters {
var image: UIImage
var allFilters: [CIFilter] = []

init(image: UIImage) {

    self.image = image

    guard  let sepia = Sepia(image: image) else {return}

    allFilters.append(contentsOf: [sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia, sepia])

       }
  }

现在我只使用一个过滤器。 SepiaCIFilter的子类。我将其创建为子类,因为将来我将根据它创建一个自定义类。这是它的实现:

class Sepia: CIFilter {

var inputImage: CIImage?
var inputIntensity: NSNumber?

@objc override var filterName: String? {
    return NSLocalizedString("Sepia", comment: "Name of a Filter")
}

convenience init?(image: UIImage, inputIntensity: NSNumber? = nil) {
    self.init()

    guard let cgImage = image.cgImage else {
        return nil
    }

    if inputIntensity != nil {
        self.inputIntensity = inputIntensity
    } else {
        self.setDefaults()
    }

    let inputImage = CIImage(cgImage: cgImage)
    self.inputImage = inputImage
}

override func setDefaults() {
    inputIntensity = 1.0
}

override var outputImage: CIImage? {
    guard let inputImage = inputImage, let inputIntensity = inputIntensity else {
        return nil
    }

    let filter = CIFilter(name: "CISepiaTone", withInputParameters: [kCIInputImageKey: inputImage, kCIInputIntensityKey: inputIntensity])

   return filter?.outputImage

  }
}

在viewController的viewDidLoad中启动Filters结构:

self.filters = Filters(image: image)

然后,我调用一个方法,该方法根据filterViews数组中过滤器的数量来配置一些视图(filters.allFilters),并对其进行迭代,并调用采用缩略图UIImage的方法并对其应用过滤器,然后在完成处理程序中将其返回(出于调试原因,我在其中使用DispatchGroup)。这是将过滤器应用于缩略图的方法:

func imageWithFilter(filter: CIFilter, completion: @escaping(UIImage?)->Void) {

    let group = DispatchGroup()
    group.enter()
    DispatchQueue.global().async {
        guard let outputImage = filter.value(forKey: kCIOutputImageKey) as? CIImage, let cgImageResult = self.context.createCGImage(outputImage, from: outputImage.extent) else  {
            DispatchQueue.main.async {
                 completion(nil)
            }
            group.leave()
            return
        }

        let filteredImage = UIImage(cgImage: cgImageResult)
        DispatchQueue.main.async {
            print (filteredImage)
            completion(filteredImage)

        }

       group.leave()
    }

    group.notify(queue: .main) {
        print ("Filteres are set")
    }
}

上面的打印语句和经过过滤的图像地址很快就会打印出来,但是图像不会出现在视图中。

我尝试使用Time Profiler,但这给了我一些奇怪的结果。例如,它显示以下内容需要很长时间才能在backtrace的根目录中执行: enter image description here

当我尝试查看Xcode中的代码时,得到以下内容,但并没有太大帮助: enter image description here

所以,这就是问题所在。如果您对如何在“照片”应用中实现如此快速和快速的响应有任何想法,或者对我的实现有任何建议,非常感谢您的帮助。

1 个答案:

答案 0 :(得分:4)

问题似乎是如何尽可能快地显示由Core Image CIFilter生成的CIImage-如此之快,以至于它在视图控制器出现时立即出现。实际上,它是如此之快,以至于用户可以使用滑块等来调整CIFilter参数,并且图像将重新实时显示并保持调整。

答案是使用Metal Kit,尤其是MTKView。渲染工作已移至设备的GPU上,并且速度极快,足够快,可以进入设备屏幕的刷新率,因此在用户旋转滑块时不会出现明显的延迟。

我有一个简单的演示,其中用户应用了一个称为VignetteFilter的自定义过滤器链:

enter image description here

随着用户滑动滑块,渐晕量(内圆)会平滑变化。每次滑动时,都会在原始图像上应用新的滤镜,并在用户滑动滑块时一遍又一遍地渲染滤镜,并与用户的运动保持同步。

如我所说,底部的视图是MTKView。通过这种方式使用MTKView并不难。它确实需要做一些准备,但这都是样板。唯一棘手的部分是实际上将图像显示在您想要的位置。

这是我的视图控制器的代码(除了滑块和过滤后的图像的显示,我省略了所有内容):

class EditingViewController: UIViewController, MTKViewDelegate {
    @IBOutlet weak var slider: UISlider!
    @IBOutlet weak var mtkview: MTKView!

    var context : CIContext!
    let displayImage : CIImage! // must be set before viewDidLoad
    let vig = VignetteFilter()
    var queue: MTLCommandQueue!

    // slider value changed
    @IBAction func doSlider(_ sender: Any?) {
        self.mtkview.setNeedsDisplay()
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // preparation, all pure boilerplate

        self.mtkview.isOpaque = false // otherwise background is black
        // must have a "device"
        guard let device = MTLCreateSystemDefaultDevice() else {
            return
        }
        self.mtkview.device = device

        // mode: draw on demand
        self.mtkview.isPaused = true
        self.mtkview.enableSetNeedsDisplay = true

        self.context = CIContext(mtlDevice: device)
        self.queue = device.makeCommandQueue()

        self.mtkview.delegate = self
        self.mtkview.setNeedsDisplay()
    }

    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
    }

    func draw(in view: MTKView) {
        // run the displayImage thru the CIFilter
        self.vig.setValue(self.displayImage, forKey: "inputImage")
        let val = Double(self.slider.value)
        self.vig.setValue(val, forKey:"inputPercentage")
        var output = self.vig.outputImage!

        // okay, `output` is the CIImage we want to display
        // scale it down to aspect-fit inside the MTKView
        var r = view.bounds
        r.size = view.drawableSize
        r = AVMakeRect(aspectRatio: output.extent.size, insideRect: r)
        output = output.transformed(by: CGAffineTransform(
            scaleX: r.size.width/output.extent.size.width, 
            y: r.size.height/output.extent.size.height))
        let x = -r.origin.x
        let y = -r.origin.y

        // minimal dance required in order to draw: render, present, commit
        let buffer = self.queue.makeCommandBuffer()!
        self.context!.render(output,
            to: view.currentDrawable!.texture,
            commandBuffer: buffer,
            bounds: CGRect(origin:CGPoint(x:x, y:y), size:view.drawableSize),
            colorSpace: CGColorSpaceCreateDeviceRGB())
        buffer.present(view.currentDrawable!)
        buffer.commit()
    }
}