In a battlefield, generals hand out timed instructions in sealed envelopes to soldiers or others, who then open the envelopes and execute the instructions in them. The same instructions can be one-timed or re-executed by different persons at given times. Since instructions are kept in envelopes, passing them around different areas for different purposes is easier than other means of instructions—for example, verbal instruction on the phone or other communication channels.
We borrow a similar idea of encapsulating instructions as different command objects in object-oriented design. Command objects can be passed around and reused at given times by different clients. A design pattern that is elaborated from the concept is called the Command pattern.
In this chapter, we are going to discuss the concepts of the pattern. We will also develop an undo infrastructure for our TouchPainter app so the user can redo/undo what he/she has drawn on the screen. The Cocoa Touch adaptation of the pattern will also be discussed later in the chapter.
A command object encapsulates information regarding how to perform instructions on a target, so a client or an invoker doesn't need to know any details of the target but can still be able to perform any available operations on it. By encapsulating a request as an object, clients can parameterize and put it in a queue or log, as well as support undoable operations. The command object binds together one or more actions with a specific receiver. The Command pattern decouples the binding between an action as an object and a receiver that executes it.
Before we dive into the details of the pattern in Objective-C, let's take a brief look at the structure of the pattern as illustrated in Figure 20-1.
So, here is what's happening in the diagram:
The client creates a ConcreteCommand
object and sets its receiver.
The Invoker
asks a generic command (in fact, ConcreteCommand
) to carry out the request.
Command
is a generic interface (protocol) that is known to Invoker
.
ConcreteCommand
acts as a middleman between Receiver
and the action
operation for it.
Receiver
can be any object that performs an actual operation with a corresponding request being carried out by a Command
(ConcreteCommand
) object.
COMMAND PATTERN: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.[18]
You'd naturally think about using the pattern when
You want your application to support undo/redo.
You want to parameterize an action as an object to perform operations and replace callbacks with different command objects
You want to specify, queue, and execute requests at different times.
You want to log changes so they can be reapplied later in case of a system failure.
You want the system to support transactions with which a transaction encapsulates a set of changes to data. The transactions can be modeled as command objects.
You may ask, "Why can't we just execute a method directly without all the hassles?" Technically, yes, you can call whatever methods or functions you want in your program, and your program still compiles and runs. The problem is every class or method needs to know each other's details in order to work, and it's very complicated to implement any other operations, such as undo. When the program grows larger and larger, it would go to the point where it is very difficult to manage and reuse objects.
Before we get to designing and implementing an undo infrastructure, let's look at some useful resources from the Cocoa Touch framework that we can use for setting up the infrastructure in the next section.
Instead of reinventing the wheel, it is a virtue to reuse what is available in the Cocoa Touch framework. The Command pattern is one of cataloged patterns adapted by the framework. By using "off-the-shelf" classes in the framework, we can focus on content development.
NSInvocation
, NSUndoManager
, and the target-action mechanism are typical adaptations of the pattern in the framework. The target-action mechanism has been covered in many other beginner iOS programming books, so we will go over NSInvocation
and NSUndoManager
only.
An instance of the NSInvocation
class is just like the original ConcreteCommand
class shown in Figure 20-1. An NSInvocation
object encapsulates any necessary information to forward an execution message to a receiver at runtime, such as a target object, method selector, and method arguments. So the receiver can be invoked with the embedded selector and other information anytime with the help of the NSInvocation
instance. The same invocation instance can repeatedly invoke the same method on the receiver or be reused with different targets and methods signatures.
NSInvocation
is a generic layer of abstraction for invocation. Framework designers at Apple apparently didn't know anything about our actual receivers, invokers, selectors, and such when they were designing the infrastructure for the NSInvocation
class. So at the creation of an NSInvocation
object, we need to provide any necessary information as a form of an NSMethodSignature
object. An NSMethodSignature
object contains any argument and returns value types of a method invocation. To elaborate from our rather generic example at the beginning of the chapter, we are now going to use NSInvocation
in place of the original Command
and ConcreteCommand
classes as illustrated in Figure 20-2.
We took off the original Command
protocol from our first class diagram, and we replaced the ConcreteCommand
class with the NSInvocation
class provided by the Cocoa Touch framework. The idea is that a client creates a new instance of NSInvocation
object with an instance of Receiver
and its operation as a selector. Then an Invoker
(e.g., a UIButton
object) will be set with the NSInvocation
object. When the Invoker
needs to invoke the action, it will call the invoke
method of the NSInvocation
object that is stored in the Invoker
. Finally, the NSInvocation
object will invoke the selector on its target to finish the process.
Since the introduction of iOS 3.0, NSUndoManager
has been available in the Cocoa Touch framework for iOS application development. NSUndoManager
was elegantly designed as a general-purpose undo stack management class, yet it is powerful and versatile enough to use in almost any type of application. We will go over some key features and the idea of NSUndoManager
here.
An instance of NSUndoManager
manages its own undo and redo stacks. The undo stack maintains all the invoked operations as objects. NSUndoManager
can also "reverse" the undo operation (a.k.a. redo) by invoking the operation object that was pushed from the undo stack. A registered undo operation can be an action that reverses a previous action invoked by a client. Instead of home-brewing our own undo stack, NSUndoManager
saves us all the hassle of dealing with undo as well as redo operations by using various delayed invocation forwarding mechanisms. An undo manager object collects all registered undo operations that occur within a single-run loop cycle, so that an undo can reverse all changes within the cycle. When an NSUndoManager
object is requested to undo the last operation, it will invoke the operation object at the top of the undo stack. When it is done, it will pop the operation object and push it to the redo stack. Likewise when redo is finished, it will move the operation object from the redo stack to the undo stack for the next undo. So NSUndoManager
exchanges operation objects between two stacks to manage the whole command history.
Initially, the undo stack is empty. Registering an undo operation adds an invocation object to the undo stack. To add an undo operation to the undo stack, you need to register it with the object that performs the undo operation. There are two ways to register undo operations with an NSUndoManager
object:
Simple undo registration
Invocation-based registration
Registering a simple undo operation requires a selector that identifies an undo operation with a receiver as arguments. The state is passed to the undo manager prior to any changes. When the undo operation is invoked, the receiver will be reset with the previous state or attributes.
The invocation-based undo involves an NSInvocation
object. Using an NSInvocation
object means it can take a method with any number and type of parameters. This approach is very useful when a state change requires multiple parameters.
In most cases, a client object that contains or manages other objects in an application owns an instance of NSUndoManager
. Since NSUndoManager
doesn't retain its invocation target receivers, the client object needs to make sure the receivers being pushed in the NSUndoManager
's undo stack have a reference count of at least 1. Otherwise, the undo manager may crash the application if one of its receivers is released when performing an undo/redo. Sometimes, there are cases where an object being modified can have its own undo manager and perform its own undo and redo operations—for example, a drawing app that has multiple drawing views, each of which keeps track of all individual strokes and their colors. Each new stroke added to an active drawing view will register a new undo operation with the view's undo manager. If an undo operation is requested on a particular view, the corresponding undo manager will perform the registered undo operation on the receiver so that the last stroke will be removed.
As we described in the previous sections, one of the highlighted uses of the Command pattern is to support undo/redo operations in an application.
We are about to design and implement an undo/redo architecture for the TouchPainter app. In the preceding sections, we have discussed using NSInvocation
to encapsulate an execution as a command object. We have also explored the possibility of borrowing the NSUndoManager
's undo architecture for our own use.
In this section, we will have two sections for different approaches to implement that. In the first one, we will use NSUndoManager
for the design. After that, we will discuss how to build our own version of undo/redo from scratch.
We've already learned some key concepts and features of NSUndoManager
in the preceding sections. In the subsections that follow, we will walk through the process of using NSUndoManager
to manage actionable NSInvocation
objects for drawing and undrawing strokes and dots against the main Scribble
object.
We need a new NSInvocation
object each time when we need the NSUndoManager
to register an undo/redo operation. It is handy to have a method or two that can generate prototype NSInvocation
objects with which we can just modify a couple of parameters of them with some particular Stroke
or Dot
objects on the go. Listing 20-1 shows two methods just for that purpose.
Example 20-1. Methods That Generate NSInvocation
Objects for Performing Drawing and Undrawing Operations Later
- (NSInvocation *) drawScribbleInvocation { NSMethodSignature *executeMethodSignature = [scribble_ methodSignatureForSelector: @selector(addMark: shouldAddToPreviousMark:)]; NSInvocation *drawInvocation = [NSInvocation invocationWithMethodSignature: executeMethodSignature]; [drawInvocation setTarget:scribble_]; [drawInvocation setSelector:@selector(addMark:shouldAddToPreviousMark:)]; BOOL attachToPreviousMark = NO; [drawInvocation setArgument:&attachToPreviousMark atIndex:3]; return drawInvocation; } - (NSInvocation *) undrawScribbleInvocation { NSMethodSignature *unexecuteMethodSignature = [scribble_ methodSignatureForSelector: @selector(removeMark:)]; NSInvocation *undrawInvocation = [NSInvocation invocationWithMethodSignature: unexecuteMethodSignature]; [undrawInvocation setTarget:scribble_]; [undrawInvocation setSelector:@selector(removeMark:)];
return undrawInvocation; }
The drawScribbleInvocation
method generates an NSInvocation
object that will be used for adding a Mark
object to scribble_
. It requires an NSMethodSignature
object for a selector that will be invoked to instantiate an NSInvocation
object. In this case, we need a method signature for the addMark:shouldAddToPreviousMark:
method of a Scribble
object. After we create an NSInvocation
object, we set the second argument (at index 3 from 0) of a BOOL
value default to NO
because we undo/redo only complete strokes and dots, not vertices. User arguments collected in the NSInvocation
object begin at index 2 because the first one (at index 0) is the receiver and the second one is _cmd
that contains the name of the selector being invoked.
Likewise, the undrawScribbleInvocation
method creates an NSInvocation
object the same way except that its selector for the invocation is now removeMark:
from a Scribble
object. The invocation
object is for undoing a complete stroke or dot later. When we get to the actual drawing code, we'll see how we can assign a real Mark
object to each of invocation objects generated from the methods.
We've seen how to generate NSInvocation
objects for drawing/undrawing strokes and dots in action, but we also need to register them with the NSUndoManager
to make those invocations undoable. Listing 20-2 shows the implementation of the methods that are used for registering undo/redo operations with the NSUndoManager
of CanvasViewController
.
Example 20-2. Methods for Executing and Unexecuting NSInvocation
Objects with Help from NSUndoManager
#pragma mark Draw Scribble Command Methods - (void) executeInvocation:(NSInvocation *)invocation withUndoInvocation:(NSInvocation *)undoInvocation { [invocation retainArguments]; [[self.undoManager prepareWithInvocationTarget:self] unexecuteInvocation:undoInvocation withRedoInvocation:invocation]; [invocation invoke]; } - (void) unexecuteInvocation:(NSInvocation *)invocation withRedoInvocation:(NSInvocation *)redoInvocation { [[self.undoManager prepareWithInvocationTarget:self] executeInvocation:redoInvocation withUndoInvocation:invocation]; [invocation invoke]; }
Both the executeInvocation:withUndoInvocation:
and unexecuteInvocation: withRedoInvocation:
methods look very similar and somewhat confusing. The first method takes an invocation object to execute straightaway and registers another invocation object as an undo operation. The latter method uses an invocation object in the first parameter to execute undo operations and registers the second one for redo.
In the executeInvocation:
method, we send a prepareWithInvocationTarget:
method to CanvasViewController
's NSUndoManager
to register an undo operation. We pass in unexecuteInvocation:undoInvocation withRedoInvocation:invocation
as a complete invocation message for an event of undo. The unexecuteInvocation:
method registers the invocation
object in executeInvocation:
as a redo operation. If you look closely, you will notice that they are crisscrossing with an invocation object that draws and another invocation object that undraws what has been drawn.
Now we are getting to change the original touch event handlers in CanvasViewController
to create invocation objects to prepare all the drawing actions for undo/redo. Listing 20-3 shows the required modifications. We'll break it up into two parts to discuss.
Example 20-3. Modifications to the Original Touch Event Handlers in CanvasViewController
That Accommodate the NSInvocation
-Based Undo/Redo Operations with NSUndoManager
#pragma mark - #pragma mark Touch Event Handlers - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { startPoint_ = [[touches anyObject] locationInView:canvasView_]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { CGPoint lastPoint = [[touches anyObject] previousLocationInView:canvasView_]; // add a new stroke to scribble // if this is indeed a drag from // a finger if (CGPointEqualToPoint(lastPoint, startPoint_)) { id <Mark> newStroke = [[[Stroke alloc] init] autorelease]; [newStroke setColor:strokeColor_]; [newStroke setSize:strokeSize_]; [scribble_ addMark:newStroke shouldAddToPreviousMark:NO];// retrieve a new NSInvocation for drawing and
// set new arguments for the draw command
NSInvocation *drawInvocation = [self drawScribbleInvocation];
[drawInvocation setArgument:&newStroke atIndex:2];
// retrieve a new NSInvocation for undrawing and
// set a new argument for the undraw command
NSInvocation *undrawInvocation = [self undrawScribbleInvocation];
[undrawInvocation setArgument:&newStroke atIndex:2];
// execute the draw command with the undraw command
[self executeInvocation:drawInvocation withUndoInvocation:undrawInvocation];
} // add the current touch as another vertex to the // temp stroke CGPoint thisPoint = [[touches anyObject] locationInView:canvasView_]; Vertex *vertex = [[[Vertex alloc] initWithLocation:thisPoint] autorelease]; // we don't need to undo every vertex // so we are keeping this [scribble_ addMark:vertex shouldAddToPreviousMark:YES]; }
The touch event handlers in this listing are discussed in the "Updating Strokes on the CanvasView in TouchPainter" section of Chapter 12. So we will not repeat ourselves with its details here but will highlight some key areas of the touch handling processes and the changes we make for undo/redo.
In the touchesMoved:
method, the type of Mark
object is determined by two situations:
If the previous touch was the first touch on the screen, then we will create an instance of Stroke
and attach it to scribble_
under the main parent.
Other touches in the method will be treated as vertices of a stroke. So we will create an instance of Vertex
and attach it to scribble_
under that last child Mark
(i.e., a Stroke
we added before) in the main parent.
At the point where it identifies the beginning of a Stroke
object, we removed the original message statement of addMark:shouldAddToPreviousMark:
for scribble_
for adding a new Stroke
object. Instead, we use the *scribbleInvocation
methods (* here is a general purpose wildcard character, not a notation for a pointer) defined previously to generate template invocation objects for adding and removing the new Stroke
object.
After setting appropriate arguments for the drawInvocation
and undrawInvocation
objects, we set them for executeInvocation
: to start executing the drawing operation. drawInvocation
will be invoked in the method, and at the same time undrawInvocation
will be registered as an undo operation.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { CGPoint lastPoint = [[touches anyObject] previousLocationInView:canvasView_]; CGPoint thisPoint = [[touches anyObject] locationInView:canvasView_]; // if the touch never moves (stays at the same spot until lifted now) // just add a dot to an existing stroke composite // otherwise add it to the temp stroke as the last vertex if (CGPointEqualToPoint(lastPoint, thisPoint))
{ Dot *singleDot = [[[Dot alloc] initWithLocation:thisPoint] autorelease]; [singleDot setColor:strokeColor_]; [singleDot setSize:strokeSize_]; [scribble_ addMark:singleDot shouldAddToPreviousMark:NO];// retrieve a new NSInvocation for drawing and
// set new arguments for the draw command
NSInvocation *drawInvocation = [self drawScribbleInvocation];
[drawInvocation setArgument:&singleDot atIndex:2];
// retrieve a new NSInvocation for undrawing and
// set a new argument for the undraw command
NSInvocation *undrawInvocation = [self undrawScribbleInvocation];
[undrawInvocation setArgument:&singleDot atIndex:2];
// execute the draw command with the undraw command
[self executeInvocation:drawInvocation withUndoInvocation:undrawInvocation];
} // reset the start point here startPoint_ = CGPointZero; // if this is the last point of stroke // don't bother to draw it as the user // won't tell the difference } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { // reset the start point here startPoint_ = CGPointZero; }
When it comes to drawing a dot in the touchesEnded
: method, we follow the same procedure we did for drawing a stroke. We use the same singleDot
object for the executeInvocation:
method.
So far we have implemented the undo/redo infrastructure with the help of NSUndoManager
. In the following sections, we are going to discuss how to build our own undo/redo from the ground up. Free feel to read on if you are interested in knowing how a bare-bones undo/redo can be implemented with the Command pattern in Objective-C. Otherwise, you can skip to the "Allowing the User to Activate Undo/Redo" section toward the end of the chapter.
Congratulations! You have made it to the point where we are about to brewing our own undo/redo architecture. Before we get to the design, let's look at how we can collect and put together all the commands executed in an application. Executed command objects can be collected and queued as a list of command history, as shown in Figure 20-3.
As long as there is a new command object executed and added to the list, the list can keep growing in one direction. In a traditional sense, you can perform undo or redo operations with any one of them by traversing the command history list. There are many approaches to implementing a command history. One of the common ways is to use an index to keep track of the current command objects in the list. By increasing or decreasing the index value, you can navigate to a particular command object for either undo or redo. In fact, using indexing to navigate for either undo or redo operations can be quite tricky. There is a simpler and less error-prone way. Instead of using a single command list for both undo and redo, we can have one stack for undo and another one for redo just like NSUndoManager
. Executed command objects are pushed to the undo stack. The one at the top is always the last command executed. When an application needs to undo the last execution, it will pop the last command off the undo stack and then undo it. When it's done, the application will push the command to the redo stack. Until all the command objects have been undone, the redo stack is already filled up with all the undone command objects. A similar procedure applies to redo, just in a different direction. A diagram that illustrates this mechanism is shown in Figure 20-4.
Figure 20-4. Transferring aCommand
from the top of the undo stack to a redo stack during an undo operation
Command objects are just being popped and pushed between the two stacks without using any complex indexing scheme to navigate the list.
We are going to implement undo/redo operations with the two-stack approach for our faithful TouchPainter app. When a user touches the canvas, either a stroke or a dot shows up on the screen. First of all, we need to make a "drawing" action as a command object. After it draws something, the app will push it to the undo stack until we need it back to undraw what it did. We call that command for drawing DrawScribbleCommand
, as shown in a class diagram in Figure 20-5.
Command
is the parent abstract class for DrawScribbleCommand
. It declares some common abstract operations of execute
and undo
. Command
also has a concrete userInfo
property that allows clients to provide any extra parameters for a Command
object. DrawScribbleCommand
has a reference to a Scribble
object so it can add or remove any Mark
objects from it. CanvasViewController
here is both a client and invoker. The overall class structure is quite similar to the original class diagram for the Command pattern in Figure 20-1.
So, how can we implement it in Objective-C? Let's take a look at the abstract Command
class in Listing 20-4.
Example 20-4. A Class Declaration of Command
in Command.h
@interface Command : NSObject { @protected NSDictionary *userInfo_; } @property (nonatomic, retain) NSDictionary *userInfo; - (void) execute; - (void) undo; @end
Besides the execute
and undo
methods, Command
declares the userInfo
property as NSDictionary
. Objects of Command
's subclasses can use what's in the userInfo
to perform any operations in an overridden execute
method. The implementation of Command
is shown in Listing 20-5.
Example 20-5. The Implementation of Command
in Command.m
#import "Command.h" @implementation Command @synthesize userInfo=userInfo_; - (void) execute { // should throw an exception. } - (void) undo { // do nothing // subclasses need to override this // method to perform actual undo. } - (void) dealloc {
[userInfo_ release]; [super dealloc]; } @end
There is not much going on in the implementation of Command
as both the execute
and undo
methods are abstract operations. Its subclasses should put some actions in them. Now let's take a look at DrawScribbleCommand
in Listing 20-6.
Example 20-6. A Class Declaration of DrawScribbleCommand
in DrawScribbleCommand.h
#import "Command.h" #import "Scribble.h" @interface DrawScribbleCommand : Command { @private Scribble *scribble_; id <Mark> mark_; BOOL shouldAddToPreviousMark_; } - (void) execute; - (void) undo; @end
It declares some private member variables to help perform some operations on a Scribble
object later. We are also re-declaring the execute
and undo
methods in the subclass, so when we look at this class again in the future we won't get lost. The implementation of execute
and undo
methods is shown in Listing 20-7.
Example 20-7. The Implementation of DrawScribbleCommand
in DrawScribbleCommand.m
#import "DrawScribbleCommand.h" @implementation DrawScribbleCommand - (void) execute { if (!userInfo_) return; scribble_ = [userInfo_ objectForKey:ScribbleObjectUserInfoKey]; mark_ = [userInfo_ objectForKey:MarkObjectUserInfoKey]; shouldAddToPreviousMark_ = [(NSNumber *)[userInfo_ objectForKey:AddToPreviousMarkUserInfoKey] boolValue]; [scribble_ addMark:mark_ shouldAddToPreviousMark:shouldAddToPreviousMark_]; } - (void) undo { [scribble_ removeMark:mark_]; }
@end
The overridden execute
method is dependent on the information embedded in the userInfo
property. If there is no userInfo
provided, the method will just bail out. OK, so what's in the userInfo
that is so important to DrawScribbleCommand
? The userInfo
dictionary package contains three key elements that are crucial for using a Scribble
object—first of all, a target Scribble
object. Without that, nothing else matters. Then there is an instance of Mark
that should be added to the Scribble
object. Finally, a BOOL
value tells how the Mark
instance should be attached in the Scribble
object. For a detailed discussion of Scribble
and its operations, see the Observer pattern, Chapter 12.
The undo
method simply tells the stored Scribble
object to remove a stored Mark
reference.
Now we know how a DrawScribbleCommand
performs actual drawing with a Scribble
object. Let's see how a bunch of DrawScribbleCommand
objects are involved in undo/redo operations defined in CanvasViewController
, as shown in Listing 20-8.
Example 20-8. Methods That Manage Command
Objects That Draw Scribbles in CanvasViewController
#pragma mark - #pragma mark Draw Scribble Command Methods - (void) executeCommand:(Command *)command prepareForUndo:(BOOL)prepareForUndo { if (prepareForUndo) { // lazy-load undoStack_ if (undoStack_ == nil) { undoStack_ = [[NSMutableArray alloc] initWithCapacity:levelsOfUndo_]; } // drop the bottom one if the // undo stack is full if ([undoStack_ count] == levelsOfUndo_) { [undoStack_ dropBottom]; } // push the command // to our undo stack [undoStack_ push:command]; } [command execute]; }
The executeCommand:prepareForUndo:
method is responsible for executing an incoming Command
object as well as pushing it in undoStack_
. The method also maintains the size of undoStack_
by dropping the one at the bottom if the stack is already full.
- (void) undoCommand { Command *command = [undoStack_ pop]; [command undo]; // push the command to the redo stack if (redoStack_ == nil) { redoStack_ = [[NSMutableArray alloc] initWithCapacity:levelsOfUndo_]; } [redoStack_ push:command]; }
When the undoCommand
method is invoked, it first tries to retrieve a reference to the last command
from the top of undoStack_
, and then sends it to an undo message to undraw the Mark
object that it preserved, as discussed in Listing 20-7. Then it will instantiate redoStack_
, if it's not done so, and push the Command
object that was popped from undoStack_
half a second ago (it shouldn't take that long, by the way).
- (void) redoCommand { Command *command = [redoStack_ pop]; [command execute]; // push the command back to the undo stack [undoStack_ push:command]; }
The redoCommand
is even simpler than undoCommand
. The first half is almost the same as undoCommand
except that the last Command
object is now popped from redoStack_
instead, and command
invokes execute
rather than undo
. After that, it pushes command
back to undoStack_
. So when the undoCommand
method is invoked again, the process will be repeated.
So how can we connect DrawScribbleCommand
objects with the actual drawing events? We are going explain with a bunch of touch event handlers defined in CanvasViewController
in Listing 20-9. The mechanism of drawing different types of Mark
based on the touch events are discussed in Chapter 12 as well as the previous "Modifying Touch Event Handlers for Invocations" section. So we will only highlight changes specific to our home-brewed infrastructure. Like in the last part, we will break the listing up into two chunks to discuss.
Example 20-9. Touch Event Handlers in CanvasViewController
That Manipulate DrawScribbleCommand
Objects
#pragma mark - #pragma mark Touch Event Handlers - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
startPoint_ = [[touches anyObject] locationInView:canvasView_]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { CGPoint lastPoint = [[touches anyObject] previousLocationInView:canvasView_]; // add a new stroke to scribble // if this is indeed a drag from // a finger if (CGPointEqualToPoint(lastPoint, startPoint_)) { id <Mark> newStroke = [[[Stroke alloc] init] autorelease]; [newStroke setColor:strokeColor_]; [newStroke setSize:strokeSize_]; [scribble_ addMark:newStroke shouldAddToPreviousMark:NO];NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
scribble_, ScribbleObjectUserInfoKey,
newStroke, MarkObjectUserInfoKey,
[NSNumber numberWithBool:NO],
AddToPreviousMarkUserInfoKey, nil];
DrawScribbleCommand *command = [[[DrawScribbleCommand alloc] init] autorelease];
[command setUserInfo:userInfo];
[self executeCommand:command prepareForUndo:YES];
} // add the current touch as another vertex to the // temp stroke CGPoint thisPoint = [[touches anyObject] locationInView:canvasView_]; Vertex *vertex = [[[Vertex alloc] initWithLocation:thisPoint] autorelease]; [scribble_ addMark:vertex shouldAddToPreviousMark:YES]; }
Now instead of using addMark:shouldAddToPreviousMark:
to add a Stroke
object, we use a DrawScribbleCommand
object to handle that. It takes a dictionary of userInfo
with values associated withthree dictionary keys, ScribbleObjectUserInfoKey
, MarkObjectUserInfoKey
, and AddToPreviousMarkUserInfoKey
. These keys are defined in Scribble
as follows:
NSString *const ScribbleObjectUserInfoKey = @"ScribbleObjectUserInfoKey"; NSString *const MarkObjectUserInfoKey = @"MarkObjectUserInfoKey"; NSString *const AddToPreviousMarkUserInfoKey = @"AddToPreviousMarkUserInfoKey";
We add scribble_
, newStroke
, and a BOOL
value of NO
as an instance of NSNumber
to the dictionary with the associated keys. Then we create an instance of the DrawScribbleCommand
object and initialize it with the userInfo
dictionary that we've just created. Instead of executing it right away, we pass it to a CanvasViewController
's instance method, executeCommand:command prepareForUndo:
, to execute and prepare for undo as described in Listing 20-8. After that, the DrawScribbleCommand
object is pushed to our previously defined undo stack (i.e., undoStack_
), and it's ready to be undone.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { CGPoint lastPoint = [[touches anyObject] previousLocationInView:canvasView_]; CGPoint thisPoint = [[touches anyObject] locationInView:canvasView_]; // if the touch never moves (stays at the same spot until lifted now) // just add a dot to an existing stroke composite // otherwise add it to the temp stroke as the last vertex if (CGPointEqualToPoint(lastPoint, thisPoint)) { Dot *singleDot = [[[Dot alloc] initWithLocation:thisPoint] autorelease]; [singleDot setColor:strokeColor_]; [singleDot setSize:strokeSize_]; [scribble_ addMark:singleDot shouldAddToPreviousMark:NO];NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
scribble_, ScribbleObjectUserInfoKey,
singleDot, MarkObjectUserInfoKey,
[NSNumber numberWithBool:NO],
AddToPreviousMarkUserInfoKey, nil];
DrawScribbleCommand *command = [[[DrawScribbleCommand alloc] init] autorelease];
[command setUserInfo:userInfo];
[self executeCommand:command prepareForUndo:YES];
}
The touchesEnd:
method determines if the touch ends at the same spot where it was first landed on the screen. If so, it will create a new Dot
object and attach it to the main Mark
parent in scribble_
. We construct a similar userInfo
with the same types of elements associated with the user keys for a new DrawScribbleCommand
object. Then we pass it to the executeCommand:
again like drawing a stroke with vertices.
// reset the start point here startPoint_ = CGPointZero; // if this is the last point of stroke // don't bother to draw it as the user // won't tell the difference } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { // reset the start point here startPoint_ = CGPointZero; }
The rest of the code is basically intact. Now you would ask, "Have we forgotten to push the operation for adding vertices into the undo stack in the touchesMoved:
method?" We haven't, but we left that out for a reason. We've designed our undo infrastructure around undoing/redoing only a complete stroke or dot, not vertices. If it undoes every "step" when the user creates a stroke, the user experience as well as performance may suffer. So we keep the original addMark:
message for scribble_
there.
Up to this point, we seem to have everything in place for our undo infrastructure to roll out. But how does the user actuate an undo/redo process in TouchPainter? On the main canvas view of TouchPainter, there are undo and redo buttons on the right-hand side of the toolbar, as shown in Figure 20-6.
Each of the buttons is tagged. When the user taps either one of them to actuate the process, we can capture and identify it in the onBarButtonHit:
method, as shown in Listing 20-10.
Example 20-10. An IBAction
Method for an Undo/Redo Button Hit in CanvasViewController
#pragma mark - #pragma mark Toolbar button hit method - (IBAction) onBarButtonHit:(id)button { UIBarButtonItem *barButton = button; if ([barButton tag] == 4) { [self undoCommand]; } else if ([barButton tag] == 5) { [self redoCommand]; } }
The code itself is self-explanatory based on what tag the button has to determine, whether it is for undo or redo.
For the version of NSUndoManager
, the undo statement is changed to the following:
[self.undoManager undo];
as for redo:
[self.undoManager redo];
We have concluded the examples on implementing undo/redo operations in our TouchPainter. The Command pattern is commonly found in many object-oriented software designs.
The Command pattern allows executable instructions encapsulated in command objects. It makes the pattern a natural choice for implementing an undo/redo infrastructure. But it doesn't stop right there. Another well-known use of command objects is for delaying executions in an invoker. Invokers can be a menu item or a button. It's quite common to use command objects to link up operations crossing between different objects—for example, hitting a button in a view controller can execute a command object to perform some operations on another view controller. The command object hides any details related to the operations.
From the sample project of this chapter, you can find a few more Command
classes used in some other areas in the TouchPainter app for different purposes. The source project files contain a lot more information than we can cover in here, so free feel to check them out and explore what you can find in them!
This chapter has introduced the Command pattern and how we can implement it in Objective-C. We have designed and implemented an undo infrastructure for the TouchPainter app so the user can undo/redo any strokes and dots drawn on the screen. We have also illustrated how the Cocoa Touch framework has adapted the pattern with different invocation and undo/redo strategies that can be applied in any iOS applications.
The benefits of decoupling between command, invoker, receiver, and client in an application are apparent. If a particular command needs to be changed in the implementation, then most of the other components in the architecture remain intact. It is very easy to add new command classes because we don't need to modify the existing classes in order to do so.
This is the end of this part about encapsulating algorithms. In the next part, we will discuss some design patterns that are related to performance and object access.
[18] The original definition appeared in Design Patterns, by the "Gang of Four" (Addison-Wesley, 1994).
3.144.37.12