Skip to content

Commit

Permalink
Merge pull request #23 from hyperoslo/feature/os-x-support
Browse files Browse the repository at this point in the history
Feature/os x support
  • Loading branch information
vadymmarkov committed Jun 23, 2016
2 parents 7e122dc + 6be05cc commit 0e3058d
Show file tree
Hide file tree
Showing 18 changed files with 580 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
included: # paths to include during linting. `--path` is ignored if present.
- Source
- Sources
excluded: # paths to ignore during linting. Takes precedence over `included`.
- Carthage
- Pods
Expand Down
4 changes: 2 additions & 2 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "krzyzanowskim/CryptoSwift" "0.4.1"
github "hyperoslo/Cache" "36ef82487589ad6c91cfc66308ff0c799512b9d6"
github "krzyzanowskim/CryptoSwift" "0.5.1"
github "hyperoslo/Cache" "d6f0b013cbe9ede4b5fe5a8b615220abce0327d6"
12 changes: 7 additions & 5 deletions Imaginary.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ Pod::Spec.new do |s|
s.source = { :git => "https://github.com/hyperoslo/Imaginary.git", :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/hyperoslo'

s.requires_arc = true
s.source_files = 'Source/**/*'

s.ios.deployment_target = '8.0'
s.osx.deployment_target = '10.9'
s.osx.deployment_target = '10.10'
s.tvos.deployment_target = '9.2'

s.frameworks = 'UIKit'
s.requires_arc = true
s.ios.source_files = 'Sources/{iOS,Shared}/**/*'
s.osx.source_files = 'Sources/{Mac,Shared}/**/*'
s.tvos.source_files = 'Sources/{iOS,Shared}/**/*'

s.frameworks = 'Foundation'
s.dependency 'Cache'
end
320 changes: 272 additions & 48 deletions Imaginary.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions Imaginary/Mac/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2016 Hyper Interaktiv AS. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
47 changes: 47 additions & 0 deletions Sources/Mac/Imaginary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Cocoa
import Cache

public struct Imaginary {

public static var preConfigure: ((imageView: NSImageView) -> Void)? = { imageView in
imageView.layer?.opacity = 0.0
}

public static var transitionClosure: ((imageView: NSImageView, image: NSImage) -> Void) = { imageView, newImage in
guard let oldImage = imageView.image else {
imageView.image = newImage
return
}

let animation = CABasicAnimation(keyPath: "contents")
animation.duration = 0.25
animation.fromValue = oldImage.cgImage
animation.toValue = newImage.cgImage
imageView.layer?.addAnimation(animation, forKey: "transitionAnimation")
imageView.image = newImage
}

public static var postConfigure: ((imageView: NSImageView) -> Void)? = { imageView in
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = imageView.layer?.opacity
animation.toValue = 1.0
imageView.layer?.addAnimation(animation, forKey: "fadeAnimation")
imageView.layer?.opacity = 1.0
}
}

public var imageCache: Cache<NSImage> {

struct Static {
static let config = Config(
frontKind: .Memory,
backKind: .Disk,
expiry: .Date(NSDate().dateByAddingTimeInterval(60 * 60 * 24 * 3)),
maxSize: 0,
maxObjects: 10)

static let cache = Cache<NSImage>(name: "Imaginary", config: config)
}

return Static.cache
}
File renamed without changes.
38 changes: 38 additions & 0 deletions Sources/Shared/Data/Decompressor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Cocoa

struct Decompressor {

static func decompress(data: NSData, scale: CGFloat = 1) -> NSImage {
guard let image = NSImage(data: data) else { return NSImage() }

image.lockFocus()

var imageRect: CGRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let imageRef = image.CGImageForProposedRect(&imageRect, context: nil, hints: nil)

if CGImageGetAlphaInfo(imageRef) != .None { return image }

let colorSpaceRef = CGImageGetColorSpace(imageRef)
let width = CGImageGetWidth(imageRef)
let height = CGImageGetHeight(imageRef)
let bytesPerPixel: Int = 4
let bytesPerRow: Int = bytesPerPixel * width
let bitsPerComponent: Int = 8

let context = CGBitmapContextCreate(nil,
width,
height,
bitsPerComponent,
bytesPerRow,
colorSpaceRef,
CGBitmapInfo.ByteOrderDefault.rawValue)
CGContextDrawImage(context, CGRectMake(0, 0, CGFloat(width), CGFloat(height)), imageRef)

guard let imageRefWithoutAlpha = CGBitmapContextCreateImage(context) else {
return NSImage()
}
let blendedImage = NSImage(CGImage: imageRefWithoutAlpha, size: NSSize(width: CGFloat(width), height: CGFloat(height)))

return blendedImage
}
}
92 changes: 92 additions & 0 deletions Sources/Shared/Data/Fetcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import Cocoa
import Foundation

class Fetcher {

enum Result {
case Success(NSImage)
case Failure(ErrorType)
}

enum Error: ErrorType {
case InvalidResponse
case InvalidStatusCode
case InvalidData
case InvalidContentLength
case ConversionError
}

let URL: NSURL
var task: NSURLSessionDataTask?
var active = false

var session: NSURLSession {
return NSURLSession.sharedSession()
}

// MARK: - Initialization

init(URL: NSURL) {
self.URL = URL
}

// MARK: - Fetching

func start(completion: (result: Result) -> Void) {
active = true

dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) { [weak self] in
guard let weakSelf = self where weakSelf.active else { return }

weakSelf.task = weakSelf.session.dataTaskWithURL(weakSelf.URL) {
[weak self] data, response, error -> Void in

guard let weakSelf = self where weakSelf.active else { return }

if let error = error {
weakSelf.complete { completion(result: .Failure(error)) }
return
}

guard let httpResponse = response as? NSHTTPURLResponse else {
weakSelf.complete { completion(result: .Failure(Error.InvalidResponse)) }
return
}

guard httpResponse.statusCode == 200 else {
weakSelf.complete { completion(result: .Failure(Error.InvalidStatusCode)) }
return
}

guard let data = data where httpResponse.validateLength(data) else {
weakSelf.complete { completion(result: .Failure(Error.InvalidContentLength)) }
return
}

guard let image = NSImage.decode(data) else {
weakSelf.complete { completion(result: .Failure(Error.ConversionError)) }
return
}

if weakSelf.active {
weakSelf.complete { completion(result: Result.Success(image)) }
}
}

weakSelf.task?.resume()
}
}

func cancel() {
task?.cancel()
active = false
}

func complete(closure: () -> Void) {
active = false

dispatch_async(dispatch_get_main_queue()) {
closure()
}
}
}
16 changes: 16 additions & 0 deletions Sources/Shared/Extensions/NSImage+CGImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Cocoa

