summaryrefslogtreecommitdiffstats
path: root/ios/chrome/browser/memory/memory_debugger.mm
blob: 13c04a0e8cf0036eaf35603099f1f1f099fd343d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/memory/memory_debugger.h"

#include <stdint.h>

#include "base/ios/ios_util.h"
#import "base/mac/scoped_nsobject.h"
#import "base/memory/scoped_ptr.h"
#import "ios/chrome/browser/memory/memory_metrics.h"
#include "ios/chrome/browser/ui/ui_util.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"

namespace {
// The number of bytes in a megabyte.
const CGFloat kNumBytesInMB = 1024 * 1024;
// The horizontal and vertical padding between subviews.
const CGFloat kPadding = 10;
}  // namespace

@implementation MemoryDebugger {
  // A timer to trigger refreshes.
  base::scoped_nsobject<NSTimer> _refreshTimer;

  // A timer to trigger continuous memory warnings.
  base::scoped_nsobject<NSTimer> _memoryWarningTimer;

  // The font to use.
  base::scoped_nsobject<UIFont> _font;

  // Labels for memory metrics.
  base::scoped_nsobject<UILabel> _physicalFreeMemoryLabel;
  base::scoped_nsobject<UILabel> _realMemoryUsedLabel;
  base::scoped_nsobject<UILabel> _xcodeGaugeLabel;
  base::scoped_nsobject<UILabel> _dirtyVirtualMemoryLabel;

  // Inputs for memory commands.
  base::scoped_nsobject<UITextField> _bloatField;
  base::scoped_nsobject<UITextField> _refreshField;
  base::scoped_nsobject<UITextField> _continuousMemoryWarningField;

  // A place to store the artifical memory bloat.
  scoped_ptr<uint8_t> _bloat;

  // Distance the view was pushed up to accomodate the keyboard.
  CGFloat _keyboardOffset;

  // The current orientation of the device.
  BOOL _currentOrientation;
}

- (instancetype)init {
  self = [super initWithFrame:CGRectZero];
  if (self) {
    _font.reset([[UIFont systemFontOfSize:14] retain]);
    self.backgroundColor = [UIColor colorWithWhite:0.8f alpha:0.9f];
    self.opaque = NO;

    [self addSubviews];
    [self adjustForOrientation:nil];
    [self sizeToFit];
    [self registerForNotifications];
  }
  return self;
}

// NSTimers create a retain cycle so they must be invalidated before this
// instance can be deallocated.
- (void)invalidateTimers {
  [_refreshTimer invalidate];
  [_memoryWarningTimer invalidate];
}

- (void)dealloc {
  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [super dealloc];
}

#pragma mark UIView methods

- (CGSize)sizeThatFits:(CGSize)size {
  CGFloat width = 0;
  CGFloat height = 0;
  for (UIView* subview in self.subviews) {
    width = MAX(width, CGRectGetMaxX(subview.frame));
    height = MAX(height, CGRectGetMaxY(subview.frame));
  }
  return CGSizeMake(width + kPadding, height + kPadding);
}

#pragma mark initialization helpers

- (void)addSubviews {
  // |index| is used to calculate the vertical position of each element in
  // the debugger view.
  NSUInteger index = 0;

  // Display some metrics.
  _physicalFreeMemoryLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]);
  [self addMetricWithName:@"Physical Free"
                  atIndex:index++
               usingLabel:_physicalFreeMemoryLabel];
  _realMemoryUsedLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]);
  [self addMetricWithName:@"Real Memory Used"
                  atIndex:index++
               usingLabel:_realMemoryUsedLabel];
  _xcodeGaugeLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]);
  [self addMetricWithName:@"Xcode Gauge"
                  atIndex:index++
               usingLabel:_xcodeGaugeLabel];
  _dirtyVirtualMemoryLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]);
  [self addMetricWithName:@"Dirty VM"
                  atIndex:index++
               usingLabel:_dirtyVirtualMemoryLabel];

// Since _performMemoryWarning is a private API it can't be compiled into
// official builds.
// TODO(lliabraa): Figure out how to support memory warnings (or something
// like them) in official builds.
#if CHROMIUM_BUILD
  [self addButtonWithTitle:@"Trigger Memory Warning"
                    target:[UIApplication sharedApplication]
                    action:@selector(_performMemoryWarning)
                withOrigin:[self originForSubviewAtIndex:index++]];
