diff --git a/OSX/DeckRocket/AppDelegate.swift b/OSX/DeckRocket/AppDelegate.swift index 040c395..e19883d 100644 --- a/OSX/DeckRocket/AppDelegate.swift +++ b/OSX/DeckRocket/AppDelegate.swift @@ -9,18 +9,18 @@ import Cocoa import MultipeerConnectivity -class AppDelegate: NSObject, NSApplicationDelegate { +final class AppDelegate: NSObject, NSApplicationDelegate { // MARK: Properties let multipeerClient = MultipeerClient() - let menuView = MenuView() + private let menuView = MenuView() // MARK: App - func applicationDidFinishLaunching(aNotification: NSNotification?) { - multipeerClient.onStateChange = {(state: MCSessionState) -> () in - var stateString = "" + func applicationDidFinishLaunching(aNotification: NSNotification) { + multipeerClient.onStateChange = { state in + let stateString: String switch state { case .NotConnected: stateString = "Not Connected" @@ -30,7 +30,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { stateString = "Connected" } dispatch_async(dispatch_get_main_queue()) { - self.menuView.menu!.itemAtIndex(0)!.title = stateString + self.menuView.menu?.itemAtIndex(0)?.title = stateString } } } diff --git a/OSX/DeckRocket/HUDView.swift b/OSX/DeckRocket/HUDView.swift index 69ee0b3..22a5707 100644 --- a/OSX/DeckRocket/HUDView.swift +++ b/OSX/DeckRocket/HUDView.swift @@ -8,14 +8,14 @@ import Cocoa -let hudWindow = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 300, height: 300), +private let hudWindow = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 300, height: 300), styleMask: NSBorderlessWindowMask, backing: .Buffered, defer: false) -class HUDView: NSView { +final class HUDView: NSView { - override class func initialize() { + override static func initialize() { hudWindow.backgroundColor = NSColor.clearColor() hudWindow.opaque = false hudWindow.makeKeyAndOrderFront(NSApp) @@ -25,8 +25,10 @@ class HUDView: NSView { DJProgressHUD.setBackgroundAlpha(0, disableActions: false) } - class func show(string: String) { - DJProgressHUD.showProgress(1, withStatus: string, fromView: hudWindow.contentView as NSView) + static func show(string: String) { + if let windowView = hudWindow.contentView as? NSView { + DJProgressHUD.showProgress(1, withStatus: string, fromView: windowView) + } let delay = 2 * Double(NSEC_PER_SEC) let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay)) dispatch_after(time, dispatch_get_main_queue()) { @@ -34,15 +36,19 @@ class HUDView: NSView { } } - class func showProgress(progress: CGFloat, string: String) { - DJProgressHUD.showProgress(progress, withStatus: string, fromView: hudWindow.contentView as NSView) + static func showProgress(progress: CGFloat, string: String) { + if let windowView = hudWindow.contentView as? NSView { + DJProgressHUD.showProgress(progress, withStatus: string, fromView: windowView) + } } - class func showWithActivity(string: String) { - DJProgressHUD.showStatus(string, fromView: hudWindow.contentView as NSView) + static func showWithActivity(string: String) { + if let windowView = hudWindow.contentView as? NSView { + DJProgressHUD.showStatus(string, fromView: windowView) + } } - class func dismiss() { + static func dismiss() { DJProgressHUD.dismiss() } } diff --git a/OSX/DeckRocket/Info.plist b/OSX/DeckRocket/Info.plist index 0155588..c091d20 100644 --- a/OSX/DeckRocket/Info.plist +++ b/OSX/DeckRocket/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.0.3 + 0.0.4 CFBundleSignature ???? CFBundleVersion diff --git a/OSX/DeckRocket/MenuView.swift b/OSX/DeckRocket/MenuView.swift index b999183..8c15679 100644 --- a/OSX/DeckRocket/MenuView.swift +++ b/OSX/DeckRocket/MenuView.swift @@ -8,12 +8,13 @@ import Cocoa -class MenuView: NSView, NSMenuDelegate { - var highlight = false +// FIXME: Use system-defined constant once accessible from Swift. +let NSVariableStatusItemLength: CGFloat = -1 - // NSVariableStatusItemLength == -1 - // Not using symbol because it doesn't link properly in Swift - let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1) +final class MenuView: NSView, NSMenuDelegate { + private var highlight = false + + private let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength) // MARK: Initializers @@ -30,26 +31,28 @@ class MenuView: NSView, NSMenuDelegate { // MARK: Menu - func setupMenu() { + private func setupMenu() { let menu = NSMenu() menu.addItemWithTitle("Not Connected", action: nil, keyEquivalent: "") - menu.itemAtIndex(0)!.enabled = false + menu.itemAtIndex(0)?.enabled = false menu.addItemWithTitle("Quit DeckRocket", action: "quit", keyEquivalent: "") self.menu = menu - self.menu!.delegate = self + self.menu?.delegate = self } override func mouseDown(theEvent: NSEvent) { super.mouseDown(theEvent) - statusItem.popUpStatusItemMenu(menu!) + if let menu = menu { + statusItem.popUpStatusItemMenu(menu) + } } - func menuWillOpen(menu: NSMenu!) { + func menuWillOpen(menu: NSMenu) { highlight = true needsDisplay = true } - func menuDidClose(menu: NSMenu!) { + func menuDidClose(menu: NSMenu) { highlight = false needsDisplay = true } @@ -63,32 +66,33 @@ class MenuView: NSView, NSMenuDelegate { // MARK: Dragging override func draggingEntered(sender: NSDraggingInfo) -> NSDragOperation { - return NSDragOperation.Copy + return .Copy } override func performDragOperation(sender: NSDraggingInfo) -> Bool { let pboard = sender.draggingPasteboard() - if contains(pboard.types as [NSString], NSFilenamesPboardType) { - let files = pboard.propertyListForType(NSFilenamesPboardType) as [String] - let file = files[0] - if validateFile(file) { - let appDelegate = NSApplication.sharedApplication().delegate as AppDelegate - appDelegate.multipeerClient.sendFile(file) + if contains((pboard.types as? [String]) ?? [], NSFilenamesPboardType) { + if let file = (pboard.propertyListForType(NSFilenamesPboardType) as? [String])?.first { + if validateFile(file) { + let appDelegate = NSApplication.sharedApplication().delegate as? AppDelegate + appDelegate?.multipeerClient.sendFile(file) + } else { + HUDView.show("Error!\nOnly PDF and Markdown files can be sent") + } } else { - HUDView.show("Error!\nOnly PDF and Markdown files can be sent") + HUDView.show("Error!\nFile not found") } } return true } - func validateFile(filePath: NSString) -> Bool { - var allowedExtensions = [ + private func validateFile(filePath: NSString) -> Bool { + let allowedExtensions = [ // Markdown "markdown", "mdown", "mkdn", "md", "mkd", "mdwn", "mdtxt", "mdtext", "text", // PDF "pdf" ] - return contains(allowedExtensions, filePath.pathExtension.lowercaseString) } } diff --git a/OSX/DeckRocket/MultipeerClient.swift b/OSX/DeckRocket/MultipeerClient.swift index 93fe175..1a12a15 100644 --- a/OSX/DeckRocket/MultipeerClient.swift +++ b/OSX/DeckRocket/MultipeerClient.swift @@ -10,26 +10,27 @@ import Foundation import MultipeerConnectivity typealias stateChange = ((state: MCSessionState) -> ())? -typealias KVOContext = UInt8 -var ProgressContext = KVOContext() +private typealias KVOContext = UInt8 +private var progressContext = KVOContext() +private var lastDisplayTime = NSDate() -class MultipeerClient: NSObject, MCNearbyServiceAdvertiserDelegate, MCSessionDelegate { +final class MultipeerClient: NSObject, MCNearbyServiceAdvertiserDelegate, MCSessionDelegate { // MARK: Properties - let localPeerID = MCPeerID(displayName: NSHost.currentHost().localizedName) - let advertiser: MCNearbyServiceAdvertiser? - var session: MCSession? + private let localPeerID = MCPeerID(displayName: NSHost.currentHost().localizedName) + private let advertiser: MCNearbyServiceAdvertiser? + private var session: MCSession? + private var pdfProgress: NSProgress? var onStateChange: stateChange? - var pdfProgress: NSProgress? // MARK: Lifecycle override init() { - super.init() advertiser = MCNearbyServiceAdvertiser(peer: localPeerID, discoveryInfo: nil, serviceType: "deckrocket") - advertiser!.delegate = self - advertiser!.startAdvertisingPeer() + super.init() + advertiser?.delegate = self + advertiser?.startAdvertisingPeer() } // MARK: Send File @@ -37,46 +38,48 @@ class MultipeerClient: NSObject, MCNearbyServiceAdvertiserDelegate, MCSessionDel func sendFile(filePath: String) { let url = NSURL(fileURLWithPath: filePath) - if session == nil || session!.connectedPeers.count == 0 { + if session == nil || session!.connectedPeers.count == 0 { // Safe to force unwrap HUDView.show("Error!\niPhone not connected") return } - let peer = session!.connectedPeers[0] as MCPeerID - pdfProgress = session!.sendResourceAtURL(url, withName: filePath.lastPathComponent, toPeer: peer) { error in - dispatch_async(dispatch_get_main_queue()) { - self.pdfProgress!.removeObserver(self, forKeyPath: "fractionCompleted", context: &ProgressContext) - if error != nil { - HUDView.show("Error!\n\(error.localizedDescription)") - } else { - HUDView.show("Success!") + if let peer = session?.connectedPeers[0] as? MCPeerID { + pdfProgress = session?.sendResourceAtURL(url, withName: filePath.lastPathComponent, toPeer: peer) { error in + dispatch_async(dispatch_get_main_queue()) { + self.pdfProgress?.removeObserver(self, forKeyPath: "fractionCompleted", context: &progressContext) + if let errorDescription = error?.localizedDescription { + HUDView.show("Error!\n\(errorDescription)") + } else { + HUDView.show("Success!") + } } } + pdfProgress?.addObserver(self, forKeyPath: "fractionCompleted", options: .New, context: &progressContext) } - pdfProgress!.addObserver(self, forKeyPath: "fractionCompleted", options: .New, context: &ProgressContext) } // MARK: MCNearbyServiceAdvertiserDelegate func advertiser(advertiser: MCNearbyServiceAdvertiser!, didReceiveInvitationFromPeer peerID: MCPeerID!, withContext context: NSData!, invitationHandler: ((Bool, MCSession!) -> Void)!) { session = MCSession(peer: localPeerID, securityIdentity: nil, encryptionPreference: .None) - session!.delegate = self - invitationHandler(true, session!) + session?.delegate = self + invitationHandler(true, session) } // MARK: MCSessionDelegate func session(session: MCSession!, peer peerID: MCPeerID!, didChangeState state: MCSessionState) { - if let block = onStateChange! { - block(state: state) - } + onStateChange??(state: state) } func session(session: MCSession!, didReceiveData data: NSData!, fromPeer peerID: MCPeerID!) { - let task = NSTask() - task.launchPath = NSBundle.mainBundle().pathForResource("deckrocket", ofType: "scpt")! - task.arguments = [NSString(data: data, encoding: NSUTF8StringEncoding)!] - task.launch() + if let launchPath = NSBundle.mainBundle().pathForResource("deckrocket", ofType: "scpt"), + argument = NSString(data: data, encoding: NSUTF8StringEncoding) { + let task = NSTask() + task.launchPath = launchPath + task.arguments = [argument] + task.launch() + } } func session(session: MCSession!, didReceiveStream stream: NSInputStream!, withName streamName: String!, fromPeer peerID: MCPeerID!) { @@ -94,13 +97,15 @@ class MultipeerClient: NSObject, MCNearbyServiceAdvertiserDelegate, MCSessionDel // MARK: KVO override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<()>) { - if context == &ProgressContext { - dispatch_async(dispatch_get_main_queue()) { - let progress = change[NSKeyValueChangeNewKey]! as CGFloat - HUDView.showProgress(progress, string: "Sending File to iPhone") - } - } else { + if context != &progressContext { super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context) + } else if abs(lastDisplayTime.timeIntervalSinceNow) > 1/60 { // Update HUD at no more than 60fps + dispatch_sync(dispatch_get_main_queue()) { + if let progress = change[NSKeyValueChangeNewKey] as? CGFloat { + HUDView.showProgress(progress, string: "Sending File to iPhone") + lastDisplayTime = NSDate() + } + } } } } diff --git a/OSX/DeckRocket/main.swift b/OSX/DeckRocket/main.swift index 02f1b46..ce79e71 100644 --- a/OSX/DeckRocket/main.swift +++ b/OSX/DeckRocket/main.swift @@ -8,4 +8,4 @@ import Cocoa -NSApplicationMain(C_ARGC, C_ARGV) +NSApplicationMain(Process.argc, Process.unsafeArgv) diff --git a/README.md b/README.md index 2bbdb63..18be833 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## Requirements -DeckRocket is built in Swift and relies on Multipeer Connectivity on both OSX and iOS. Therefore Xcode 6.1, OS X 10.10 and iOS 8 are all required to build, install and use DeckRocket. +DeckRocket is built in Swift and relies on Multipeer Connectivity on both OSX and iOS. Xcode 6.3b2, OS X 10.10 and iOS 8 are all required to build, install and use DeckRocket. ## Usage diff --git a/iOS/DeckRocket/AppDelegate.swift b/iOS/DeckRocket/AppDelegate.swift index 5b55504..b9c3fd0 100644 --- a/iOS/DeckRocket/AppDelegate.swift +++ b/iOS/DeckRocket/AppDelegate.swift @@ -9,15 +9,13 @@ import UIKit @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { +final class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? + var window: UIWindow? = UIWindow(frame: UIScreen.mainScreen().bounds) - func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool { - self.window = UIWindow(frame: UIScreen.mainScreen().bounds) - - self.window!.rootViewController = ViewController() - self.window!.makeKeyAndVisible() + func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool { + window?.rootViewController = ViewController() + window?.makeKeyAndVisible() return true } } diff --git a/iOS/DeckRocket/Cell.swift b/iOS/DeckRocket/Cell.swift index 3423fe5..e9a589b 100644 --- a/iOS/DeckRocket/Cell.swift +++ b/iOS/DeckRocket/Cell.swift @@ -8,7 +8,7 @@ import UIKit -class Cell: UICollectionViewCell { +final class Cell: UICollectionViewCell { let imageView = UIImageView() override init(frame: CGRect) { @@ -22,6 +22,6 @@ class Cell: UICollectionViewCell { } override func layoutSubviews() { - imageView.frame = self.bounds + imageView.frame = bounds } } diff --git a/iOS/DeckRocket/CollectionViewLayout.swift b/iOS/DeckRocket/CollectionViewLayout.swift index 443b4d0..5ff40e6 100644 --- a/iOS/DeckRocket/CollectionViewLayout.swift +++ b/iOS/DeckRocket/CollectionViewLayout.swift @@ -8,11 +8,13 @@ import UIKit -class CollectionViewLayout: UICollectionViewFlowLayout { +final class CollectionViewLayout: UICollectionViewFlowLayout { override init() { super.init() - itemSize = UIApplication.sharedApplication().delegate!.window!!.bounds.size + if let windowSize = UIApplication.sharedApplication().delegate?.window??.bounds.size { + itemSize = windowSize + } scrollDirection = .Horizontal minimumInteritemSpacing = 0 minimumLineSpacing = 0 diff --git a/iOS/DeckRocket/FileType.swift b/iOS/DeckRocket/FileType.swift index cef74b6..c6178d7 100644 --- a/iOS/DeckRocket/FileType.swift +++ b/iOS/DeckRocket/FileType.swift @@ -9,27 +9,26 @@ import Foundation enum FileType { - case PDF, Markdown, Unknown + case PDF, Markdown - init(fileExtension: String) { - var ext = fileExtension.lowercaseString + init?(fileExtension: String) { + let ext = fileExtension.lowercaseString if contains(FileType.extensionsForType(PDF), ext) { self = PDF + return } else if contains(FileType.extensionsForType(Markdown), ext) { self = Markdown - } else { - self = Unknown + return } + return nil } static func extensionsForType(fileType: FileType) -> [String] { switch fileType { - case PDF: - return ["pdf"] - case Markdown: - return ["markdown", "mdown", "mkdn", "md", "mkd", "mdwn", "mdtxt", "mdtext", "text"] - case Unknown: - return [String]() + case PDF: + return ["pdf"] + case Markdown: + return ["markdown", "mdown", "mkdn", "md", "mkd", "mdwn", "mdtxt", "mdtext", "text"] } } } diff --git a/iOS/DeckRocket/Info.plist b/iOS/DeckRocket/Info.plist index a6971c2..1284c58 100644 --- a/iOS/DeckRocket/Info.plist +++ b/iOS/DeckRocket/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.0.3 + 0.0.4 CFBundleSignature ???? CFBundleVersion diff --git a/iOS/DeckRocket/MultipeerClient.swift b/iOS/DeckRocket/MultipeerClient.swift index 4d60627..bcfe398 100644 --- a/iOS/DeckRocket/MultipeerClient.swift +++ b/iOS/DeckRocket/MultipeerClient.swift @@ -11,33 +11,35 @@ import MultipeerConnectivity typealias stateChange = ((state: MCSessionState, peerID: MCPeerID) -> ())? -class MultipeerClient: NSObject, MCNearbyServiceBrowserDelegate, MCSessionDelegate { +final class MultipeerClient: NSObject, MCNearbyServiceBrowserDelegate, MCSessionDelegate { // MARK: Properties - let localPeerID = MCPeerID(displayName: UIDevice.currentDevice().name) - var browser: MCNearbyServiceBrowser? - var session: MCSession? - var state = MCSessionState.NotConnected + private let localPeerID = MCPeerID(displayName: UIDevice.currentDevice().name) + let browser: MCNearbyServiceBrowser? + private(set) var session: MCSession? + private(set) var state = MCSessionState.NotConnected var onStateChange: stateChange? // MARK: Init override init() { - super.init() browser = MCNearbyServiceBrowser(peer: localPeerID, serviceType: "deckrocket") - browser!.delegate = self - browser!.startBrowsingForPeers() + super.init() + browser?.delegate = self + browser?.startBrowsingForPeers() } // MARK: Send func send(data: NSData) { - session!.sendData(data, toPeers: session!.connectedPeers, withMode: .Reliable, error: nil) + session?.sendData(data, toPeers: session!.connectedPeers, withMode: .Reliable, error: nil) // Safe to force unwrap } func sendString(string: NSString) { - send(string.dataUsingEncoding(NSUTF8StringEncoding)!) + if let stringData = string.dataUsingEncoding(NSUTF8StringEncoding) { + send(stringData) + } } // MARK: MCNearbyServiceBrowserDelegate @@ -45,7 +47,7 @@ class MultipeerClient: NSObject, MCNearbyServiceBrowserDelegate, MCSessionDelega func browser(browser: MCNearbyServiceBrowser!, foundPeer peerID: MCPeerID!, withDiscoveryInfo info: [NSObject : AnyObject]!) { if session == nil { session = MCSession(peer: localPeerID) - session!.delegate = self + session?.delegate = self } browser.invitePeer(peerID, toSession: session, withContext: nil, timeout: 30) } @@ -58,9 +60,7 @@ class MultipeerClient: NSObject, MCNearbyServiceBrowserDelegate, MCSessionDelega func session(session: MCSession!, peer peerID: MCPeerID!, didChangeState state: MCSessionState) { self.state = state - if let block = onStateChange! { - block(state: state, peerID: peerID) - } + onStateChange??(state: state, peerID: peerID) } func session(session: MCSession!, didReceiveData data: NSData!, fromPeer peerID: MCPeerID!) { @@ -77,15 +77,12 @@ class MultipeerClient: NSObject, MCNearbyServiceBrowserDelegate, MCSessionDelega func session(session: MCSession!, didFinishReceivingResourceWithName resourceName: String!, fromPeer peerID: MCPeerID!, atURL localURL: NSURL!, withError error: NSError!) { if error == nil { - dispatch_async(dispatch_get_main_queue()) { - let fileType = FileType(fileExtension: resourceName.pathExtension) + if let fileType = FileType(fileExtension: resourceName.pathExtension) { switch fileType { - case .PDF: - self.handlePDF(resourceName, atURL: localURL) - case .Markdown: - self.handleMarkdown(resourceName, atURL: localURL) - case .Unknown: - println("file type unknown") + case .PDF: + handlePDF(resourceName, atURL: localURL) + case .Markdown: + handleMarkdown(resourceName, atURL: localURL) } } } @@ -93,38 +90,39 @@ class MultipeerClient: NSObject, MCNearbyServiceBrowserDelegate, MCSessionDelega // MARK: Handle Resources - func handlePDF(resourceName: String!, atURL localURL: NSURL!) { - promptToLoadResource("New Presentation File", resourceName: resourceName, atURL: localURL, userDefaultsKey: "pdfPath") + private func handlePDF(resourceName: String!, atURL localURL: NSURL!) { + promptToLoadResource("New Presentation File", resourceName: resourceName, atURL: localURL, userDefaultsKey: "pdfName") } - func handleMarkdown(resourceName: String!, atURL localURL: NSURL!) { - promptToLoadResource("New Markdown File", resourceName: resourceName, atURL: localURL, userDefaultsKey: "mdPath") + private func handleMarkdown(resourceName: String!, atURL localURL: NSURL!) { + promptToLoadResource("New Markdown File", resourceName: resourceName, atURL: localURL, userDefaultsKey: "mdName") } - func promptToLoadResource(title: String, resourceName: String, atURL localURL: NSURL, userDefaultsKey: String) { - let rootVC = UIApplication.sharedApplication().delegate!.window!!.rootViewController as ViewController + private func promptToLoadResource(title: String, resourceName: String, atURL localURL: NSURL, userDefaultsKey: String) { + let rootVC = UIApplication.sharedApplication().delegate?.window??.rootViewController as? ViewController let alert = UIAlertController(title: title, message: "Would you like to load \"\(resourceName)\"?", preferredStyle: .Alert) alert.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)) alert.addAction(UIAlertAction(title: "Load", style: .Default) { action in - var error: NSError? = nil - let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as NSString let filePath = documentsPath.stringByAppendingPathComponent(resourceName) + let fileManager = NSFileManager.defaultManager() - if NSFileManager.defaultManager().fileExistsAtPath(filePath) { - NSFileManager.defaultManager().removeItemAtPath(filePath, error: nil) + var error: NSError? = nil + if fileManager.fileExistsAtPath(filePath) { + fileManager.removeItemAtPath(filePath, error: &error) } - let url = NSURL(fileURLWithPath: filePath) - - NSFileManager.defaultManager().moveItemAtURL(localURL, toURL: url!, error: &error) - if error == nil { - NSUserDefaults.standardUserDefaults().setObject(filePath, forKey: userDefaultsKey) + if let url = NSURL(fileURLWithPath: filePath) where fileManager.moveItemAtURL(localURL, toURL: url, error: &error) { + NSUserDefaults.standardUserDefaults().setObject(resourceName, forKey: userDefaultsKey) NSUserDefaults.standardUserDefaults().synchronize() - rootVC.updatePresentation() + rootVC?.updatePresentation() + } else { + let message = error?.localizedDescription ?? "move file failed with no error" + fatalError(message) } }) - - rootVC.presentViewController(alert, animated: true, completion: nil) + dispatch_async(dispatch_get_main_queue()) { + rootVC?.presentViewController(alert, animated: true, completion: nil) + } } } diff --git a/iOS/DeckRocket/PDFImages.swift b/iOS/DeckRocket/PDFImages.swift index bd10b5d..82019b2 100644 --- a/iOS/DeckRocket/PDFImages.swift +++ b/iOS/DeckRocket/PDFImages.swift @@ -11,36 +11,38 @@ import UIKit import CoreGraphics extension UIImage { - class func imagesFromPDFPath(pdfPath: String) -> [UIImage] { - let pdfURL = NSURL(fileURLWithPath: pdfPath) - let pdf = CGPDFDocumentCreateWithURL(pdfURL) - let numberOfPages = CGPDFDocumentGetNumberOfPages(pdf) - var images = [UIImage]() - - if numberOfPages == 0 { + static func imagesFromPDFPath(pdfPath: String) -> [UIImage] { + if let pdfURL = NSURL(fileURLWithPath: pdfPath), + let pdf = CGPDFDocumentCreateWithURL(pdfURL) { + let numberOfPages = CGPDFDocumentGetNumberOfPages(pdf) + + if numberOfPages == 0 { + return [] + } + + var images = [UIImage]() + if let screenSize = UIApplication.sharedApplication().delegate?.window??.bounds.size { + let largestDimension = max(screenSize.width, screenSize.height) + let largestSize = CGSize(width: largestDimension, height: largestDimension) + + for pageNumber in 1...Int(numberOfPages) { + if let image = UIImage(pdfURL: pdfURL, page: pageNumber, fitSize: largestSize) { + images.append(image) + } + } + } return images } - - let screenSize = UIApplication.sharedApplication().delegate!.window!!.bounds.size - let largestDimension = max(screenSize.width, screenSize.height) - let largestSize = CGSize(width: largestDimension, height: largestDimension) - - for pageNumber in 1...numberOfPages { - images.append(UIImage.imageWithPDFURL(pdfURL!, page: pageNumber, fitSize: largestSize)) - } - return images + return [] } - class func pdfRectForURL(url: NSURL, page: UInt) -> CGRect { - let pdf = CGPDFDocumentCreateWithURL(url); - let pageRef = CGPDFDocumentGetPage(pdf, page); - - let rect = CGPDFPageGetBoxRect(pageRef, kCGPDFCropBox) - - return rect + private static func pdfRectForURL(url: NSURL, page: Int) -> CGRect { + let pdf = CGPDFDocumentCreateWithURL(url) + let pageRef = CGPDFDocumentGetPage(pdf, page) + return CGPDFPageGetBoxRect(pageRef, kCGPDFCropBox) } - class func imageWithPDFURL(url: NSURL, page: UInt, size: CGSize) -> UIImage { + convenience init?(pdfURL: NSURL, page: Int, size: CGSize) { UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.mainScreen().scale) let ctx = UIGraphicsGetCurrentContext() @@ -50,28 +52,26 @@ extension UIImage { CGContextScaleCTM(ctx, 1, -1) CGContextTranslateCTM(ctx, 0, -size.height) - let pdf = CGPDFDocumentCreateWithURL(url) - + let pdf = CGPDFDocumentCreateWithURL(pdfURL) let pageRef = CGPDFDocumentGetPage(pdf, page) - let rect = CGPDFPageGetBoxRect(pageRef, kCGPDFCropBox) - CGContextScaleCTM(ctx, size.width/rect.size.width, size.height/rect.size.height) + CGContextScaleCTM(ctx, size.width / rect.size.width, size.height / rect.size.height) CGContextTranslateCTM(ctx, -rect.origin.x, -rect.origin.y) CGContextDrawPDFPage(ctx, pageRef) - let pdfImage = UIGraphicsGetImageFromCurrentImageContext(); + let pdfImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - return pdfImage + self.init(CGImage: pdfImage.CGImage) } - class func imageWithPDFURL(url: NSURL, page: UInt, fitSize size: CGSize) -> UIImage { - let rect = pdfRectForURL(url, page: page) - let scaleFactor = max(rect.size.width/size.width, rect.size.height/size.height) - let newWidth = ceil(rect.size.width/scaleFactor) - let newHeight = ceil(rect.size.height/scaleFactor) + convenience init?(pdfURL: NSURL, page: Int, fitSize size: CGSize) { + let rect = UIImage.pdfRectForURL(pdfURL, page: page) + let scaleFactor = max(rect.size.width / size.width, rect.size.height / size.height) + let newWidth = ceil(rect.size.width / scaleFactor) + let newHeight = ceil(rect.size.height / scaleFactor) let newSize = CGSize(width: newWidth, height: newHeight) - return UIImage.imageWithPDFURL(url, page: page, size: newSize) + self.init(pdfURL: pdfURL, page: page, size: newSize) } } diff --git a/iOS/DeckRocket/Presentation.swift b/iOS/DeckRocket/Presentation.swift index 9abbd26..905750b 100644 --- a/iOS/DeckRocket/Presentation.swift +++ b/iOS/DeckRocket/Presentation.swift @@ -9,73 +9,48 @@ import Foundation import UIKit -class Presentation { +struct Presentation { // MARK: Properties - var markdown = "" - var slides = [Slide]() + let markdown: String + let slides: [Slide] // MARK: Initializers init(pdfPath: String, markdown: String?) { let slideImages = UIImage.imagesFromPDFPath(pdfPath) + let pages = (markdown != nil) ? Presentation.pages(markdown!) : [] - var pages = [String]() - - if markdown != nil { - self.markdown = markdown! - pages = self.pages() - } - - for (index, image) in enumerate(slideImages) { - var page: String? - if pages.count > index { - page = pages[index] - } - slides.append(Slide(image: image, markdown: page?)) + self.markdown = markdown ?? "" + slides = map(enumerate(slideImages)) { index, image in + let page: String? = (pages.count > index) ? pages[index] : nil + return Slide(image: image, markdown: page) } } // MARK: Markdown Parsing - func pages() -> [String] { - let locations = pageLocations() - - var pages = [String]() - - for (index, end) in enumerate(locations) { - var start = 0 - if index > 0 { - start = locations[index - 1] - } - var substring = (markdown as NSString).substringWithRange(NSRange(location: start, length: end-start)) - substring = substring.stringByReplacingOccurrencesOfString("---\n", withString: "") - pages.append(substring) + private static func pages(markdown: NSString) -> [String] { + let locations = pageLocations(markdown) + return map(enumerate(locations)) { index, end in + let start = (index > 0) ? locations[index - 1] : 0 + return markdown.substringWithRange(NSRange(location: start, length: end - start)) + .stringByReplacingOccurrencesOfString("---\n", withString: "") } - - return pages } - func pageLocations() -> [Int] { + private static func pageLocations(markdown: NSString) -> [Int] { // Pattern must match http://www.decksetapp.com/support/#i-separated-my-content-by-----but-deckset-shows-it-on-one-slide-whats-wrong let pattern = "^\\-\\-\\-" // ^\-\-\- let pagesExpression = NSRegularExpression(pattern: pattern, - options: NSRegularExpressionOptions.AnchorsMatchLines, + options: .AnchorsMatchLines, error: nil) - var pageDelimiters = [Int]() - - let range = NSRange(location: 0, length: (markdown as NSString).length) - if let matches = pagesExpression?.matchesInString(markdown, options: NSMatchingOptions(0), range: range) { - for match in matches as [NSTextCheckingResult] { - pageDelimiters.append(match.range.location) - } - } - - // EOF is an implicit page delimiter - pageDelimiters.append(range.length) - - return pageDelimiters + let range = NSRange(location: 0, length: markdown.length) + return (pagesExpression? + .matchesInString(markdown as! String, options: nil, range: range) + .map {$0.range.location} ?? []) + + [range.length] // EOF is an implicit page delimiter } } diff --git a/iOS/DeckRocket/Slide.swift b/iOS/DeckRocket/Slide.swift index 08e24e4..3e10c2c 100644 --- a/iOS/DeckRocket/Slide.swift +++ b/iOS/DeckRocket/Slide.swift @@ -9,7 +9,7 @@ import Foundation import UIKit -class Slide { +struct Slide { // MARK: Properties @@ -22,49 +22,43 @@ class Slide { init(image: UIImage, markdown: String?) { self.image = image - if markdown != nil { - self.markdown = markdown - body = bodyFromMarkdown() - notes = notesFromMarkdown() - } + self.markdown = markdown + body = Slide.bodyFromMarkdown(markdown) + notes = Slide.notesFromMarkdown(markdown) } // MARK: String Parsing - func bodyFromMarkdown() -> String { + private static func bodyFromMarkdown(markdown: NSString?) -> String? { // Skip the trailing \n - let bodyRange = NSRange(location: 0, length: notesStart() - 1) - return (markdown! as NSString).substringWithRange(bodyRange) + return markdown?.substringWithRange(NSRange(location: 0, length: notesStart(markdown!) - 1)) // Safe to force unwrap } - func notesFromMarkdown() -> String? { - let nsMarkdown = markdown! as NSString - - var start = notesStart() - if start == nsMarkdown.length { - // No notes - return nil + private static func notesFromMarkdown(markdown: NSString?) -> String? { + if let markdown = markdown { + let start = notesStart(markdown) + if start == markdown.length { + return nil // No notes + } + // Skip the leading ^ + let startWithOutLeadingCaret = start + 1 + let length = markdown.length - startWithOutLeadingCaret + let notesRange = NSRange(location: startWithOutLeadingCaret, length: length) + return markdown.substringWithRange(notesRange) } - - // Skip the leading ^ - start++ - let length = nsMarkdown.length - start - - let notesRange = NSRange(location: start, length: length) - return nsMarkdown.substringWithRange(notesRange) + return nil } - func notesStart() -> Int { + private static func notesStart(markdown: NSString) -> Int { // Pattern must match http://www.decksetapp.com/support/#how-do-i-add-presenter-notes let pattern = "^\\^" // ^\^ let notesExpression = NSRegularExpression(pattern: pattern, - options: NSRegularExpressionOptions.AnchorsMatchLines, + options: .AnchorsMatchLines, error: nil) - let fullRange = NSRange(location: 0, length: (markdown! as NSString).length) - if let notesMatch = notesExpression?.firstMatchInString(markdown!, options: NSMatchingOptions(0), range: fullRange) { - return notesMatch.range.location - } - return fullRange.length + let fullRange = NSRange(location: 0, length: markdown.length) + return notesExpression? + .firstMatchInString(markdown as! String, options: nil, range: fullRange)?.range.location // Safe to force unwrap + ?? fullRange.length } } diff --git a/iOS/DeckRocket/ViewController.swift b/iOS/DeckRocket/ViewController.swift index ea32556..36dd7ac 100644 --- a/iOS/DeckRocket/ViewController.swift +++ b/iOS/DeckRocket/ViewController.swift @@ -9,16 +9,31 @@ import UIKit import MultipeerConnectivity -class ViewController: UICollectionViewController, UIScrollViewDelegate { +let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as! NSString + +private func userDefaultsString(key: String) -> String? { + return NSUserDefaults.standardUserDefaults().objectForKey(key) as? NSString as? String +} + +private func userDefaultsPathIfFileExists(key: String) -> String? { + if let name = userDefaultsString(key), let path = Optional(documentsPath.stringByAppendingPathComponent(name)) where NSFileManager.defaultManager().fileExistsAtPath(path) { + return path + } + return nil +} + +final class ViewController: UICollectionViewController, UIScrollViewDelegate { // MARK: Properties - var presentation: Presentation? - let multipeerClient = MultipeerClient() - let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .Dark)) - let notesView = UITextView() - let nextSlideView = UIImageView() - let infoLabel = UILabel() + private var presentation: Presentation? + private let multipeerClient = MultipeerClient() + // UIVisualEffectView's alpha can't be animated, so we nest it in a parent view + private let effectParentView = UIView() + private let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .Dark)) + private let notesView = UITextView() + private let nextSlideView = UIImageView() + private let infoLabel = UILabel() // MARK: View Lifecycle @@ -40,31 +55,31 @@ class ViewController: UICollectionViewController, UIScrollViewDelegate { // MARK: Connectivity Updates - func setupConnectivityObserver() { - multipeerClient.onStateChange = {(state: MCSessionState, peerID: MCPeerID) -> () in + private func setupConnectivityObserver() { + multipeerClient.onStateChange = { state, peerID in dispatch_async(dispatch_get_main_queue(), { self.notesView.alpha = 0 self.nextSlideView.alpha = 0 switch state { case .NotConnected: if self.multipeerClient.session == nil { - self.effectView.alpha = 1 + self.effectParentView.alpha = 1 self.infoLabel.text = "Not Connected" - } else if self.multipeerClient.session!.connectedPeers.count == 0 { - self.effectView.alpha = 1 + } else if self.multipeerClient.session!.connectedPeers.count == 0 { // Safe to force unwrap + self.effectParentView.alpha = 1 self.infoLabel.text = "Not Connected" - self.multipeerClient.browser!.invitePeer(peerID, toSession: self.multipeerClient.session, withContext: nil, timeout: 30) + self.multipeerClient.browser?.invitePeer(peerID, toSession: self.multipeerClient.session, withContext: nil, timeout: 30) } case .Connected: if let presentation = self.presentation { - self.effectView.alpha = 0 + self.effectParentView.alpha = 0 self.infoLabel.text = "" } else { - self.effectView.alpha = 1 + self.effectParentView.alpha = 1 self.infoLabel.text = "No Presentation Loaded" } case .Connecting: - self.effectView.alpha = 1 + self.effectParentView.alpha = 1 self.infoLabel.text = "Connecting..." } }) @@ -74,22 +89,24 @@ class ViewController: UICollectionViewController, UIScrollViewDelegate { // MARK: Presentation Updates func updatePresentation() { - if let pdfPath = NSUserDefaults.standardUserDefaults().objectForKey("pdfPath") as? NSString { - var markdown: String? - if let mdPath = NSUserDefaults.standardUserDefaults().objectForKey("mdPath") as? NSString { + if let pdfPath = userDefaultsPathIfFileExists("pdfName") { + let markdown: String? + if let mdPath = userDefaultsPathIfFileExists("mdName") { markdown = String(contentsOfFile: mdPath, encoding: NSUTF8StringEncoding) + } else { + markdown = nil } - presentation = Presentation(pdfPath: pdfPath, markdown: markdown?) - collectionView!.contentOffset.x = 0 - collectionView!.reloadData() + presentation = Presentation(pdfPath: pdfPath, markdown: markdown) + collectionView?.contentOffset.x = 0 + collectionView?.reloadData() } - // Force state change block - multipeerClient.onStateChange!!(state: multipeerClient.state, peerID: MCPeerID()) + // Trigger state change block + multipeerClient.onStateChange??(state: multipeerClient.state, peerID: MCPeerID()) } // MARK: UI - func setupUI() { + private func setupUI() { setupCollectionView() setupEffectView() setupInfoLabel() @@ -97,24 +114,32 @@ class ViewController: UICollectionViewController, UIScrollViewDelegate { setupNextSlideView() } - func setupCollectionView() { - collectionView!.registerClass(Cell.self, forCellWithReuseIdentifier: "cell") - collectionView!.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: "longPress:")) - collectionView!.pagingEnabled = true - collectionView!.showsHorizontalScrollIndicator = false + private func setupCollectionView() { + collectionView?.registerClass(Cell.self, forCellWithReuseIdentifier: "cell") + collectionView?.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: "longPress:")) + collectionView?.pagingEnabled = true + collectionView?.showsHorizontalScrollIndicator = false } - func setupEffectView() { - effectView.setTranslatesAutoresizingMaskIntoConstraints(false) - view.addSubview(effectView) + private func setupEffectView() { + effectParentView.setTranslatesAutoresizingMaskIntoConstraints(false) + view.addSubview(effectParentView) - let horizontal = NSLayoutConstraint.constraintsWithVisualFormat("|[effectView]|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["effectView": effectView]) - let vertical = NSLayoutConstraint.constraintsWithVisualFormat("V:|[effectView]|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["effectView": effectView]) + let horizontal = NSLayoutConstraint.constraintsWithVisualFormat("|[effectParentView]|", options: nil, metrics: nil, views: ["effectParentView": effectParentView]) + let vertical = NSLayoutConstraint.constraintsWithVisualFormat("V:|[effectParentView]|", options: nil, metrics: nil, views: ["effectParentView": effectParentView]) view.addConstraints(horizontal) view.addConstraints(vertical) + + effectView.setTranslatesAutoresizingMaskIntoConstraints(false) + effectParentView.addSubview(effectView) + + let horizontal2 = NSLayoutConstraint.constraintsWithVisualFormat("|[effectView]|", options: nil, metrics: nil, views: ["effectView": effectView]) + let vertical2 = NSLayoutConstraint.constraintsWithVisualFormat("V:|[effectView]|", options: nil, metrics: nil, views: ["effectView": effectView]) + effectParentView.addConstraints(horizontal2) + effectParentView.addConstraints(vertical2) } - func setupInfoLabel() { + private func setupInfoLabel() { infoLabel.setTranslatesAutoresizingMaskIntoConstraints(false) infoLabel.text = "Not Connected" infoLabel.textColor = UIColor.whiteColor() @@ -138,7 +163,7 @@ class ViewController: UICollectionViewController, UIScrollViewDelegate { effectView.addConstraints([centerX, centerY]) } - func setupNotesView() { + private func setupNotesView() { notesView.setTranslatesAutoresizingMaskIntoConstraints(false) notesView.font = UIFont.systemFontOfSize(30) notesView.backgroundColor = UIColor.clearColor() @@ -147,13 +172,13 @@ class ViewController: UICollectionViewController, UIScrollViewDelegate { notesView.alpha = 0 effectView.addSubview(notesView) - let horizontal = NSLayoutConstraint.constraintsWithVisualFormat("|[notesView]|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["notesView": notesView]) - let vertical = NSLayoutConstraint.constraintsWithVisualFormat("V:|-20-[notesView]|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["notesView": notesView]) + let horizontal = NSLayoutConstraint.constraintsWithVisualFormat("|[notesView]|", options: nil, metrics: nil, views: ["notesView": notesView]) + let vertical = NSLayoutConstraint.constraintsWithVisualFormat("V:|-20-[notesView]|", options: nil, metrics: nil, views: ["notesView": notesView]) effectView.addConstraints(horizontal) effectView.addConstraints(vertical) } - func setupNextSlideView() { + private func setupNextSlideView() { nextSlideView.setTranslatesAutoresizingMaskIntoConstraints(false) nextSlideView.contentMode = UIViewContentMode.ScaleAspectFit effectView.addSubview(nextSlideView) @@ -207,51 +232,54 @@ class ViewController: UICollectionViewController, UIScrollViewDelegate { break default: // Don't do anything if the effect view is now being used to show a connectivity message - if multipeerClient.session!.connectedPeers.count > 0 { + if let session = multipeerClient.session where session.connectedPeers.count > 0 { showNotes(false) } } } - func showNotes(show: Bool) { - let currentSlideIndex = Int(currentSlide()) - notesView.text = presentation!.slides[currentSlideIndex].notes - notesView.alpha = 1 - nextSlideView.alpha = 1 - - if currentSlideIndex < presentation!.slides.count - 1 { - nextSlideView.image = presentation!.slides[currentSlideIndex+1].image - } else { - nextSlideView.image = nil - } - UIView.animateWithDuration(0.25, animations: { - self.effectView.alpha = CGFloat(show) - }) { finished in - self.notesView.alpha = CGFloat(show) - self.nextSlideView.alpha = CGFloat(show) + private func showNotes(show: Bool) { + if let presentation = presentation { + let currentSlideIndex = Int(currentSlide()) + notesView.text = presentation.slides[currentSlideIndex].notes + notesView.alpha = 1 + nextSlideView.alpha = 1 + + if currentSlideIndex < presentation.slides.count - 1 { + nextSlideView.image = presentation.slides[currentSlideIndex + 1].image + } else { + nextSlideView.image = nil + } + let alpha = CGFloat(show) + UIView.animateWithDuration(0.25, animations: { + self.effectParentView.alpha = alpha + }) { finished in + self.notesView.alpha = alpha + self.nextSlideView.alpha = alpha + } } } // MARK: Collection View override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - if let presentation = self.presentation { - return presentation.slides.count - } - return 0 + return presentation?.slides.count ?? 0 } override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as Cell - let slide = presentation!.slides[indexPath.item] - cell.imageView.image = slide.image + let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! Cell + let slide = presentation?.slides[indexPath.item] + cell.imageView.image = slide?.image return cell } // MARK: UIScrollViewDelegate - func currentSlide() -> UInt { - return UInt(round(collectionView!.contentOffset.x / collectionView!.frame.size.width)) + private func currentSlide() -> UInt { + if let collectionView = collectionView { + return UInt(round(collectionView.contentOffset.x / collectionView.frame.size.width)) + } + return 0 } override func scrollViewDidEndDecelerating(scrollView: UIScrollView) { @@ -263,18 +291,18 @@ class ViewController: UICollectionViewController, UIScrollViewDelegate { override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) { // Update Layout - let layout = collectionView!.collectionViewLayout as CollectionViewLayout - layout.invalidateLayout() - layout.itemSize = CGSize(width: view.bounds.size.height, height: view.bounds.size.width) + let layout = collectionView?.collectionViewLayout as? CollectionViewLayout + layout?.invalidateLayout() + layout?.itemSize = CGSize(width: view.bounds.size.height, height: view.bounds.size.width) // Update Offset - let targetOffset = CGFloat(self.currentSlide()) * layout.itemSize.width + let targetOffset = CGFloat(currentSlide()) * (layout?.itemSize.width ?? 0) // We do this half-way through the animation let delay = (duration / 2) * NSTimeInterval(NSEC_PER_SEC) let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay)) dispatch_after(time, dispatch_get_main_queue()) { - self.collectionView!.contentOffset.x = targetOffset + self.collectionView?.contentOffset.x = targetOffset } } }