extension NSImage {

var cgImage: CGImage? {
guard let data = TIFFRepresentation,
source = CGImageSourceCreateWithData(data, nil),
maskRef = CGImageSourceCreateImageAtIndex(source, 0, nil)
else {
return nil
}

return maskRef
}

}
11 changes: 11 additions & 0 deletions Sources/Shared/Extensions/NSImage+Cachable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Cocoa
import Cache

extension NSImage {

public typealias CacheType = NSImage

public static func decode(data: NSData) -> CacheType? {
return Decompressor.decompress(data)
}
}
66 changes: 66 additions & 0 deletions Sources/Shared/Extensions/NSImageView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Cocoa

extension NSImageView {

public func setImage(URL: NSURL?, placeholder: NSImage? = nil, completion: ((NSImage?) -> ())? = nil) {
image = placeholder

guard let URL = URL else { return }

let key = URL.absoluteString

if let fetcher = fetcher {
fetcher.cancel()
self.fetcher = nil
}

imageCache.object(key) { [weak self] object in
guard let weakSelf = self else { return }

if let image = object {
dispatch_async(dispatch_get_main_queue()) {
weakSelf.image = image
completion?(image)
}

return
}

if placeholder == nil {
Imaginary.preConfigure?(imageView: weakSelf)
}

weakSelf.fetcher = Fetcher(URL: URL)

weakSelf.fetcher?.start { [weak self] result in
guard let weakSelf = self else { return }

switch result {
case let .Success(image):
Imaginary.transitionClosure(imageView: weakSelf, image: image)
imageCache.add(key, object: image)
completion?(image)
default:
break
}
Imaginary.postConfigure?(imageView: weakSelf)
}
}
}

var fetcher: Fetcher? {
get {
let wrapper = objc_getAssociatedObject(self, &Capsule.ObjectKey) as? Capsule
let fetcher = wrapper?.value as? Fetcher
return fetcher
}
set (fetcher) {
var wrapper: Capsule?
if let fetcher = fetcher {
wrapper = Capsule(value: fetcher)
}
objc_setAssociatedObject(self, &Capsule.ObjectKey,
wrapper, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 comments on commit 0e3058d

Please sign in to comment.