Exhibition/StandaloneImageWindowController.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Contains a simple `NSWindowController` subclass to display a floating `ImageFile` preview. |
*/ |
import Cocoa |
/** |
`StandaloneImageWindowController` is a window controller that displays a floating |
`ImageFile` preview. It is required to follow its aspect ratio in windowed mode, |
but allows that strict sizing to be broken in fullscreen. Demonstrates allowing |
specialized window sizes to be tileable. |
*/ |
class StandaloneImageWindowController: NSWindowController, NSWindowDelegate { |
// MARK: Properties |
override var windowNibName: String { |
return "StandaloneImageWindowController" |
} |
@IBOutlet var imageView: NSImageView! |
@IBOutlet var aspectRatioConstraint: NSLayoutConstraint! |
@IBOutlet var bottomConstraint: NSLayoutConstraint! |
@IBOutlet var leadingConstraint: NSLayoutConstraint! |
/// The `ImageFile` presented by the receiver. |
var imageFile: ImageFile? { |
didSet { |
/* |
If the window is already loaded, update the image view with the |
new set image. |
*/ |
if isWindowLoaded { |
updateImageViewWithImageFile(imageFile) |
} |
/* |
Update the mapping from presented `ImageFile` to presenting |
`StandaloneImageWindowController` when the `image` changes. |
*/ |
if let oldImage = oldValue { |
StandaloneImageWindowController.imageFileToPresentingStandaloneController[oldImage] = nil |
} |
if let newImage = imageFile { |
StandaloneImageWindowController.imageFileToPresentingStandaloneController[newImage] = self |
} |
} |
} |
// MARK: Life Cycle |
override func windowDidLoad() { |
super.windowDidLoad() |
guard let window = window else { |
fatalError("`window` is expected to be non nil by this time.") |
} |
// Hide the titlebar and title. |
window.titlebarAppearsTransparent = true |
window.titleVisibility = .hidden |
// Allow the window to be dragged anywhere. |
window.isMovableByWindowBackground = true |
// Make the window dark, giving it a dark background and fullscreen titlebar |
window.appearance = NSAppearance(named: NSAppearanceNameVibrantDark) |
/* |
Even thought the window is not fullscreen capable or allowed to have |
certain sizes when windowed, always allow it to go into a tile. Its |
constraints will be tweaked before entering fullscreen, see |
`windowWillEnterFullscreen()`. |
*/ |
window.collectionBehavior = [window.collectionBehavior, .fullScreenAllowsTiling] |
window.minFullScreenContentSize = NSSize(width: 1.0, height: 1.0) |
window.maxFullScreenContentSize = NSSize(width: 10000.0, height: 10000.0) |
updateImageViewWithImageFile(imageFile) |
} |
fileprivate func updateImageViewWithImageFile(_ imageFile: ImageFile?) { |
// Set the window's title to be the new `ImageFile` name, empty if the no image file. |
window?.title = imageFile?.fileNameExcludingExtension ?? "" |
if let imageFile = imageFile { |
imageFile.fetchImageWithCompletionHandler { image in |
/* |
Verify that the loaded image representation is from the currently |
set `ImageFile`. |
*/ |
if self.imageFile == imageFile { |
self.imageView?.image = image |
/* |
Calculate the aspect ratio of the newly set image and update |
the constraint from that. |
*/ |
let aspectRatio = image.size.width / image.size.height |
self.updateAspectRatioConstraintWithAspectRatio(aspectRatio) |
} |
} |
} |
else { |
imageView.image = nil |
updateAspectRatioConstraintWithAspectRatio(1.0) |
} |
} |
/// Updates the `imageView`'s aspect ratio constraint with a new aspect ratio. |
fileprivate func updateAspectRatioConstraintWithAspectRatio(_ aspectRatio: CGFloat) { |
/* |
Deactivate the existing aspect ratio constraint and create a new one |
with the newly calculated aspect ratio. |
*/ |
aspectRatioConstraint.isActive = false |
aspectRatioConstraint = imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: aspectRatio) |
aspectRatioConstraint.identifier = "StandaloneImageWindowController.AspectRatio" |
aspectRatioConstraint.isActive = true |
} |
// MARK: Full Screen Notifications |
func windowWillEnterFullScreen(_ notification: Notification) { |
/* |
Lower the bottom constraint priority so that the window is not required |
to be sized tightly to the aspect ratio constrained image. When a tile, |
it typically will not follow that aspect ratio sizing. |
*/ |
bottomConstraint.priority = 200.0 |
leadingConstraint.priority = 200.0 |
// Show the title while in fullscreen (when the titlebar is revealed on menubar hover). |
window?.titleVisibility = .visible |
} |
func windowWillExitFullScreen(_ notification: Notification) { |
// Reinforce the strict aspect ratio sizing of the window. |
bottomConstraint.priority = 999.0 |
leadingConstraint.priority = 999.0 |
// Rehide the title. |
window?.titleVisibility = .hidden |
} |
// MARK: ImageFile Presentation Mapping |
/** |
The mapping from presented `ImageFile` to presenting `StandaloneImageWindowController`. |
This also keeps the presenting window controllers alive for the duration |
of the presentation. |
*/ |
fileprivate static var imageFileToPresentingStandaloneController = [ImageFile: StandaloneImageWindowController]() |
/** |
Returns a `StandaloneImageWindowController` for the `ImageFile`. If a window |
controller is already presenting that `ImageFile`, it returns the existing |
one, otherwise creates a new one. |
*/ |
fileprivate class func standaloneImageWindowControllerForImage(_ imageFile: ImageFile) -> StandaloneImageWindowController { |
let imageController: StandaloneImageWindowController |
if let existingImageController = StandaloneImageWindowController.imageFileToPresentingStandaloneController[imageFile] { |
// If there is an existing controller for that image, reuse it and just reshow it. |
imageController = existingImageController |
} |
else { |
/* |
Otherwise create a new one. Setting the `image` will associate it |
with the `ImageFile` to `StandaloneImageWindowController`. |
*/ |
imageController = StandaloneImageWindowController() |
imageController.imageFile = imageFile |
imageController.window?.layoutIfNeeded() |
imageController.window?.center() |
} |
return imageController |
} |
/** |
Presents the `ImageFile` in a `StandaloneImageWindowController`. If one |
already exists, it orders its window front. Otherwise it creates a new window |
controller and orders it in. |
*/ |
class func showStandaloneImage(_ image: ImageFile) { |
let standaloneImageController = standaloneImageWindowControllerForImage(image) |
standaloneImageController.showWindow(nil) |
} |
func windowWillClose(_ notification: Notification) { |
guard let imageFile = imageFile else { return } |
/* |
When the window closes, remove the mapping from the set `image` to `self` |
as the presentation has ended. Note that this often will result in `self` |
being deallocated. |
*/ |
StandaloneImageWindowController.imageFileToPresentingStandaloneController[imageFile] = nil |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-28