#endif  // CHROMIUM_BUILD

  // Display a text input to set the amount of artificial memory bloat and a
  // button to reset the bloat to zero.
  _bloatField.reset([[UITextField alloc] initWithFrame:CGRectZero]);
  [self addLabelWithText:@"Set bloat (MB)"
                   input:_bloatField
             inputTarget:self
             inputAction:@selector(updateBloat)
         buttonWithTitle:@"Clear"
            buttonTarget:self
            buttonAction:@selector(clearBloat)
                 atIndex:index++];
  [_bloatField setText:@"0"];
  [self updateBloat];

// Since _performMemoryWarning is a private API it can't be compiled into
// official builds.
// TODO(lliabraa): Figure out how to support memory warnings (or something
// like them) in official builds.
#if CHROMIUM_BUILD
  // Display a text input to control the rate of continuous memory warnings.
  _continuousMemoryWarningField.reset(
      [[UITextField alloc] initWithFrame:CGRectZero]);
  [self addLabelWithText:@"Set memory warning interval (secs)"
                   input:_continuousMemoryWarningField
             inputTarget:self
             inputAction:@selector(updateMemoryWarningInterval)
                 atIndex:index++];
  [_continuousMemoryWarningField setText:@"0.0"];
#endif  // CHROMIUM_BUILD

  // Display a text input to control the refresh rate of the memory debugger.
  _refreshField.reset([[UITextField alloc] initWithFrame:CGRectZero]);
  [self addLabelWithText:@"Set refresh interval (secs)"
                   input:_refreshField
             inputTarget:self
             inputAction:@selector(updateRefreshInterval)
                 atIndex:index++];
  [_refreshField setText:@"0.5"];
  [self updateRefreshInterval];
}

- (void)registerForNotifications {
  // On iOS 7, the screen coordinate system is not dependent on orientation so
  // the debugger has to handle its own rotation.
  if (!base::ios::IsRunningOnIOS8OrLater()) {
    // Register to receive orientation notifications.
    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(adjustForOrientation:)
               name:UIDeviceOrientationDidChangeNotification
             object:nil];
  }

  // Register to receive memory warning.
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(lowMemoryWarningReceived:)
             name:UIApplicationDidReceiveMemoryWarningNotification
           object:nil];

  // Register to receive keyboard will show notification.
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(keyboardWillShow:)
             name:UIKeyboardWillShowNotification
           object:nil];

  // Register to receive keyboard will hide notification.
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(keyboardWillHide:)
             name:UIKeyboardWillHideNotification
           object:nil];
}

// Adds subviews for the specified metric, the value of which will be displayed
// in |label|.
- (void)addMetricWithName:(NSString*)name
                  atIndex:(NSUInteger)index
               usingLabel:(UILabel*)label {
  // The width of the view for the metric's name.
  const CGFloat kNameWidth = 150;
  // The width of the view for each metric.
  const CGFloat kMetricWidth = 100;
  CGPoint nameOrigin = [self originForSubviewAtIndex:index];
  CGRect nameFrame =
      CGRectMake(nameOrigin.x, nameOrigin.y, kNameWidth, [_font lineHeight]);
  base::scoped_nsobject<UILabel> nameLabel(
      [[UILabel alloc] initWithFrame:nameFrame]);
  [nameLabel setText:[NSString stringWithFormat:@"%@: ", name]];
  [nameLabel setFont:_font];
  [self addSubview:nameLabel];
  label.frame = CGRectMake(CGRectGetMaxX(nameFrame), nameFrame.origin.y,
                           kMetricWidth, [_font lineHeight]);
  [label setFont:_font];
  [label setTextAlignment:NSTextAlignmentRight];
  [self addSubview:label];
}

// Adds a subview for a button with the given title and target/action.
- (void)addButtonWithTitle:(NSString*)title
                    target:(id)target
                    action:(SEL)action
                withOrigin:(CGPoint)origin {
  base::scoped_nsobject<UIButton> button(
      [[UIButton buttonWithType:UIButtonTypeSystem] retain]);
  [button setTitle:title forState:UIControlStateNormal];
  [button titleLabel].font = _font;
  [[button titleLabel] setTextAlignment:NSTextAlignmentCenter];
  [button sizeToFit];
  [button setFrame:CGRectMake(origin.x, origin.y, [button frame].size.width,
                              [_font lineHeight])];
  [button addTarget:target
                action:action
      forControlEvents:UIControlEventTouchUpInside];
  [self addSubview:button];
}

