Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save jaywardell/c5806f26e9bdacf730c4629063289e7d to your computer and use it in GitHub Desktop.

Select an option

Save jaywardell/c5806f26e9bdacf730c4629063289e7d to your computer and use it in GitHub Desktop.
// A Simple AppKit NSViewControllerRepresentable to wrap a NSViewCOntroller subclass that presents a NSImageView in a NSScrollVIew.
// This was done more or less for educational purposes for myself, but maybe someone will find it helpful
public struct ImageRepresentable: NSViewControllerRepresentable {
public typealias NSViewControllerType = ImageViewController
let image: NSImage
let minSize: CGSize?
init(image: NSImage, minSize: CGSize? = nil) {
self.image = image
self.minSize = minSize
}
@Environment(\.dismiss) var dismiss: DismissAction
static var urlIndicator: String { String(describing: Self.self) }
@State private var viewController: ImageViewController?
fileprivate func closeWindow() {
dismiss()
}
public func makeNSViewController(context: Context) -> ImageViewController {
ImageViewController(image)
}
public func updateNSViewController(_ uiViewController: ImageViewController, context: Context) {
}
@available(macOS 14.0, *)
@MainActor @preconcurrency public func sizeThatFits(_ proposal: ProposedViewSize, nsViewController: ImageViewController, context: Self.Context) -> CGSize? {
let viewSize = nsViewController.scroller.contentSize
guard let screenSize = NSScreen.main?.frame.size else {
// this shouldn't really happen, but to be safe..
return proposal.replacingUnspecifiedDimensions(by: viewSize)
}
let imageSize = nsViewController.scroller.documentView?.bounds.size ?? image.size
var sizes = [
screenSize,
imageSize,
]
// if the image is smaller than the screen, then the image size is the smallest it can get
if let minSize,
imageSize.width > screenSize.width ||
imageSize.height > screenSize.height {
sizes.append(minSize)
}
let ideal = sizes.reduce(screenSize) {
CGSize(width: min($0.width, $1.width),
height: min($0.height, $1.height))
}
// don't allow a zero size ever
if proposal == .zero {
return ideal
}
return proposal.replacingUnspecifiedDimensions(by: ideal)
}
}
public class ImageViewController: NSViewController {
let image: NSImage
init(_ image: NSImage) {
self.image = image
super.init(nibName: nil, bundle: nil)
self.representedObject = image
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private(set) var scroller: NSScrollView!
public override func loadView() {
let imageRect = CGRect(origin: .zero, size: image.size)
let imageView = NSImageView(frame: imageRect)
imageView.bounds = imageRect
imageView.image = image
self.scroller = NSScrollView()
scroller.hasHorizontalScroller = true
scroller.hasVerticalScroller = true
scroller.borderType = .noBorder
scroller.addSubview(imageView)
scroller.documentView = imageView
scroller.allowsMagnification = true
self.view = scroller
view.frame = CGRect(origin: .zero, size: image.size)
}
public override func viewWillAppear() {
super.viewWillAppear()
// NOTE: AppKit uses a Quadrant I coordinate system, so the top of the image is at the highest number
// NOTE: This code has no effect if called in viewDidLoad().
// It's not until viewWillAppear() that the view is setup in such a way that it works
let top = NSPoint(x: 0, y: image.size.height)
scroller?.documentView?.scroll(top)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment