Revenge of the 80s: Cut/Copy/Paste, Undo/Redo, and More Big Hits (CocoaConf Chicago, 2015)

Post on 16-Jul-2015

426 views 2 download

Transcript of Revenge of the 80s: Cut/Copy/Paste, Undo/Redo, and More Big Hits (CocoaConf Chicago, 2015)

Revenge of the ’80sCut/Copy/Paste, Undo/Redo, and

More Big HitsChris Adamson • @invalidnameCocoaConf Chicago, March 2015

Edit Menu• Undo: Returns you to the state prior to your

previous action˝

• Redo: Undoes an undo˝

• Cut: Remove selection, put it on clipboard˝

• Copy: Put selection on clipboard˝

• Paste: Insert contents of clipboard˝

• Delete: Delete selection

Demo

DuranDuranFans

Where do these events come from?

MainMenu.nib or Main.storyboard

First Responder

The Responder Chain

“The responder chain is a linked series of responder objects to which an event or action message is applied. When a given responder object doesn’t handle a particular message, the object passes the message to its successor in the chain (that is, its next responder). This allows responder objects to delegate responsibility for handling the message to other, typically higher-level objects.”

NSResponder

• NSView˝

• NSViewController˝

• NSWindow˝

• NSWindowController

Responding to menu events

@IBAction func delete (sender: AnyObject!) {˝ NSLog ("delete")˝ deleteSelectedFan()˝}

–Clueless User

“But I didn’t mean to delete!”

NSUndoManager

• Object to manage a stack of undoable actions˝

• You get an undoManager property for free from NSResponder and NSDocument˝

• Test with canUndo/canRedo, perform with undo and redo

Undoable Actions• UndoManager.registerUndoWithTarget(_:

selector:object:) adds an undoable action to the stack˝

• Target is the object to be called˝

• Selector is a method that takes one parameter˝

• Object is the parameter

Deleting a Fan

