Here is what I came up with. Any critique or suggestions to make it better are much a appreciated.
import Cocoa
class ViewController: NSViewController {
var textLayer: CATextLayer!
var filenameLayer: CATextLayer!
var animationTime = 1.5
var textField = NSTextField()
//-----------------------------------
var text: String? {
didSet {
let font = NSFont.systemFont(ofSize: textLayer.fontSize)
let attributes = [kCTFontAttributeName : font]
var size = text?.size(withAttributes: attributes as [NSAttributedStringKey : Any]) ?? CGSize.zero
// Ensure the size is a whole number
// I added 5 here because the last portion of 'interval' was being cut off
size.width = ceil(size.width + 5)
size.height = ceil(size.height)
textLayer.bounds = CGRect(origin: CGPoint.zero, size: size)
textLayer.superlayer?.bounds = CGRect(x: 0, y: 0,
width: size.width + 16, height: size.height + 20)
textLayer.string = text
}
}
//-----------------------------------
var filename: String? {
didSet {
let font = NSFont.systemFont(ofSize: filenameLayer.fontSize)
let attributes = [kCTFontAttributeName : font]
var size
= filename?.size(withAttributes: attributes as [NSAttributedStringKey : Any]) ?? CGSize.zero
// Ensure the size is a whole number
// I added 5 here because the last portion of 'filename' was being cut off
size.width = ceil(size.width + 5)
size.height = ceil(size.height)
filenameLayer.bounds = CGRect(origin: CGPoint.zero, size: size)
filenameLayer.superlayer?.bounds = CGRect(x: 0, y: 0,
width: size.width + 16, height: size.height + 20)
filenameLayer.string = filename
}
}
//============================================================================================================
func thumbImageFromImage(image: NSImage) -> NSImage {
let targetHeight: CGFloat = 200.0
let imageSize = image.size
let smallerSize = NSSize(width: targetHeight * imageSize.width / imageSize.height,
height: targetHeight)
let smallerImage = NSImage(size: smallerSize, flipped: false) { (rect) -> Bool in
image.draw(in: rect)
return true
}
return smallerImage
}
//--------------------------------------------------------------------------------------------------------------
func addImagesFromFolderURL(folderURL: URL) {
let t0 = Date.timeIntervalSinceReferenceDate
let fileManager = FileManager()
let directoryEnumerator = fileManager.enumerator(at: folderURL,
includingPropertiesForKeys: nil,
options: [],
errorHandler: nil)!
var allowedFiles = 10
while let url = directoryEnumerator.nextObject() as? URL {
// Skip directories
var urlResource: URLResourceValues!
do {
urlResource =
try url.resourceValues(forKeys: [.isDirectoryKey])
} catch {
print("error checking whether URL is directory: \(error)")
continue
}
guard !urlResource.isDirectory! else { continue }
guard let image = NSImage(contentsOf: url) else { continue }
// allowedFiles-- error: Unary operator '--' cannot be applied to an operand of type '@lvalue Int'
allowedFiles = allowedFiles - 1
// displays 11 images
guard allowedFiles >= 0 else { break }
let thumbImage = thumbImageFromImage(image: image)
presentImage(image: thumbImage)
// find file name and assign to filename
filename = url.deletingPathExtension().lastPathComponent as String
//print("filename: \(filename!)") // used for testing
let t1 = Date.timeIntervalSinceReferenceDate
let interval = t1 - t0
text = String(format: "%0.1fs", interval)
}
}
//--------------------------------------------------------------------------------------------------------------
func randomPoint () -> CGPoint {
let superLayerBounds = view.layer!.bounds
return CGPoint(x: CGFloat(arc4random_uniform(UInt32(superLayerBounds.maxX))),
y: CGFloat(arc4random_uniform(UInt32(superLayerBounds.maxY))))
}
//--------------------------------------------------------------------------------------------------------------
@objc func reScatter(sender: NSButton) {
animationTime = textField.doubleValue
// Animation - new postionAnimation must be created with each button push
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
for layer in (view.layer?.sublayers)! {
let positionAnimation = CABasicAnimation()
positionAnimation.duration = animationTime
positionAnimation.timingFunction = timingFunction
CATransaction.begin()
if layer.name == "image" {
// create a new "position"
layer.actions = ["position" : positionAnimation]
// create a new random point for each layer
layer.position = randomPoint()
}
CATransaction.commit()
}
}
//--------------------------------------------------------------------------------------------------------------
override func viewDidLoad() {
super.viewDidLoad()
// Set view to be layer-hosting
view.layer = CALayer()
view.layer?.frame = CGRect(x: 50, y: 50, width: 1000, height: 750)
view.layer?.bounds = (view.layer?.frame)!
view.wantsLayer = true
// added for Challenge
view.layer?.shadowColor = NSColor.gray.cgColor
view.layer?.shadowRadius = 5
view.layer?.shadowOpacity = 1
// adding a button and text field
// button that starts the repositioning of the images
let frame = NSRect(x: 990, y: 0, width: 80, height: 30)
let button = NSButton(frame: frame)
button.bezelStyle = .rounded
button.title = "Shuffle"
button.target = self
button.action = #selector(reScatter)
view.addSubview(button)
// text field to enter float for timing
let textFrame = NSRect(x: 940, y: 7, width: 50, height: 19)
textField = NSTextField(frame: textFrame)
textField.bezelStyle = .roundedBezel
textField.isEditable = true
textField.isSelectable = true
view.addSubview(textField)
// Label for corresponding text field
let textLabelFrame = NSRect(x: 750, y: 7, width: 180, height: 19)
let textLabel = NSTextField(labelWithString: "Enter scatter time in seconds")
textLabel.frame = textLabelFrame
textLabel.isEditable = false
textLabel.isSelectable = false
textLabel.backgroundColor = NSColor.clear
view.addSubview(textLabel)
// Text container for time interval display
let textContainer = CALayer()
textContainer.anchorPoint = CGPoint.zero
textContainer.position = CGPoint(x: 10, y: 10)
textContainer.zPosition = 100
textContainer.backgroundColor = NSColor.black.cgColor
textContainer.borderColor = NSColor.white.cgColor
textContainer.borderWidth = 2
textContainer.cornerRadius = 15
textContainer.shadowOpacity = 0.5
view.layer!.addSublayer(textContainer)
let textLayer = CATextLayer()
textLayer.anchorPoint = CGPoint.zero
textLayer.position = CGPoint(x: 10, y: 6)
textLayer.zPosition = 100
textLayer.fontSize = 24
textLayer.foregroundColor = NSColor.white.cgColor
self.textLayer = textLayer
textContainer.addSublayer(textLayer)
// Rely on text's didSet to update textLayer's bounds
text = "Loading..."
// added to avoid unwrapping nil -- fatal error
let filenameLayer = CATextLayer()
filenameLayer.string = filename
self.filenameLayer = filenameLayer
filename = "Loading..."
let url = URL(fileURLWithPath: "/Library/Desktop Pictures")
addImagesFromFolderURL(folderURL: url)
}
//--------------------------------------------------------------------------------------------------------------
func presentImage(image: NSImage) {
let superLayerBounds = view.layer!.bounds
let center = CGPoint(x: superLayerBounds.midX, y: superLayerBounds.midY)
let imageBounds = CGRect(origin: CGPoint.zero, size: image.size)
let randomPoint = self.randomPoint()
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let positionAnimation = CABasicAnimation()
positionAnimation.fromValue = NSValue(point: center)
positionAnimation.duration = animationTime
positionAnimation.timingFunction = timingFunction
let layer = CALayer()
layer.contents = image
layer.actions = ["position" : positionAnimation]
layer.name = "image"
// added for Challenge
// white boarder for image
layer.borderWidth = 5
layer.borderColor = NSColor.white.cgColor
// shadow for image
layer.shadowColor = NSColor.lightGray.cgColor
layer.masksToBounds = false // Allow shadow to de drawn outside of bounds (false is default)
layer.shadowOpacity = 0.5 // must be non zero
layer.shadowOffset = CGSize(width: 10, height: -10)
// Adding text layer here to add filename to image, lower left
let textContainer = CALayer()
textContainer.anchorPoint = CGPoint.zero
textContainer.position = CGPoint(x: 10, y: 10)
textContainer.zPosition = 100
textContainer.backgroundColor = NSColor.clear.cgColor
textContainer.borderColor = NSColor.clear.cgColor
textContainer.borderWidth = 2
textContainer.cornerRadius = 15
textContainer.shadowOpacity = 0.5
layer.addSublayer(textContainer)
let filenameLayer = CATextLayer()
filenameLayer.anchorPoint = CGPoint.zero
filenameLayer.position = CGPoint(x: 10, y: 6)
filenameLayer.zPosition = 100
filenameLayer.fontSize = 24
filenameLayer.foregroundColor = NSColor.white.cgColor
filenameLayer.string = filename
self.filenameLayer = filenameLayer
textContainer.addSublayer(filenameLayer)
CATransaction.begin()
view.layer!.addSublayer(layer)
layer.position = randomPoint
layer.bounds = imageBounds
CATransaction.commit()
}
}
And here is the result: