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
layer?.contents = image- CALayer can display any image directly.resizeAspectFill- Scales the image to fill the layer, cropping if needed.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
.kfproperty to FillImageView - The
.kfproperty returnsKingfisherWrapper<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 FillImageViewbase- Access to the actual FillImageView instanceKingfisherManager.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.
Start the conversation