How to make scale to fill NSImageView with remote Kingfisher image

Issue #1006

When building macOS apps, you often need images that fill their container while keeping their aspect ratio - like cover photos or thumbnails. NSImageView doesn’t do this well out of the box. Here’s how to build a custom solution that works great with Kingfisher.

NSImageView has scaling options, but none of them give us true “aspect fill” behavior - where the image fills the entire view and crops the overflow. You might try .scaleProportionallyUpOrDown, but it leaves empty space instead of filling the container.

Instead of fighting with NSImageView, we can use a plain NSView with a CALayer. Layers have a contentsGravity property that supports .resizeAspectFill - exactly what we need.

final class FillImageView: NSView {
    var image: NSImage? {
        didSet {
            layer?.contents = image
        }
    }

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)

        // Create and configure the layer
        layer = CALayer()
        layer?.contentsGravity = .resizeAspectFill
        layer?.masksToBounds = true  // Clip the overflow
        wantsLayer = true
    }

    required init?(coder: NSCoder) {
        fatalError()
    }
}

How it works

  1. layer?.contents = image - CALayer can display any image directly
  2. .resizeAspectFill - Scales the image to fill the layer, cropping if needed
  3. .masksToBounds = true - Clips anything outside the layer bounds

Making It Work with Kingfisher

Kingfisher’s .kf extension only works on NSImageView by default. Since FillImageView is a plain NSView, we need to add support ourselves.

Understanding Kingfisher’s Protocol System

Kingfisher uses a clever protocol-based design to add the .kf namespace to views:

KingfisherCompatible (protocol)
  KingfisherWrapper<Base> (generic struct)
   Your extensions on KingfisherWrapper

KingfisherCompatible - A protocol that adds a .kf property to any type. When you conform to it, you get access to view.kf which returns a KingfisherWrapper<YourViewType>.

KingfisherWrapper - A generic struct that wraps your view. It has a base property that points back to your original view. You add your image-loading methods as extensions on this wrapper.

This pattern keeps Kingfisher’s methods in their own namespace (.kf) instead of polluting your view’s API directly.

Step 1: Conform to KingfisherCompatible

import Kingfisher

extension FillImageView: KingfisherCompatible {}

This single line does a lot:

  • Adds a .kf property to FillImageView
  • The .kf property returns KingfisherWrapper<FillImageView>
  • Now you can write extensions on that wrapper

Step 2: Add the KingfisherWrapper Extension

extension KingfisherWrapper where Base: FillImageView {
    @discardableResult
    func setImage(
        with url: URL?,
        placeholder: NSImage? = nil,
        options: KingfisherOptionsInfo? = nil,
        completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil
    ) -> DownloadTask? {
        // 'base' is our FillImageView instance
        guard let url else {
            base.image = placeholder
            return nil
        }

        // Use KingfisherManager for caching and downloading
        return KingfisherManager.shared.retrieveImage(with: url, options: options ?? []) { result in
            Task { @MainActor in
                switch result {
                case .success(let value):
                    self.base.image = value.image  // Set image on our view
                case .failure:
                    self.base.image = placeholder
                }
                completionHandler?(result)
            }
        }
    }

    func cancelDownloadTask() {
        KingfisherManager.shared.downloader.cancelAll()
    }
}

Key points:

  • where Base: FillImageView - This extension only applies when the wrapper contains a FillImageView
  • base - Access to the actual FillImageView instance
  • KingfisherManager.shared - Handles all the caching, downloading, and memory management
  • @MainActor - UI updates must happen on the main thread

Step 3: Use It Like Any Other Kingfisher View

let imageView = FillImageView()

// Load remote image with caching
imageView.kf.setImage(with: imageURL)

// With placeholder
imageView.kf.setImage(with: imageURL, placeholder: NSImage(named: "placeholder"))

// With completion handler
imageView.kf.setImage(with: imageURL) { result in
    switch result {
    case .success(let value):
        print("Image loaded: \(value.source)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

// Cancel when view disappears
imageView.kf.cancelDownloadTask()

Usage in Collection View Cells

FillImageView works great in collection views where you need fast scrolling:

class ImageCell: NSCollectionViewItem {
    private let fillImageView = FillImageView()

    override func prepareForReuse() {
        super.prepareForReuse()
        fillImageView.kf.cancelDownloadTask()
        fillImageView.image = nil
    }

    func configure(with url: URL) {
        fillImageView.kf.setImage(with: url)
    }
}

This gives you a fast, reusable image view that works seamlessly with Kingfisher’s caching and download management.

Written by

I’m open source contributor, writer, speaker and product maker.

Start the conversation