// Adds subviews for a UI component with label and input text field.
//
// -------------------------
// | labelText  | <input>  |
// -------------------------
//
// The inputTarget/inputAction will be invoked when the user finishes editing
// in |input|.
- (void)addLabelWithText:(NSString*)labelText
                   input:(UITextField*)input
             inputTarget:(id)inputTarget
             inputAction:(SEL)inputAction
                 atIndex:(NSUInteger)index {
  [self addLabelWithText:labelText
                   input:input
             inputTarget:inputTarget
             inputAction:inputAction
         buttonWithTitle:nil
            buttonTarget:nil
            buttonAction:nil
                 atIndex:index];
}

// Adds subviews for a UI component with label, input text field and button.
//
// -------------------------------------
// | labelText  | <input>  |  <button> |
// -------------------------------------
//
// The inputTarget/inputAction will be invoked when the user finishes editing
// in |input|.
- (void)addLabelWithText:(NSString*)labelText
                   input:(UITextField*)input
             inputTarget:(id)inputTarget
             inputAction:(SEL)inputAction
         buttonWithTitle:(NSString*)buttonTitle
            buttonTarget:(id)buttonTarget
            buttonAction:(SEL)buttonAction
                 atIndex:(NSUInteger)index {
  base::scoped_nsobject<UILabel> label(
      [[UILabel alloc] initWithFrame:CGRectZero]);
  if (labelText) {
    [label setText:[NSString stringWithFormat:@"%@: ", labelText]];
  }
  [label setFont:_font];
  [label sizeToFit];
  CGPoint labelOrigin = [self originForSubviewAtIndex:index];
  [label setFrame:CGRectOffset([label frame], labelOrigin.x, labelOrigin.y)];
  [self addSubview:label];
  if (input) {
    // The width of the views for each input text field.
    const CGFloat kInputWidth = 50;
    input.frame =
        CGRectMake(CGRectGetMaxX([label frame]) + kPadding,
                   [label frame].origin.y, kInputWidth, [_font lineHeight]);
    input.font = _font;
    input.backgroundColor = [UIColor whiteColor];
    input.delegate = self;
    input.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
    input.adjustsFontSizeToFitWidth = YES;
    input.textAlignment = NSTextAlignmentRight;
    [input addTarget:inputTarget
                  action:inputAction
        forControlEvents:UIControlEventEditingDidEnd];

    [self addSubview:input];
  }

  if (buttonTitle) {
    const CGFloat kButtonXOffset =
        input ? CGRectGetMaxX(input.frame) : CGRectGetMaxX([label frame]);
    CGPoint origin =
        CGPointMake(kButtonXOffset + kPadding, [label frame].origin.y);
    [self addButtonWithTitle:buttonTitle
                      target:buttonTarget
                      action:buttonAction
                  withOrigin:origin];
  }
}

// Returns the CGPoint of the origin of the subview at |index|.
- (CGPoint)originForSubviewAtIndex:(NSUInteger)index {
  return CGPointMake(kPadding,
                     (index + 1) * kPadding + index * [_font lineHeight]);
}

#pragma mark Refresh callback

// Updates content and ensures the view is visible.
- (void)refresh:(NSTimer*)timer {
  [self.superview bringSubviewToFront:self];
  [self updateMemoryInfo];
}

#pragma mark Memory inspection

// Updates the memory metrics shown.
- (void)updateMemoryInfo {
  CGFloat value = memory_util::GetFreePhysicalBytes() / kNumBytesInMB;
  [_physicalFreeMemoryLabel
      setText:[NSString stringWithFormat:@"%.2f MB", value]];
  value = memory_util::GetRealMemoryUsedInBytes() / kNumBytesInMB;
  [_realMemoryUsedLabel setText:[NSString stringWithFormat:@"%.2f MB", value]];
  value = memory_util::GetInternalVMBytes() / kNumBytesInMB;
  [_xcodeGaugeLabel setText:[NSString stringWithFormat:@"%.2f MB", value]];
  value = memory_util::GetDirtyVMBytes() / kNumBytesInMB;
  [_dirtyVirtualMemoryLabel
      setText:[NSString stringWithFormat:@"%.2f MB", value]];
}

#pragma mark Memory Warning notification callback

// Flashes the debugger to indicate memory warning.
- (void)lowMemoryWarningReceived:(NSNotification*)notification {
  UIColor* originalColor = self.backgroundColor;
  self.backgroundColor =
      [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.9];
  [UIView animateWithDuration:1.0
                        delay:0.0
                      options:UIViewAnimationOptionAllowUserInteraction
                   animations:^{
                     self.backgroundColor = originalColor;
                   }
                   completion:nil];
}

