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

56
Revenge of the ’80s Cut/Copy/Paste, Undo/Redo, and More Big Hits Chris Adamson • @invalidname CocoaConf Chicago, March 2015

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

Page 1: 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

Page 2: Revenge of the 80s: Cut/Copy/Paste, Undo/Redo, and More Big Hits (CocoaConf Chicago, 2015)
Page 3: Revenge of the 80s: Cut/Copy/Paste, Undo/Redo, and More Big Hits (CocoaConf Chicago, 2015)
Page 4: Revenge of the 80s: Cut/Copy/Paste, Undo/Redo, and More Big Hits (CocoaConf Chicago, 2015)
Page 5: Revenge of the 80s: Cut/Copy/Paste, Undo/Redo, and More Big Hits (CocoaConf Chicago, 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

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

Demo

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

DuranDuranFans

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

Where do these events come from?

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

MainMenu.nib or Main.storyboard

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

First Responder

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

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.”

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

NSResponder

• NSView˝

• NSViewController˝

• NSWindow˝

• NSWindowController

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

Responding to menu events

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

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

–Clueless User

“But I didn’t mean to delete!”

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

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

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

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

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

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)˝}

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

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!)˝ }˝ }˝}

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

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()˝}

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

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

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

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

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

Cut / Copy / Paste

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

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

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

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

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

Uniform Type Identifiers (UTI)

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

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

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

Standard UTIs

• Defined in CoreServices.h˝

• Text types: kUTTypePlainText, kUTTypeHTML, kUTTypeCSource˝

• Image types: kUTTypeJPEG, kUTTypePNG˝

• Others: kUTTypePDF, kUTTypeMPEG4, kUTTypeVCard, etc.

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

Custom UTIs

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

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

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

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

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

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

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?

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

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˝ }˝}

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

Reading from NSPasteboard

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

• Initialize custom objects from pasteboard data by implementing NSPasteboardReading

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

NSPasteboardReading• readableTypesForPasteboard(): Indicates

which types you read, in order of preference.˝

• Initialize from pasteboard:˝

• Obj-C: initWithPasteboardPropertyList:ofType˝

• Swift: init (pasteboardPropertyList:ofType:)

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

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˝ }˝}

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

What about mobile?

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

Demo

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

HammerFans

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

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?

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

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

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

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) }

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

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

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

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

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

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) } }

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

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)

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

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

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

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

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

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

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

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

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

Wrap-up

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

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

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

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!)

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

Q&A@invalidname — [email protected]˝

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