I’m having a problem with this one. I built it from the code in the book and it ran correctly, I thought. The time interval displayed in the bottom left of the parent layer and the images were displayed with the animation. Now that I’ve added the file names to the mix, the names don’t match the image and the time layer displays ‘desert’ every time I run it, instead of the time interval. When I place a breakpoint where I can check the text variable for each loop, it is storing the time interval correctly, however ‘Desert’ is displayed.
I know I’m missing something simple again. I checked the solution previously posted here and even edited my code to match. Then the text of the file names was cut off and I had the same results of mismatched names and ‘desert’ where the time interval should be displayed. As you will see, I went back to my version since the file names aren’t truncated when displayed on each image.
Here is my code:
import Cocoa
class ViewController: NSViewController {
var textLayer: CATextLayer!
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: textLayer.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)
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 = 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 }
// find file name and assign to fileName
fileName = url.deletingPathExtension().lastPathComponent as String
//print("FileName: \(fileName!)") // used for testing
// 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)
let t1 = Date.timeIntervalSinceReferenceDate
let interval = t1 - t0
text = String(format: "%0.1fs", interval)
}
}
//--------------------------------------------------------------------------------------------------------------
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
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..."
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 =
CGPoint(x: CGFloat(arc4random_uniform(UInt32(superLayerBounds.maxX))),
y: CGFloat(arc4random_uniform(UInt32(superLayerBounds.maxY))))
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let positionAnimation = CABasicAnimation()
positionAnimation.fromValue = NSValue(point: center)
positionAnimation.duration = 1.5
positionAnimation.timingFunction = timingFunction
let layer = CALayer()
layer.contents = image
layer.actions = ["position" : positionAnimation]
// Adding text layer here to add file name to images
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 textLayer = CATextLayer()
textLayer.anchorPoint = CGPoint.zero
textLayer.position = CGPoint(x: 10, y: 6)
textLayer.zPosition = 100
textLayer.fontSize = 24
textLayer.foregroundColor = NSColor.white.cgColor
textLayer.string = fileName
self.textLayer = textLayer
textContainer.addSublayer(textLayer)
// Rely on text's didSet to update textLayer's bounds
fileName = "Loading..."
CATransaction.begin()
view.layer!.addSublayer(layer)
layer.position = randomPoint
layer.bounds = imageBounds
CATransaction.commit()
}
}
I have figured out how to correctly display interval. My mistake was using the same variable, textLayer, for both filename and interval. I have added another instance of CATextLayer, filenameLayer, to correct that issue. I’m still having the problem of displaying the wrong filename with the image. One thing at a time…
import Cocoa
class ViewController: NSViewController {
var textLayer: CATextLayer!
var filenameLayer: CATextLayer!
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 }
// find file name and assign to filename
filename = url.deletingPathExtension().lastPathComponent as String
//print("filename: \(filename!)") // used for testing
// 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)
let t1 = Date.timeIntervalSinceReferenceDate
let interval = t1 - t0
text = String(format: "%0.1fs", interval)
}
}
//--------------------------------------------------------------------------------------------------------------
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
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
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 =
CGPoint(x: CGFloat(arc4random_uniform(UInt32(superLayerBounds.maxX))),
y: CGFloat(arc4random_uniform(UInt32(superLayerBounds.maxY))))
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let positionAnimation = CABasicAnimation()
positionAnimation.fromValue = NSValue(point: center)
positionAnimation.duration = 1.5
positionAnimation.timingFunction = timingFunction
let layer = CALayer()
layer.contents = image
layer.actions = ["position" : positionAnimation]
// Adding text layer here to add filename to image
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)
// Rely on filename's didSet to update filenameLayer's bounds
filename = "Loading..."
CATransaction.begin()
view.layer!.addSublayer(layer)
layer.position = randomPoint
layer.bounds = imageBounds
CATransaction.commit()
}
}
Got it…as my wife always says, walk away from it for a while, then go back to it and you can figure it out. In addImagesFromFolderURL() I had to move the following line of code to AFTER presentImage() was called to display the correct names with their images (I know that isn’t the correct syntax for naming a function/method, but this site always wants to make the required format into a smily face) (_ :
// find file name and assign to filename
filename = url.deletingPathExtension().lastPathComponent as String
Corrected code for addImagesFromFolderURL():
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)
}
}