My Solution to the second Challenge: Reposition Image Layers


#1

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: