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

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

    16-Jul-2015
  • Category

    Technology

  • view

    426
  • download

    2

Embed Size (px)

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 doesnt 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 didnt 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 apps 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 theres 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 Apples, 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 its a method you want to handle.

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

    For fun, return true for everything youll 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 thats 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 targets Info.plist (on iOS too!)

  • Q&[email protected] [email protected]

    Slides: http://slideshare.com/invalidnameCode: http://github.com/invalidname