#pragma mark Rotation notification callback

- (void)didMoveToSuperview {
  UIView* superview = [self superview];
  if (superview)
    [self setCenter:[superview center]];
}

- (void)adjustForOrientation:(NSNotification*)notification {
  if (base::ios::IsRunningOnIOS8OrLater()) {
    return;
  }
  UIInterfaceOrientation orientation =
      [[UIApplication sharedApplication] statusBarOrientation];
  if (orientation == _currentOrientation) {
    return;
  }
  _currentOrientation = orientation;
  CGFloat angle;
  switch (orientation) {
    case UIInterfaceOrientationPortrait:
      angle = 0;
      break;
    case UIInterfaceOrientationPortraitUpsideDown:
      angle = M_PI;
      break;
    case UIInterfaceOrientationLandscapeLeft:
      angle = -M_PI_2;
      break;
    case UIInterfaceOrientationLandscapeRight:
      angle = M_PI_2;
      break;
    case UIInterfaceOrientationUnknown:
    default:
      angle = 0;
  }

  // Since the debugger view is in screen coordinates and handles its own
  // rotation via the |transform| property, the view's position after rotation
  // can be unexpected and partially off-screen. Centering the view before
  // rotating it ensures that the view remains within the bounds of the screen.
  if (self.superview) {
    self.center = self.superview.center;
  }
  self.transform = CGAffineTransformMakeRotation(angle);
}

#pragma mark Keyboard notification callbacks

