Revenge of the 80s: Cut/Copy/Paste, Undo/Redo, and More Big Hits (CocoaConf Chicago, 2015)
-
Upload
chris-adamson -
Category
Technology
-
view
426 -
download
2
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 — [email protected]˝
Slides: http://slideshare.com/invalidname˝Code: http://github.com/invalidname