When the first 128K Macs landed in 1984, it was the first time many of us could undo a mistake with just a keystroke, or exchange data between documents or applications with cut/copy/paste and the system clipboard. Fast forward 30 years and we all use this stuff… but do you know how to actually impement it? Especially on iOS, these everyday features are surprisingly absent from many developers' toolchests. In this session, we'll flashback to the era of Reagan, Rubik's Cubes, and Return of the Jedi, to see these hot hits of the early 80's are represented in modern-day Cocoa.
WordPress Websites for Engineers: Elevate Your Brand
Revenge of the 80s: Cut/Copy/Paste, Undo/Redo, and More Big Hits (CocoaConf Chicago, 2015)
1. Revenge of the ’80s
Cut/Copy/Paste, Undo/Redo, and
More Big Hits
Chris Adamson • @invalidname
CocoaConf Chicago, March 2015
2.
3.
4.
5. 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
11. 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.”
15. 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
17. 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)˝
}
18. 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!)˝
}˝
}˝
}
20. 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
21. 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
23. 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
24. 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
26. 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
27. Standard UTIs
• Defined in CoreServices.h˝
• Text types: kUTTypePlainText,
kUTTypeHTML, kUTTypeCSource˝
• Image types: kUTTypeJPEG, kUTTypePNG˝
• Others: kUTTypePDF, kUTTypeMPEG4,
kUTTypeVCard, etc.
28. Custom UTIs
• Create a unique reverse-DNS string
(“com.cocoaconf.mytype”) and declare it
in your app’s Info.plist
30. 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
31. 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?
32. 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˝
}˝
}
33. Reading from
NSPasteboard
• Fetch contents for a given type with
dataForType(), stringForType, or
propertyListForType()˝
• Initialize custom objects from
pasteboard data by implementing
NSPasteboardReading
42. 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?
43. 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
44. 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)
}
45. 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
46. 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
48. 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)
49. 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
50. 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
51. Writing to UIPasteboard
func 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
52. Reading from UIPasteboard
override 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
54. 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
55. 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!)