func deleteSelectedFan() {˝ let index = fansTable.selectedRow˝ if index < 0 {˝ return˝ }˝ let deletedFan = fans[index]˝ let undoInfo = ["fan" : deletedFan , "index" : index]˝ undoManager?.registerUndoWithTarget(self,˝ selector: “undoDeleteFan:",˝ object: undoInfo)˝ deleteAtIndex(index)˝}

undoDeleteFan

func undoDeleteFan (userInfo: AnyObject?) {˝ NSLog ("undoDeleteFan")˝ if let undoInfo = userInfo as? [String : AnyObject] {˝ let fan = undoInfo["fan"] as Fan?˝ let index = undoInfo["index"] as Int?˝ if fan != nil && index != nil {˝ addFan (fan!, atIndex: index!)˝ }˝ }˝}

addFan

func addFan (fan: Fan, atIndex: Int) {˝ NSLog ("addFan")˝ fans.insert(fan, atIndex: atIndex)˝ let undoInfo = ["fan" : fan, "index" : atIndex]˝ undoManager?.registerUndoWithTarget(self,˝ selector: “undoAddFan:",˝ object: undoInfo)˝ fansTable.reloadData()˝}

Redo• Redoable actions are simply undoable

actions registered in the course of performing an undo˝

• undoDeleteFan() calls addFan(), which registers an undoable action to perform a delete (undoAddFan(), which calls deleteAtIndex())˝

• So, redo calls the delete

Undo considerations• Undo action has to contain all the data

needed to restore old state˝

• For a delete, this means the deleted object is typically retained by the undo manager˝

• Memory considerations˝

• Performance considerations˝

• Consider setting levelsOfUndo, or clear stack with removeAllActions

Cut / Copy / Paste

NSPasteboard

• Storage for cut/copy/paste items˝

• System provides a “general” pasteboard, plus named pasteboards for drag-and-drop, fonts, and rulers˝

• Can also create your own pasteboard

One object, many flavors

• Each app has its own distinct abilities and limitations when it comes to reading and writing data˝

• So, objects on the pasteboard can have many different representations˝

• Image could PDF, PNG, JPEG, etc.; text could be plain-text, RTF, etc.˝

• Cut/copy provide multiple representations; paste indicates the types it can work with

Uniform Type Identifiers (UTI)

UTIs

• A UTI is a string that describes a type of data˝

• Arranged in a hierarchy: more specific types “conform to” a more general type˝

• public.jpeg conforms to public.image

Standard UTIs

• Defined in CoreServices.h˝

• Text types: kUTTypePlainText, kUTTypeHTML, kUTTypeCSource˝

• Image types: kUTTypeJPEG, kUTTypePNG˝

• Others: kUTTypePDF, kUTTypeMPEG4, kUTTypeVCard, etc.

Custom UTIs

• Create a unique reverse-DNS string (“com.cocoaconf.mytype”) and declare it in your app’s Info.plist

Writing to NSPasteboard

• NSPasteboard.setData(forType:) takes an NSData and a string type˝

• Type is either a UTI or some legacy NSPasteboard constants˝

• NSPasteboard.writeObjects() lets you write multiple objects

NSPasteboardWriting

• Protocol for classes you want to add to the pasteboard with writeObjects()˝

• writeableTypesForPasteboard(): Indicates the types you provide˝

• pasteboardPropertyListForType(): Provides the contents for a given type, typically as an NSData

Fan class

let FAN_UTI = "com.subfurther.cocoaconf.80s.fan"˝

class Fan : NSObject, NSCoding, NSPasteboardWriting,˝ NSPasteboardReading {˝ var firstName: String?˝ var lastName: String?˝ var favoriteSong: String?

Fan class: NSPasteboardWriting

func writableTypesForPasteboard(pasteboard: NSPasteboard!) -> [AnyObject]! {˝ return [FAN_UTI, NSPasteboardTypeString]˝}˝ ˝func pasteboardPropertyListForType(type: String!) -> AnyObject! {˝ switch type {˝ case NSPasteboardTypeString:˝ return fullName()˝ case FAN_UTI:˝ let data = NSMutableData()˝ let archiver = NSKeyedArchiver(forWritingWithMutableData: data)˝ archiver.encodeObject(firstName, forKey: "firstName")˝ archiver.encodeObject(lastName, forKey: "lastName")˝ archiver.encodeObject(favoriteSong, forKey: "favoriteSong")˝ archiver.finishEncoding()˝ return data˝ default:˝ return nil˝ }˝}

Reading from NSPasteboard

• Fetch contents for a given type with dataForType(), stringForType, or propertyListForType()˝

• Initialize custom objects from pasteboard data by implementing NSPasteboardReading

NSPasteboardReading• readableTypesForPasteboard(): Indicates

which types you read, in order of preference.˝

• Initialize from pasteboard:˝

• Obj-C: initWithPasteboardPropertyList:ofType˝

• Swift: init (pasteboardPropertyList:ofType:)

Fan class: NSPasteboardReading

class func readableTypesForPasteboard(pasteboard: NSPasteboard!) -> [AnyObject]! {˝ return [FAN_UTI]˝}˝ ˝required init?(pasteboardPropertyList propertyList: AnyObject!, ofType type: String!) {˝ super.init()˝ switch type {˝ case NSPasteboardTypeString:˝ return nil˝ case FAN_UTI:˝ if let data = propertyList as? NSData {˝ let unarchiver = NSKeyedUnarchiver(forReadingWithData: data)˝ self.firstName = unarchiver.decodeObjectForKey("firstName") as String?˝ self.lastName = unarchiver.decodeObjectForKey("lastName") as String?˝ self.favoriteSong = unarchiver.decodeObjectForKey("favoriteSong") as String?˝ }˝ default:˝ return nil // die˝ }˝}

What about mobile?

Demo

HammerFans

HammerFans• Functionally equivalent to DuranDuranFans

• Mostly reused the Fan class

• Mostly reused the undo/redo code

• undoManager provided by UIResponder & UIDocument

• Pasteboard is pretty different

• But where do the events come from, since there’s no MainMenu?

UIMenuController

• Commonly shown in response to a long-press gesture

• Shows a popup menu targeted at a given CGRect in a UIView

• Allows you to add UIMenuItems

• Your own items will always appear after all of Apple’s, frequently on a second or third “page” of menu items

Show UIMenuController// MARK - UIMenuController @IBAction func handleTableLongPress(sender: AnyObject) { var targetRect = CGRect (x: self.view.bounds.width/2.0, y: self.view.bounds.height/2.0, width: 0, height: 0) if let recognizer = sender as? UIGestureRecognizer { let touchPoint = recognizer.locationInView(fansTable) targetRect = CGRect(origin: touchPoint, size: CGSizeZero) // also auto-select row, if possible let indexPath = fansTable.indexPathForRowAtPoint (touchPoint) if indexPath != nil { fansTable.selectRowAtIndexPath(indexPath!, animated: false, scrollPosition: .None) } } UIMenuController.sharedMenuController().setTargetRect(targetRect, inView: fansTable) UIMenuController.sharedMenuController().setMenuVisible(true, animated: true) }

UIResponderStandardEditActions

• Informal protocol on NSObject

• Defines actions that editing controllers are anticipated to provide:

• cut:, copy:, delete:, paste:, select:, selectAll:

• By default, shake gesture will send undo: or redo: up the responder chain too

Handling a menu action• Your view or viewController must override

UIResponder.canBecomeFirstResponder() to return true/YES

• When queried with UIResponder.canPerformAction(withSender:), inspect the selector to see if it’s a method you want to handle.

• If so, return true/YES; if not, typically call super.canPerformAction:

• For fun, return true for everything — you’ll see all the accessibility, I18N, and spelling/autocomplete calls. Beware: any action starting with “_” is a private API

UIResponder.canPerformAction()

override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool { switch action { case "copy:","cut:","paste:","delete:": return true default: return super.canPerformAction(action, withSender: sender) } }

Responding to menu events

override func delete (sender: AnyObject!) { NSLog ("delete") deleteSelectedFan() }

This is identical to the Mac version, except that delete: must be marked as an override in Swift (overrides

UIResponderStandardEditActions)

UIPasteboard• Similar in spirit to NSPasteboard

• Two system pasteboards, general and name-find, plus you can make your own

• Get and set pasteboard data by a string type, typically a UTI

• Does not provide equivalents for NSPasteboardReading, NSPasteboardWriting

• Common UTIs defined in MobileCoreServices.h

Custom objects on pasteboard

• Get/set the UIPasteboard.items property

• Array of dictionaries

• Each dictionary is keyed by its type, and the value is the representation (often NSData) of that type

Writing to UIPasteboardfunc writeSelectedFanToPasteboard () { let selection = selectedFanAndIndex() if selection != nil { UIPasteboard.generalPasteboard().items =

[selection!.fan.dictionaryForPasteboard()] } }

func dictionaryForPasteboard() -> [NSString : AnyObject] { var dictionary : [NSString : AnyObject] = [:] // public.text dictionary [kUTTypeText] = self.fullName() // FAN_UTI let data = NSMutableData() let archiver = NSKeyedArchiver(forWritingWithMutableData: data) archiver.encodeObject(firstName, forKey: "firstName") archiver.encodeObject(lastName, forKey: "lastName") archiver.encodeObject(favoriteSong, forKey: "favoriteSong") archiver.finishEncoding() dictionary [FAN_UTI] = data return dictionary }

ViewController

Fan

Reading from UIPasteboardoverride func paste (sender: AnyObject!) { NSLog ("paste") let pasteboard = UIPasteboard.generalPasteboard() if let pasteData = pasteboard.dataForPasteboardType(FAN_UTI) { if let fan = Fan (pasteboardData: pasteData) { addFan(fan, atIndex: fans.count) } } }

init? (pasteboardData: NSData) { super.init() let unarchiver = NSKeyedUnarchiver(forReadingWithData: pasteboardData) self.firstName = unarchiver.decodeObjectForKey("firstName") as String? self.lastName = unarchiver.decodeObjectForKey("lastName") as String? self.favoriteSong = unarchiver.decodeObjectForKey("favoriteSong") as String? if self.firstName == nil || self.lastName == nil { return nil } }

ViewController

Fan

Wrap-up

Wrap-up: Undo/Redo

• Undo Manager is usually provided to you through window/view controller or document class˝

• You register your undoable actions with a target/action metaphor. Your action must be able to re-create the pre-edit state.˝

• Redos are just undoable actions registered while performing an undo

Wrap-Up: Cut, Copy, Paste

• UTIs define how data is represented on the pasteboard (also in documents, but that’s another session)˝

• Try to provide a range of UTIs so other apps can get a richer copy of your data˝

• You can define your own UTIs in the app target’s Info.plist (on iOS too!)

Q&A@invalidname — invalidname@gmail.com˝

Slides: http://slideshare.com/invalidname˝Code: http://github.com/invalidname