// Ensures the debugger is visible by shifting it up as the keyboard animates
// in.
- (void)keyboardWillShow:(NSNotification*)notification {
  NSDictionary* userInfo = [notification userInfo];
  NSValue* keyboardFrameValue =
      [userInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
  CGFloat keyboardHeight = CurrentKeyboardHeight(keyboardFrameValue);

  // Get the coord of the bottom of the debugger's frame. This is orientation
  // dependent on iOS 7 because the debugger is in screen coords.
  CGFloat bottomOfFrame = CGRectGetMaxY(self.frame);
  if (!base::ios::IsRunningOnIOS8OrLater() && IsLandscape())
    bottomOfFrame = CGRectGetMaxX(self.frame);

  // Shift the debugger up by the "height" of the keyboard, but since the
  // keyboard rect is in screen coords, use the orientation to find the height.
  CGFloat distanceFromBottom = CurrentScreenHeight() - bottomOfFrame;
  _keyboardOffset = -1 * fmax(0.0f, keyboardHeight - distanceFromBottom);
  [self animateForKeyboardNotification:notification
                            withOffset:CGPointMake(0, _keyboardOffset)];
}

// Shifts the debugger back down when the keyboard is hidden.
- (void)keyboardWillHide:(NSNotification*)notification {
  [self animateForKeyboardNotification:notification
                            withOffset:CGPointMake(0, -_keyboardOffset)];
}

- (void)animateForKeyboardNotification:(NSNotification*)notification
                            withOffset:(CGPoint)offset {
  // Account for orientation.
  offset = CGPointApplyAffineTransform(offset, self.transform);
  // Normally this would use an animation block, but there is no API to
  // convert the UIKeyboardAnimationCurveUserInfoKey's value from a
  // UIViewAnimationCurve to a UIViewAnimationOption. Awesome!
  NSDictionary* userInfo = [notification userInfo];
  [UIView beginAnimations:nil context:nullptr];
  [UIView setAnimationDuration:
              [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
  NSInteger animationCurveKeyValue =
      [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
  UIViewAnimationCurve animationCurve =
      (UIViewAnimationCurve)animationCurveKeyValue;
  [UIView setAnimationCurve:animationCurve];
  [UIView setAnimationBeginsFromCurrentState:YES];
  self.frame = CGRectOffset(self.frame, offset.x, offset.y);
  [UIView commitAnimations];
}

#pragma mark Artificial memory bloat methods

- (void)updateBloat {
  double bloatSizeMB;
  NSScanner* scanner = [NSScanner scannerWithString:[_bloatField text]];
  if (![scanner scanDouble:&bloatSizeMB] || bloatSizeMB < 0.0) {
    bloatSizeMB = 0;
    NSString* errorMessage =
        [NSString stringWithFormat:@"Invalid value \"%@\" for bloat size.\n"
                                   @"Must be a positive number.\n"
                                   @"Resetting to %.1f MB",
                                   [_bloatField text], bloatSizeMB];
    [self alert:errorMessage];
    [_bloatField setText:[NSString stringWithFormat:@"%.1f", bloatSizeMB]];
  }
  const CGFloat kBloatSizeBytes = ceil(bloatSizeMB * kNumBytesInMB);
  const uint64_t kNumberOfBytes = static_cast<uint64_t>(kBloatSizeBytes);
  _bloat.reset(kNumberOfBytes ? new uint8_t[kNumberOfBytes] : nullptr);
  if (_bloat) {
    memset(_bloat.get(), -1, kNumberOfBytes);  // Occupy memory.
  } else {
    if (kNumberOfBytes) {
      [self alert:@"Could not allocate memory."];
    }
  }
}

- (void)clearBloat {
  [_bloatField setText:@"0"];
  [_bloatField resignFirstResponder];
  [self updateBloat];
}

#pragma mark Refresh interval methods

- (void)updateRefreshInterval {
  double refreshTimerValue;
  NSScanner* scanner = [NSScanner scannerWithString:[_refreshField text]];
  if (![scanner scanDouble:&refreshTimerValue] || refreshTimerValue < 0.0) {
    refreshTimerValue = 0.5;
    NSString* errorMessage = [NSString
        stringWithFormat:@"Invalid value \"%@\" for refresh interval.\n"
                         @"Must be a positive number.\n" @"Resetting to %.1f",
                         [_refreshField text], refreshTimerValue];
    [self alert:errorMessage];
    [_refreshField
        setText:[NSString stringWithFormat:@"%.1f", refreshTimerValue]];
    return;
  }
  [_refreshTimer invalidate];
  _refreshTimer.reset(
      [[NSTimer scheduledTimerWithTimeInterval:refreshTimerValue
                                        target:self
                                      selector:@selector(refresh:)
                                      userInfo:nil
                                       repeats:YES] retain]);
}

#pragma mark Memory warning interval methods

// Since _performMemoryWarning is a private API it can't be compiled into
// official builds.
// TODO(lliabraa): Figure out how to support memory warnings (or something
// like them) in official builds.
#if CHROMIUM_BUILD
- (void)updateMemoryWarningInterval {
  [_memoryWarningTimer invalidate];
  double timerValue;
  NSString* text = [_continuousMemoryWarningField text];
  NSScanner* scanner = [NSScanner scannerWithString:text];
  BOOL valueFound = [scanner scanDouble:&timerValue];
  // If the text field is empty or contains 0, return early to turn off
  // continuous memory warnings.
  if (![text length] || timerValue == 0.0) {
    return;
  }
  // If no value could be parsed or a non-positive value was found, throw up an
  // error message and return early to turn off continuous memory warnings.
  if (!valueFound || timerValue <= 0.0) {
    NSString* errorMessage = [NSString
        stringWithFormat:@"Invalid value \"%@\" for memory warning interval.\n"
                         @"Must be a positive number.\n"
                         @"Turning off continuous memory warnings",
                         text];
    [self alert:errorMessage];
    [_continuousMemoryWarningField setText:@""];
    return;
  }
  // If a valid value was found have the timer start triggering continuous
  // memory warnings.
  _memoryWarningTimer.reset(
      [[NSTimer scheduledTimerWithTimeInterval:timerValue
                                        target:[UIApplication sharedApplication]
                                      selector:@selector(_performMemoryWarning)
                                      userInfo:nil
                                       repeats:YES] retain]);
}
#endif  // CHROMIUM_BUILD

#pragma mark UITextViewDelegate methods

// Dismisses the keyboard if the user hits return.
- (BOOL)textFieldShouldReturn:(UITextField*)textField {
  [textField resignFirstResponder];
  return YES;
}

#pragma mark UIResponder methods

// Allows the debugger to be dragged around the screen.
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
  UITouch* touch = [touches anyObject];
  CGPoint start = [touch previousLocationInView:self];
  CGPoint end = [touch locationInView:self];
  CGPoint offset = CGPointMake(end.x - start.x, end.y - start.y);
  offset = CGPointApplyAffineTransform(offset, self.transform);
  self.frame = CGRectOffset(self.frame, offset.x, offset.y);
}

#pragma mark Error handling

// Shows an alert with the given |errorMessage|.
- (void)alert:(NSString*)errorMessage {
  UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Error"
                                                  message:errorMessage
                                                 delegate:self
                                        cancelButtonTitle:@"OK"
                                        otherButtonTitles:nil, nil];
  [alert show];
}

@end