diff --git a/Display.xcodeproj/project.pbxproj b/Display.xcodeproj/project.pbxproj index 93f7294fe1..994d9c495f 100644 --- a/Display.xcodeproj/project.pbxproj +++ b/Display.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ D06EE8451B7140FF00837186 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06EE8441B7140FF00837186 /* Font.swift */; }; D07921A91B6FC0C0005C23D9 /* KeyboardHostWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07921A81B6FC0C0005C23D9 /* KeyboardHostWindow.swift */; }; D07921AC1B6FC92B005C23D9 /* StatusBarHostWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07921AB1B6FC92B005C23D9 /* StatusBarHostWindow.swift */; }; + D0E49C881B83A3580099E553 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E49C871B83A3580099E553 /* ImageCache.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -115,6 +116,7 @@ D06EE8441B7140FF00837186 /* Font.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = ""; }; D07921A81B6FC0C0005C23D9 /* KeyboardHostWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardHostWindow.swift; sourceTree = ""; }; D07921AB1B6FC92B005C23D9 /* StatusBarHostWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarHostWindow.swift; sourceTree = ""; }; + D0E49C871B83A3580099E553 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -172,6 +174,7 @@ D07921A71B6FC0AE005C23D9 /* Keyboard */, D05CC3211B695AA600E235A3 /* Navigation */, D02BDAEC1B6A7053008AFAD2 /* Nodes */, + D0E49C861B83A1680099E553 /* Image Cache */, D05CC2A11B69326C00E235A3 /* Window.swift */, D05CC2E21B69552C00E235A3 /* ViewController.swift */, D05CC2E11B69534100E235A3 /* Supporting Files */, @@ -273,6 +276,14 @@ name = "Status Bar"; sourceTree = ""; }; + D0E49C861B83A1680099E553 /* Image Cache */ = { + isa = PBXGroup; + children = ( + D0E49C871B83A3580099E553 /* ImageCache.swift */, + ); + name = "Image Cache"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -393,6 +404,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D0E49C881B83A3580099E553 /* ImageCache.swift in Sources */, D05CC3181B695A9600E235A3 /* NavigationItemTransitionState.swift in Sources */, D07921AC1B6FC92B005C23D9 /* StatusBarHostWindow.swift in Sources */, D05CC2F81B6955D000E235A3 /* UIViewController+Navigation.m in Sources */, diff --git a/Display/CAAnimationUtils.swift b/Display/CAAnimationUtils.swift index 5c9b095749..7f4940375f 100644 --- a/Display/CAAnimationUtils.swift +++ b/Display/CAAnimationUtils.swift @@ -36,7 +36,7 @@ public extension CALayer { self.addAnimation(animation, forKey: keyPath) - self.setValue(to, forKey: keyPath) + //self.setValue(to, forKey: keyPath) } public func animateAlpha(from from: CGFloat, to: CGFloat, duration: NSTimeInterval) { diff --git a/Display/ImageCache.swift b/Display/ImageCache.swift new file mode 100644 index 0000000000..f78e841305 --- /dev/null +++ b/Display/ImageCache.swift @@ -0,0 +1,154 @@ +import Foundation + +private final class ImageCacheData { + let size: CGSize + let bytesPerRow: Int + var data: NSPurgeableData + + var isDiscarded: Bool { + return self.data.isContentDiscarded() + } + + var image: UIImage? { + if self.data.beginContentAccess() { + return self.createImage() + } + return nil + } + + init(size: CGSize, generator: CGContextRef -> Void, @noescape takenImage: UIImage -> Void) { + self.size = size + + self.bytesPerRow = (4 * Int(size.width) + 15) & (~15) + self.data = NSPurgeableData(length: self.bytesPerRow * Int(size.height))! + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let bitmapInfo = CGImageAlphaInfo.PremultipliedFirst.rawValue | CGBitmapInfo.ByteOrder32Little.rawValue + + if let context = CGBitmapContextCreate(self.data.mutableBytes, Int(size.width), Int(size.height), 8, bytesPerRow, colorSpace, bitmapInfo) + { + CGContextTranslateCTM(context, size.width / 2.0, size.height / 2.0) + CGContextScaleCTM(context, 1.0, -1.0) + CGContextTranslateCTM(context, -size.width / 2.0, -size.height / 2.0) + + UIGraphicsPushContext(context) + + generator(context) + + UIGraphicsPopContext() + } + + takenImage(self.createImage()) + } + + private func createImage() -> UIImage { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let bitmapInfo = CGImageAlphaInfo.PremultipliedFirst.rawValue | CGBitmapInfo.ByteOrder32Little.rawValue + + let unmanagedData = withUnsafePointer(&self.data, { pointer in + return Unmanaged.fromOpaque(COpaquePointer(pointer)) + }) + unmanagedData.retain() + let dataProvider = CGDataProviderCreateWithData(UnsafeMutablePointer(unmanagedData.toOpaque()), self.data.bytes, self.bytesPerRow, { info, _, _ in + let unmanagedData = Unmanaged.fromOpaque(COpaquePointer(info)) + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { + unmanagedData.takeUnretainedValue().endContentAccess() + unmanagedData.release() + }) + }) + + let image = CGImageCreate(Int(self.size.width), Int(self.size.height), 8, 32, self.bytesPerRow, colorSpace, CGBitmapInfo(rawValue: bitmapInfo), dataProvider, nil, false, CGColorRenderingIntent(rawValue: 0)!) + + let result = UIImage(CGImage: image!) + return result + } +} + +private final class ImageCacheResidentImage { + let key: String + let image: UIImage + var accessIndex: Int + + init(key: String, image: UIImage, accessIndex: Int) { + self.key = key + self.image = image + self.accessIndex = accessIndex + } +} + +public final class ImageCache { + let maxResidentSize: Int + var mutex = pthread_mutex_t() + + private var imageDatas: [String : ImageCacheData] = [:] + private var residentImages: [String : ImageCacheResidentImage] = [:] + var nextAccessIndex = 1 + var residentImagesSize = 0 + + public init(maxResidentSize: Int) { + self.maxResidentSize = maxResidentSize + pthread_mutex_init(&self.mutex, nil) + } + + deinit { + pthread_mutex_destroy(&self.mutex) + } + + public func addImageForKey(key: String, size: CGSize, generator: CGContextRef -> Void) { + var image: UIImage? + let imageData = ImageCacheData(size: size, generator: generator, takenImage: { image = $0 }) + + pthread_mutex_lock(&self.mutex) + self.imageDatas[key] = imageData + self.addResidentImage(image!, forKey: key) + pthread_mutex_unlock(&self.mutex) + } + + public func imageForKey(key: String) -> UIImage? { + var image: UIImage? + + pthread_mutex_lock(&self.mutex); + if let residentImage = self.residentImages[key] { + image = residentImage.image + self.nextAccessIndex++ + residentImage.accessIndex = self.nextAccessIndex + } else { + if let imageData = self.imageDatas[key] { + if let takenImage = imageData.image { + image = takenImage + self.addResidentImage(takenImage, forKey: key) + } else { + self.imageDatas.removeValueForKey(key) + } + } + } + pthread_mutex_unlock(&self.mutex) + + return image + } + + private func addResidentImage(image: UIImage, forKey key: String) { + let imageSize = Int(image.size.width * image.size.height * image.scale) * 4 + + if self.residentImagesSize + imageSize > self.maxResidentSize { + let sizeToRemove = self.residentImagesSize - (self.maxResidentSize - imageSize) + let sortedImages = self.residentImages.values.sort({ $0.accessIndex < $1.accessIndex }) + + var removedSize = 0 + var i = sortedImages.count - 1 + while i >= 0 && removedSize < sizeToRemove { + let currentImage = sortedImages[i] + let currentImageSize = Int(currentImage.image.size.width * currentImage.image.size.height * currentImage.image.scale) * 4 + removedSize += currentImageSize + self.residentImages.removeValueForKey(currentImage.key) + i-- + } + + self.residentImagesSize = max(0, self.residentImagesSize - removedSize) + } + + self.residentImagesSize += imageSize + self.nextAccessIndex++ + self.residentImages[key] = ImageCacheResidentImage(key: key, image: image, accessIndex: self.nextAccessIndex) + } +} diff --git a/Display/NavigationItemWrapper.swift b/Display/NavigationItemWrapper.swift index 47ed0bcba4..4855745fb5 100644 --- a/Display/NavigationItemWrapper.swift +++ b/Display/NavigationItemWrapper.swift @@ -192,8 +192,6 @@ internal class NavigationItemWrapper { if suspendLayout { return } - self.titleNode.measure(self.parentNode.bounds.size) - self.titleNode.frame = self.titleFrame self.backButtonNode.measure(self.parentNode.frame.size) self.backButtonNode.frame = self.backButtonFrame @@ -208,6 +206,9 @@ internal class NavigationItemWrapper { rightBarButtonItemWrapper.buttonNode.measure(self.parentNode.frame.size) rightBarButtonItemWrapper.buttonNode.frame = self.rightButtonFrame! } + + self.titleNode.measure(CGSize(width: self.parentNode.bounds.size.width - 140.0, height: CGFloat.max)) + self.titleNode.frame = self.titleFrame } func interpolatePosition(from: CGPoint, _ to: CGPoint, value: CGFloat) -> CGPoint { diff --git a/Display/NavigationTitleNode.swift b/Display/NavigationTitleNode.swift index f268dd3871..4b6b98cec9 100644 --- a/Display/NavigationTitleNode.swift +++ b/Display/NavigationTitleNode.swift @@ -17,6 +17,8 @@ public class NavigationTitleNode: ASDisplayNode { public init(text: NSString) { self.label = ASTextNode() + self.label.maximumLineCount = 1 + self.label.truncationMode = .ByTruncatingTail self.label.displaysAsynchronously = false super.init() diff --git a/Display/UIKitUtils.swift b/Display/UIKitUtils.swift index 0d6dace84b..82d70d8eea 100644 --- a/Display/UIKitUtils.swift +++ b/Display/UIKitUtils.swift @@ -34,6 +34,10 @@ public extension UIColor { convenience init(_ rgb: Int) { self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0) } + + convenience init(_ rgb: Int, _ alpha: CGFloat) { + self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: alpha) + } } public extension CGSize { @@ -47,4 +51,13 @@ public extension CGSize { } return fittedSize } + + public func aspectFilled(size: CGSize) -> CGSize { + let scale = max(size.width / max(1.0, self.width), size.height / max(1.0, self.height)) + return CGSize(width: floor(self.width * scale), height: floor(self.height * scale)) + } +} + +public func assertNotOnMainThread(file: String = __FILE__, line: Int = __LINE__) { + assert(!NSThread.isMainThread(), "\(file):\(line) running on main thread") }