Skip to main content
Revenge of the ’80s

Cut/Copy/Paste, Undo/Redo, and
More Big Hits
Chris Adamson • @invalidname

CocoaConf 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 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
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
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