From http://www.jwz.org/xscreensaver/xscreensaver-5.32.tar.gz
[xscreensaver] / OSX / SaverRunner.m
index 797cd2cec5b289ee7cd9d86b5181da5076127cb4..2a105056ba464b71722b5186b0448d3c1727ddc2 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006-2011 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-2014 Jamie Zawinski <jwz@jwz.org>
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -9,7 +9,7 @@
  * implied warranty.
  */
 
-/* This program serves two purposes:
+/* This program serves three purposes:
 
    First, It is a test harness for screen savers.  When it launches, it
    looks around for .saver bundles (in the current directory, and then in
 
    Second, it can be used to transform any screen saver into a standalone
    program.  Just put one (and only one) .saver bundle into the app
-   bundle's Contents/PlugIns/ directory, and it will load and run that
+   bundle's Contents/Resources/ directory, and it will load and run that
    saver at start-up (without the saver-selection menu or other chrome).
    This is how the "Phosphor.app" and "Apple2.app" programs work.
+
+   Third, it is the scaffolding which turns a set of screen savers into
+   a single iPhone / iPad program.  In that case, all of the savers are
+   linked in to this executable, since iOS does not allow dynamic loading
+   of bundles that have executable code in them.  Bleh.
  */
 
+#import <TargetConditionals.h>
 #import "SaverRunner.h"
+#import "SaverListController.h"
 #import "XScreenSaverGLView.h"
+#import "yarandom.h"
+
+#ifdef USE_IPHONE
+
+@interface RotateyViewController : UINavigationController
+{
+  BOOL allowRotation;
+}
+@end
+
+@implementation RotateyViewController
+
+/* This subclass exists so that we can ask that the SaverListController and
+   preferences panels be auto-rotated by the system.  Note that the 
+   XScreenSaverView is not auto-rotated because it is on a different UIWindow.
+ */
+
+- (id)initWithRotation:(BOOL)rotatep
+{
+  self = [super init];
+  allowRotation = rotatep;
+  return self;
+}
+
+- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o
+{
+  return allowRotation;                                /* Deprecated in iOS 6 */
+}
+
+- (BOOL)shouldAutorotate                       /* Added in iOS 6 */
+{
+  return allowRotation;
+}
+
+- (NSUInteger)supportedInterfaceOrientations   /* Added in iOS 6 */
+{
+  return UIInterfaceOrientationMaskAll;
+}
+
+@end
+
+
+/* This subclass exists to ensure that all events on the saverWindow actually
+   go to the saverView.  For some reason, the rootViewController's
+   UILayoutContainerView was capturing all of our events (touches and shakes).
+ */
+
+@interface EventCapturingWindow : UIWindow
+@property(assign) UIView *eventView;
+@end
+
+@implementation EventCapturingWindow
+@synthesize eventView;
+
+/* Always deliver touch events to the eventView if we have one.
+ */
+- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
+{
+  if (eventView)
+    return eventView;
+  else
+    return [super hitTest:point withEvent:event];
+}
+
+/* Always deliver motion events to the eventView if we have one.
+ */
+- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+  if (eventView)
+    [eventView motionBegan:motion withEvent:event];
+  else
+    [super motionBegan:motion withEvent:event];
+}
+
+- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+  if (eventView)
+    [eventView motionEnded:motion withEvent:event];
+  else
+    [super motionEnded:motion withEvent:event];
+}
+
+- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
+{
+  if (eventView)
+    [eventView motionCancelled:motion withEvent:event];
+  else
+    [super motionCancelled:motion withEvent:event];
+}
+
+@end
+
+
+#endif // USE_IPHONE
+
 
 @implementation SaverRunner
 
+
 - (ScreenSaverView *) makeSaverView: (NSString *) module
+                           withSize: (NSSize) size
 {
+  Class new_class = 0;
+
+# ifndef USE_IPHONE
+
+  // Load the XScreenSaverView subclass and code from a ".saver" bundle.
+
   NSString *name = [module stringByAppendingPathExtension:@"saver"];
   NSString *path = [saverDir stringByAppendingPathComponent:name];
+
+  if (! [[NSFileManager defaultManager] fileExistsAtPath:path]) {
+    NSLog(@"bundle \"%@\" does not exist", path);
+    return 0;
+  }
+
+  NSLog(@"Loading %@", path);
+
+  // NSBundle *obundle = saverBundle;
+
   saverBundle = [NSBundle bundleWithPath:path];
-  Class new_class = [saverBundle principalClass];
-  NSAssert1 (new_class, @"unable to load \"%@\"", path);
+  if (saverBundle)
+    new_class = [saverBundle principalClass];
 
+  // Not entirely unsurprisingly, this tends to break the world.
+  // if (obundle && obundle != saverBundle)
+  //  [obundle unload];
+
+# else  // USE_IPHONE
+
+  // Determine whether to create an X11 view or an OpenGL view by
+  // looking for the "gl" tag in the xml file.  This is kind of awful.
+
+  NSString *path = [saverDir
+                     stringByAppendingPathComponent:
+                       [[[module lowercaseString]
+                          stringByReplacingOccurrencesOfString:@" "
+                          withString:@""]
+                         stringByAppendingPathExtension:@"xml"]];
+  NSData *xmld = [NSData dataWithContentsOfFile:path];
+  NSAssert (xmld, @"no XML: %@", path);
+  NSString *xml = [XScreenSaverView decompressXML:xmld];
+  Bool gl_p = (xml && [xml rangeOfString:@"gl=\"yes\""].length > 0);
+
+  new_class = (gl_p
+               ? [XScreenSaverGLView class]
+               : [XScreenSaverView class]);
+
+# endif // USE_IPHONE
+
+  if (! new_class)
+    return 0;
 
   NSRect rect;
   rect.origin.x = rect.origin.y = 0;
-  rect.size.width = 320;
-  rect.size.height = 240;
+  rect.size.width  = size.width;
+  rect.size.height = size.height;
 
-  id instance = [[new_class alloc] initWithFrame:rect isPreview:YES];
-  NSAssert1 (instance, @"unable to instantiate %@", new_class);
+  XScreenSaverView *instance =
+    [(XScreenSaverView *) [new_class alloc]
+                          initWithFrame:rect
+                          saverName:module
+                          isPreview:YES];
+  if (! instance) {
+    NSLog(@"Failed to instantiate %@ for \"%@\"", new_class, module);
+    return 0;
+  }
 
 
   /* KLUGE: Inform the underlying program that we're in "standalone"
-     mode.  This is kind of horrible but I haven't thought of a more
-     sensible way to make this work.
+     mode, e.g. running as "Phosphor.app" rather than "Phosphor.saver".
+     This is kind of horrible but I haven't thought of a more sensible
+     way to make this work.
    */
+# ifndef USE_IPHONE
   if ([saverNames count] == 1) {
     putenv (strdup ("XSCREENSAVER_STANDALONE=1"));
   }
+# endif
 
   return (ScreenSaverView *) instance;
 }
 
 
+#ifndef USE_IPHONE
+
 static ScreenSaverView *
 find_saverView_child (NSView *v)
 {
@@ -90,6 +250,9 @@ find_saverView (NSView *v)
 }
 
 
+/* Changes the contents of the menubar menus to correspond to
+   the running saver.  Desktop only.
+ */
 static void
 relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 {
@@ -116,7 +279,6 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 - (void) openPreferences: (id) sender
 {
   ScreenSaverView *sv;
-
   if ([sender isKindOfClass:[NSView class]]) { // Sent from button
     sv = find_saverView ((NSView *) sender);
   } else {
@@ -130,6 +292,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   }
 
   NSAssert (sv, @"no saver view");
+  if (!sv) return;
   NSWindow *prefs = [sv configureSheet];
 
   [NSApp beginSheet:prefs
@@ -144,11 +307,13 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
      ones will blow up if one re-inits but the other doesn't.
    */
   if (code != NSCancelButton) {
-    [sv stopAnimation];
+    if ([sv isAnimating])
+      [sv stopAnimation];
     [sv startAnimation];
   }
 }
 
+
 - (void) preferencesClosed: (NSWindow *) sheet
                 returnCode: (int) returnCode
                contextInfo: (void  *) contextInfo
@@ -156,40 +321,388 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   [NSApp stopModalWithCode:returnCode];
 }
 
+#else  // USE_IPHONE
 
-- (void)loadSaver:(NSString *)name
+
+- (UIImage *) screenshot
 {
-  int i;
-  for (i = 0; i < [windows count]; i++) {
-    NSWindow *window = [windows objectAtIndex:i];
-    NSView *cv = [window contentView];
-    ScreenSaverView *old_view = find_saverView (cv);
-    NSView *sup = [old_view superview];
+  return saved_screenshot;
+}
+
+- (void) saveScreenshot
+{
+  // Most of this is from:
+  // http://developer.apple.com/library/ios/#qa/qa1703/_index.html
+  // The rotation stuff is by me.
+
+  CGSize size = [[UIScreen mainScreen] bounds].size;
+
+  UIInterfaceOrientation orient =
+    [[window rootViewController] interfaceOrientation];
+  if (orient == UIInterfaceOrientationLandscapeLeft ||
+      orient == UIInterfaceOrientationLandscapeRight) {
+    // Rotate the shape of the canvas 90 degrees.
+    double s = size.width;
+    size.width = size.height;
+    size.height = s;
+  }
+
+
+  // Create a graphics context with the target size
+  // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to
+  // take the scale into consideration
+  // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
+
+  if (UIGraphicsBeginImageContextWithOptions)
+    UIGraphicsBeginImageContextWithOptions (size, NO, 0);
+  else
+    UIGraphicsBeginImageContext (size);
+
+  CGContextRef ctx = UIGraphicsGetCurrentContext();
+
+
+  // Rotate the graphics context to match current hardware rotation.
+  //
+  switch (orient) {
+  case UIInterfaceOrientationPortraitUpsideDown:
+    CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
+    CGContextRotateCTM (ctx, M_PI);
+    CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
+    break;
+  case UIInterfaceOrientationLandscapeLeft:
+  case UIInterfaceOrientationLandscapeRight:
+    CGContextTranslateCTM (ctx,  
+                           ([window frame].size.height -
+                            [window frame].size.width) / 2,
+                           ([window frame].size.width -
+                            [window frame].size.height) / 2);
+    CGContextTranslateCTM (ctx,  [window center].x,  [window center].y);
+    CGContextRotateCTM (ctx, 
+                        (orient == UIInterfaceOrientationLandscapeLeft
+                         ?  M_PI/2
+                         : -M_PI/2));
+    CGContextTranslateCTM (ctx, -[window center].x, -[window center].y);
+    break;
+  default:
+    break;
+  }
+
+  // Iterate over every window from back to front
+  //
+  for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
+    if (![win respondsToSelector:@selector(screen)] ||
+        [win screen] == [UIScreen mainScreen]) {
+
+      // -renderInContext: renders in the coordinate space of the layer,
+      // so we must first apply the layer's geometry to the graphics context
+      CGContextSaveGState (ctx);
+
+      // Center the context around the window's anchor point
+      CGContextTranslateCTM (ctx, [win center].x, [win center].y);
+
+      // Apply the window's transform about the anchor point
+      CGContextConcatCTM (ctx, [win transform]);
+
+      // Offset by the portion of the bounds left of and above anchor point
+      CGContextTranslateCTM (ctx,
+        -[win bounds].size.width  * [[win layer] anchorPoint].x,
+        -[win bounds].size.height * [[win layer] anchorPoint].y);
+
+      // Render the layer hierarchy to the current context
+      [[win layer] renderInContext:ctx];
+
+      // Restore the context
+      CGContextRestoreGState (ctx);
+    }
+  }
+
+  if (saved_screenshot)
+    [saved_screenshot release];
+  saved_screenshot = [UIGraphicsGetImageFromCurrentImageContext() retain];
+
+  UIGraphicsEndImageContext();
+}
 
-    NSString *old_title = [window title];
+
+- (void) openPreferences: (NSString *) saver
+{
+  [self loadSaver:saver launch:NO];
+  if (! saverView) return;
+
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs setObject:saver forKey:@"selectedSaverName"];
+  [prefs synchronize];
+
+  [rotating_nav pushViewController: [saverView configureView]
+                      animated:YES];
+}
+
+
+#endif // USE_IPHONE
+
+
+
+- (void)loadSaver:(NSString *)name launch:(BOOL)launch
+{
+# ifndef USE_IPHONE
+
+  if (saverName && [saverName isEqualToString: name]) {
+    if (launch)
+      for (NSWindow *win in windows) {
+        ScreenSaverView *sv = find_saverView ([win contentView]);
+        if (![sv isAnimating])
+          [sv startAnimation];
+      }
+    return;
+  }
+
+  saverName = name;
+
+  for (NSWindow *win in windows) {
+    NSView *cv = [win contentView];
+    NSString *old_title = [win title];
     if (!old_title) old_title = @"XScreenSaver";
-    [window setTitle: name];
+    [win setTitle: name];
     relabel_menus (menubar, old_title, name);
 
-    [old_view stopAnimation];
-    [old_view removeFromSuperview];
+    ScreenSaverView *old_view = find_saverView (cv);
+    NSView *sup = old_view ? [old_view superview] : cv;
+
+    if (old_view) {
+      if ([old_view isAnimating])
+        [old_view stopAnimation];
+      [old_view removeFromSuperview];
+    }
+
+    NSSize size = [cv frame].size;
+    ScreenSaverView *new_view = [self makeSaverView:name withSize: size];
+    NSAssert (new_view, @"unable to make a saver view");
 
-    ScreenSaverView *new_view = [self makeSaverView:name];
-    [new_view setFrame: [old_view frame]];
+    [new_view setFrame: (old_view ? [old_view frame] : [cv frame])];
     [sup addSubview: new_view];
-    [window makeFirstResponder:new_view];
+    [win makeFirstResponder:new_view];
     [new_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
-    [new_view startAnimation];
+    [new_view retain];
+    if (launch)
+      [new_view startAnimation];
   }
 
   NSUserDefaultsController *ctl =
     [NSUserDefaultsController sharedUserDefaultsController];
   [ctl save:self];
+
+# else  // USE_IPHONE
+
+#  if TARGET_IPHONE_SIMULATOR
+  NSLog (@"selecting saver \"%@\"", name);
+#  endif
+
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+  [prefs setObject:name forKey:@"selectedSaverName"];
+  [prefs synchronize];
+
+/* Cacheing this screws up rotation when starting a saver twice in a row.
+  if (saverName && [saverName isEqualToString: name]) {
+    if ([saverView isAnimating])
+      return;
+    else
+      goto LAUNCH;
+  }
+*/
+
+  saverName = name;
+
+  if (saverView) {
+    if ([saverView isAnimating])
+      [saverView stopAnimation];
+    [saverView removeFromSuperview];
+    [backgroundView removeFromSuperview];
+    [[NSNotificationCenter defaultCenter] removeObserver:saverView];
+    [saverView release];
+  }
+
+  UIScreen *screen = [UIScreen mainScreen];
+  NSSize size;
+  double scale;
+
+# ifndef __IPHONE_8_0                          // iOS 7 SDK or earlier
+
+  size = [screen bounds].size;                 //  points, not pixels
+  scale = [screen scale];                      //  available in iOS 4
+
+# else                                         // iOS 8 SDK or later
+
+  if ([screen respondsToSelector:@selector(nativeBounds)]) {
+    size = [screen nativeBounds].size;         //  available in iOS 8
+    scale = 1;  // nativeBounds is in pixels.
+
+    /* 'nativeScale' is very confusing.
+
+       iPhone 4s:
+          bounds:        320x480   scale:        2
+          nativeBounds:  640x960   nativeScale:  2
+       iPhone 5s:
+          bounds:        320x568   scale:        2
+          nativeBounds:  640x1136  nativeScale:  2
+       iPad 2:
+          bounds:       768x1024   scale:        1
+          nativeBounds: 768x1024   nativeScale:  1
+       iPad Retina/Air:
+          bounds:       768x1024   scale:        2
+          nativeBounds: 1536x2048  nativeScale:  2
+       iPhone 6:
+          bounds:        320x568   scale:        2
+          nativeBounds:  640x1136  nativeScale:  2
+       iPhone 6+:
+          bounds:        320x568   scale:        2
+          nativeBounds:  960x1704  nativeScale:  3
+
+       According to a StackOverflow comment:
+
+         The iPhone 6+ renders internally using @3x assets at a virtual
+         resolution of 2208x1242 (with 736x414 points), then samples that down
+         for display. The same as using a scaled resolution on a Retina MacBook
+         -- it lets them hit an integral multiple for pixel assets while still
+         having e.g. 12pt text look the same size on the screen.
+
+         The 6, the 5s, the 5, the 4s and the 4 are all 326 pixels per inch,
+         and use @2x assets to stick to the approximately 160 points per inch
+         of all previous devices.
+
+         The 6+ is 401 pixels per inch. So it'd hypothetically need roughly
+         @2.46x assets. Instead Apple uses @3x assets and scales the complete
+         output down to about 84% of its natural size.
+
+         In practice Apple has decided to go with more like 87%, turning the
+         1080 into 1242. No doubt that was to find something as close as
+         possible to 84% that still produced integral sizes in both directions
+         -- 1242/1080 = 2208/1920 exactly, whereas if you'd turned the 1080
+         into, say, 1286, you'd somehow need to render 2286.22 pixels
+         vertically to scale well.
+     */
+
+  } else {
+    size = [screen bounds].size;               //  points, not pixels
+    scale = [screen scale];                    //  available in iOS 4
+  }
+# endif  // iOS 8
+
+  size.width  = ceilf (size.width  / scale);
+  size.height = ceilf (size.height / scale);
+
+
+# if TARGET_IPHONE_SIMULATOR
+  NSLog(@"screen: %.0fx%0.f",
+        [[screen currentMode] size].width,
+        [[screen currentMode] size].height);
+  NSLog(@"bounds: %.0fx%0.f x %.1f = %.0fx%0.f",
+        [screen bounds].size.width,
+        [screen bounds].size.height,
+        [screen scale],
+        [screen scale] * [screen bounds].size.width,
+        [screen scale] * [screen bounds].size.height);
+
+#  ifdef __IPHONE_8_0
+  if ([screen respondsToSelector:@selector(nativeBounds)])
+    NSLog(@"native: %.0fx%0.f / %.1f = %.0fx%0.f",
+          [screen nativeBounds].size.width,
+          [screen nativeBounds].size.height,
+          [screen nativeScale],
+          [screen nativeBounds].size.width  / [screen nativeScale],
+          [screen nativeBounds].size.height / [screen nativeScale]);
+#  endif
+
+
+  /* Our view must be full screen, and view sizes are measured in points,
+     not pixels.  However, since our view is on a UINavigationController
+     that does not rotate, the size must be portrait-mode even if the
+     device is landscape.
+
+     On iOS 7, [screen bounds] always returned portrait-mode values.
+     On iOS 8, it rotates.  So swap as necessary.
+     On iOS 8, [screen nativeBounds] is unrotated, in pixels not points.
+   */
+  size = [screen bounds].size;
+  if (size.width > size.height) {
+    double s = size.width;
+    size.width = size.height;
+    size.height = s;
+  }
+
+  NSLog(@"saverView: %.0fx%.0f", size.width, size.height);
+# endif // TARGET_IPHONE_SIMULATOR
+
+
+  saverView = [self makeSaverView:name withSize:size];
+
+  if (! saverView) {
+    [[[UIAlertView alloc] initWithTitle: name
+                          message: @"Unable to load!"
+                          delegate: nil
+                          cancelButtonTitle: @"Bummer"
+                          otherButtonTitles: nil]
+     show];
+    return;
+  }
+
+  [[NSNotificationCenter defaultCenter]
+    addObserver:saverView
+    selector:@selector(didRotate:)
+    name:UIDeviceOrientationDidChangeNotification object:nil];
+
+  /* LAUNCH: */
+
+  if (launch) {
+    [self saveScreenshot];
+    NSRect f;
+    f.origin.x = 0;
+    f.origin.y = 0;
+    f.size = [[UIScreen mainScreen] bounds].size;
+    if (f.size.width > f.size.height) {  // Force portrait
+      double swap = f.size.width;
+      f.size.width = f.size.height;
+      f.size.height = swap;
+    }
+    [backgroundView setFrame:f];
+    [saverView setFrame:f];
+    [saverWindow addSubview: backgroundView];
+    [backgroundView addSubview: saverView];
+    [saverWindow setFrame:f];
+    [saverView setBackgroundColor:[NSColor blackColor]];
+
+    [saverWindow setHidden:NO];
+    [saverWindow makeKeyAndVisible];
+    [saverView startAnimation];
+    [self aboutPanel:nil];
+
+    // Tell the UILayoutContainerView to stop intercepting our events.
+    //    [[saverWindow rootViewController] view].userInteractionEnabled = NO;
+    //    saverView.userInteractionEnabled = YES;
+
+    // Tell the saverWindow that all events should go to saverView.
+    //
+    NSAssert ([saverWindow isKindOfClass:[EventCapturingWindow class]],
+              @"saverWindow is not an EventCapturingWindow");
+    ((EventCapturingWindow *) saverWindow).eventView = saverView;
+
+    // Doing this makes savers cut back to the list instead of fading,
+    // even though [XScreenSaverView stopAndClose] does setHidden:NO first.
+    // [window setHidden:YES];
+  }
+# endif // USE_IPHONE
+}
+
+
+- (void)loadSaver:(NSString *)name
+{
+  [self loadSaver:name launch:YES];
 }
 
 
 - (void)aboutPanel:(id)sender
 {
+# ifndef USE_IPHONE
+
   NSDictionary *bd = [saverBundle infoDictionary];
   NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:20];
 
@@ -205,7 +718,189 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 
   [[NSApplication sharedApplication]
     orderFrontStandardAboutPanelWithOptions:d];
+# else  // USE_IPHONE
+
+  if ([saverNames count] == 1)
+    return;
+
+  NSString *name = saverName;
+  NSString *year = [self makeDesc:saverName yearOnly:YES];
+
+
+  CGRect frame = [saverView frame];
+  CGFloat rot;
+  CGFloat pt1 = 24;
+  CGFloat pt2 = 14;
+  UIFont *font1 = [UIFont boldSystemFontOfSize:  pt1];
+  UIFont *font2 = [UIFont italicSystemFontOfSize:pt2];
+
+# ifdef __IPHONE_7_0
+  CGSize s = CGSizeMake(frame.size.width, frame.size.height);
+  CGSize tsize1 = [[[NSAttributedString alloc]
+                     initWithString: name
+                     attributes:@{ NSFontAttributeName: font1 }]
+                    boundingRectWithSize: s
+                    options: NSStringDrawingUsesLineFragmentOrigin
+                    context: nil].size;
+  CGSize tsize2 = [[[NSAttributedString alloc]
+                     initWithString: name
+                     attributes:@{ NSFontAttributeName: font2 }]
+                    boundingRectWithSize: s
+                    options: NSStringDrawingUsesLineFragmentOrigin
+                    context: nil].size;
+# else // iOS 6 or Cocoa
+  CGSize tsize1 = [name sizeWithFont:font1
+                   constrainedToSize:CGSizeMake(frame.size.width,
+                                                frame.size.height)];
+  CGSize tsize2 = [year sizeWithFont:font2
+                   constrainedToSize:CGSizeMake(frame.size.width,
+                                                frame.size.height)];
+#endif
+
+  CGSize tsize = CGSizeMake (tsize1.width > tsize2.width ?
+                             tsize1.width : tsize2.width,
+                             tsize1.height + tsize2.height);
+
+  tsize.width  = ceilf(tsize.width);
+  tsize.height = ceilf(tsize.height);
+
+  // Don't know how to find inner margin of UITextView.
+  CGFloat margin = 10;
+  tsize.width  += margin * 4;
+  tsize.height += margin * 2;
+
+  if ([saverView frame].size.width >= 768)
+    tsize.height += pt1 * 3;  // extra bottom margin on iPad
+
+  frame = CGRectMake (0, 0, tsize.width, tsize.height);
+
+  UIInterfaceOrientation orient = [rotating_nav interfaceOrientation];
+
+  /* Get the text oriented properly, and move it to the bottom of the
+     screen, since many savers have action in the middle.
+   */
+  switch (orient) {
+  case UIDeviceOrientationLandscapeRight:     
+    rot = -M_PI/2;
+    frame.origin.x = ([saverView frame].size.width
+                      - (tsize.width - tsize.height) / 2
+                      - tsize.height);
+    frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
+    break;
+  case UIDeviceOrientationLandscapeLeft:
+    rot = M_PI/2;
+    frame.origin.x = -(tsize.width - tsize.height) / 2;
+    frame.origin.y = ([saverView frame].size.height - tsize.height) / 2;
+    break;
+  case UIDeviceOrientationPortraitUpsideDown: 
+    rot = M_PI;
+    frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
+    frame.origin.y = 0;
+    break;
+  default:
+    rot = 0;
+    frame.origin.x = ([saverView frame].size.width  - tsize.width) / 2;
+    frame.origin.y =  [saverView frame].size.height - tsize.height;
+    break;
+  }
+
+  if (aboutBox)
+    [aboutBox removeFromSuperview];
+
+  aboutBox = [[UIView alloc] initWithFrame:frame];
+
+  aboutBox.transform = CGAffineTransformMakeRotation (rot);
+  aboutBox.backgroundColor = [UIColor clearColor];
+
+  /* There seems to be no easy way to stroke the font, so instead draw
+     it 5 times, 4 in black and 1 in yellow, offset by 1 pixel, and add
+     a black shadow to each.  (You'd think the shadow alone would be
+     enough, but there's no way to make it dark enough to be legible.)
+   */
+  for (int i = 0; i < 5; i++) {
+    UITextView *textview;
+    int off = 1;
+    frame.origin.x = frame.origin.y = 0;
+    switch (i) {
+      case 0: frame.origin.x = -off; break;
+      case 1: frame.origin.x =  off; break;
+      case 2: frame.origin.y = -off; break;
+      case 3: frame.origin.y =  off; break;
+    }
+
+    for (int j = 0; j < 2; j++) {
+
+      frame.origin.y = (j == 0 ? 0 : pt1);
+      textview = [[UITextView alloc] initWithFrame:frame];
+      textview.font = (j == 0 ? font1 : font2);
+      textview.text = (j == 0 ? name  : year);
+      textview.textAlignment = NSTextAlignmentCenter;
+      textview.showsHorizontalScrollIndicator = NO;
+      textview.showsVerticalScrollIndicator   = NO;
+      textview.scrollEnabled = NO;
+      textview.editable = NO;
+      textview.userInteractionEnabled = NO;
+      textview.backgroundColor = [UIColor clearColor];
+      textview.textColor = (i == 4 
+                            ? [UIColor yellowColor]
+                            : [UIColor blackColor]);
+
+      CALayer *textLayer = (CALayer *)
+        [textview.layer.sublayers objectAtIndex:0];
+      textLayer.shadowColor   = [UIColor blackColor].CGColor;
+      textLayer.shadowOffset  = CGSizeMake(0, 0);
+      textLayer.shadowOpacity = 1;
+      textLayer.shadowRadius  = 2;
+
+      [aboutBox addSubview:textview];
+    }
+  }
+
+  CABasicAnimation *anim = 
+    [CABasicAnimation animationWithKeyPath:@"opacity"];
+  anim.duration     = 0.3;
+  anim.repeatCount  = 1;
+  anim.autoreverses = NO;
+  anim.fromValue    = [NSNumber numberWithFloat:0.0];
+  anim.toValue      = [NSNumber numberWithFloat:1.0];
+  [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
+
+  [backgroundView addSubview:aboutBox];
+
+  if (splashTimer)
+    [splashTimer invalidate];
+
+  splashTimer =
+    [NSTimer scheduledTimerWithTimeInterval: anim.duration + 2
+             target:self
+             selector:@selector(aboutOff)
+             userInfo:nil
+             repeats:NO];
+# endif // USE_IPHONE
+}
+
+
+# ifdef USE_IPHONE
+- (void)aboutOff
+{
+  if (aboutBox) {
+    if (splashTimer) {
+      [splashTimer invalidate];
+      splashTimer = 0;
+    }
+    CABasicAnimation *anim = 
+      [CABasicAnimation animationWithKeyPath:@"opacity"];
+    anim.duration     = 0.3;
+    anim.repeatCount  = 1;
+    anim.autoreverses = NO;
+    anim.fromValue    = [NSNumber numberWithFloat: 1];
+    anim.toValue      = [NSNumber numberWithFloat: 0];
+    anim.delegate     = self;
+    aboutBox.layer.opacity = 0;
+    [aboutBox.layer addAnimation:anim forKey:@"animateOpacity"];
+  }
 }
+#endif // USE_IPHONE
 
 
 
@@ -214,77 +909,100 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
   NSString *name = [prefs stringForKey:@"selectedSaverName"];
 
+  if (! name) return;
+
   if (! [saverNames containsObject:name]) {
-    NSLog (@"Saver \"%@\" does not exist", name);
+    NSLog (@"saver \"%@\" does not exist", name);
     return;
   }
 
-  if (name) [self loadSaver: name];
+  [self loadSaver: name];
 }
 
 
 - (NSArray *) listSaverBundleNamesInDir:(NSString *)dir
 {
+# ifndef USE_IPHONE
+  NSString *ext = @"saver";
+# else
+  NSString *ext = @"xml";
+# endif
+
   NSArray *files = [[NSFileManager defaultManager]
                      contentsOfDirectoryAtPath:dir error:nil];
   if (! files) return 0;
+  NSMutableArray *result = [NSMutableArray arrayWithCapacity: [files count]+1];
 
-  int n = [files count];
-  NSMutableArray *result = [NSMutableArray arrayWithCapacity: n+1];
-
-  int i;
-  for (i = 0; i < n; i++) {
-    NSString *p = [files objectAtIndex:i];
-    if ([[p pathExtension] caseInsensitiveCompare:@"saver"]) 
+  for (NSString *p in files) {
+    if ([[p pathExtension] caseInsensitiveCompare: ext]) 
       continue;
-    [result addObject: [[p lastPathComponent] stringByDeletingPathExtension]];
+
+    NSString *name = [[p lastPathComponent] stringByDeletingPathExtension];
+
+# ifdef USE_IPHONE
+    // Get the saver name's capitalization right by reading the XML file.
+
+    p = [dir stringByAppendingPathComponent: p];
+    NSData *xmld = [NSData dataWithContentsOfFile:p];
+    NSAssert (xmld, @"no XML: %@", p);
+    NSString *xml = [XScreenSaverView decompressXML:xmld];
+    NSRange r = [xml rangeOfString:@"_label=\"" options:0];
+    NSAssert1 (r.length, @"no name in %@", p);
+    if (r.length) {
+      xml = [xml substringFromIndex: r.location + r.length];
+      r = [xml rangeOfString:@"\"" options:0];
+      if (r.length) name = [xml substringToIndex: r.location];
+    }
+
+# endif // USE_IPHONE
+
+    NSAssert1 (name, @"no name in %@", p);
+    if (name) [result addObject: name];
   }
 
+  if (! [result count])
+    result = 0;
+
   return result;
 }
 
 
+
 - (NSArray *) listSaverBundleNames
 {
   NSMutableArray *dirs = [NSMutableArray arrayWithCapacity: 10];
 
-  // First look in the bundle itself.
+# ifndef USE_IPHONE
+  // On MacOS, look in the "Contents/Resources/" and "Contents/PlugIns/"
+  // directories in the bundle.
+  [dirs addObject: [[[[NSBundle mainBundle] bundlePath]
+                      stringByAppendingPathComponent:@"Contents"]
+                     stringByAppendingPathComponent:@"Resources"]];
   [dirs addObject: [[NSBundle mainBundle] builtInPlugInsPath]];
 
-  // Then look in the same directory as the executable.
+  // Also look in the same directory as the executable.
   [dirs addObject: [[[NSBundle mainBundle] bundlePath]
                      stringByDeletingLastPathComponent]];
 
-  // Then look in standard screensaver directories.
-  [dirs addObject: @"~/Library/Screen Savers"];
-  [dirs addObject: @"/Library/Screen Savers"];
-  [dirs addObject: @"/System/Library/Screen Savers"];
+  // Finally, look in standard MacOS screensaver directories.
+//  [dirs addObject: @"~/Library/Screen Savers"];
+//  [dirs addObject: @"/Library/Screen Savers"];
+//  [dirs addObject: @"/System/Library/Screen Savers"];
+
+# else  // USE_IPHONE
+
+  // On iOS, only look in the bundle's root directory.
+  [dirs addObject: [[NSBundle mainBundle] bundlePath]];
+
+# endif // USE_IPHONE
 
   int i;
   for (i = 0; i < [dirs count]; i++) {
     NSString *dir = [dirs objectAtIndex:i];
     NSArray *names = [self listSaverBundleNamesInDir:dir];
     if (! names) continue;
-
-    // Make sure this directory is on $PATH.
-
-    const char *cdir = [dir cStringUsingEncoding:NSUTF8StringEncoding];
-    const char *opath = getenv ("PATH");
-    if (!opath) opath = "/bin"; // $PATH is unset when running under Shark!
-    char *npath = (char *) malloc (strlen (opath) + strlen (cdir) + 30);
-    strcpy (npath, "PATH=");
-    strcat (npath, cdir);
-    strcat (npath, ":");
-    strcat (npath, opath);
-    if (putenv (npath)) {
-      perror ("putenv");
-      abort();
-    }
-    /* Don't free (npath) -- MacOS's putenv() does not copy it. */
-
     saverDir   = [dir retain];
     saverNames = [names retain];
-
     return names;
   }
 
@@ -296,10 +1014,14 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
     err = [err stringByAppendingString:@"/"];
   }
   NSLog (@"%@", err);
-  exit (1);
+  return [NSArray array];
 }
 
 
+/* Create the popup menu of available saver names.
+ */
+#ifndef USE_IPHONE
+
 - (NSPopUpButton *) makeMenu
 {
   NSRect rect;
@@ -341,6 +1063,114 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   return popup;
 }
 
+#else  // USE_IPHONE
+
+- (NSString *) makeDesc:(NSString *)saver
+                  yearOnly:(BOOL) yearp
+{
+  NSString *desc = 0;
+  NSString *path = [saverDir stringByAppendingPathComponent:
+                               [[saver lowercaseString]
+                                 stringByReplacingOccurrencesOfString:@" "
+                                 withString:@""]];
+  NSRange r;
+
+  path = [path stringByAppendingPathExtension:@"xml"];
+  NSData *xmld = [NSData dataWithContentsOfFile:path];
+  if (! xmld) goto FAIL;
+  desc = [XScreenSaverView decompressXML:xmld];
+  if (! desc) goto FAIL;
+
+  r = [desc rangeOfString:@"<_description>"
+            options:NSCaseInsensitiveSearch];
+  if (r.length == 0) {
+    desc = 0;
+    goto FAIL;
+  }
+  desc = [desc substringFromIndex: r.location + r.length];
+  r = [desc rangeOfString:@"</_description>"
+            options:NSCaseInsensitiveSearch];
+  if (r.length > 0)
+    desc = [desc substringToIndex: r.location];
+
+  // Leading and trailing whitespace.
+  desc = [desc stringByTrimmingCharactersInSet:
+                 [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+
+  // Let's see if we can find a year on the last line.
+  r = [desc rangeOfString:@"\n" options:NSBackwardsSearch];
+  NSString *year = 0;
+  for (NSString *word in
+         [[desc substringFromIndex:r.location + r.length]
+           componentsSeparatedByCharactersInSet:
+             [NSCharacterSet characterSetWithCharactersInString:
+                               @" \t\n-."]]) {
+    int n = [word doubleValue];
+    if (n > 1970 && n < 2100)
+      year = word;
+  }
+
+  // Delete everything after the first blank line.
+  //
+  r = [desc rangeOfString:@"\n\n" options:0];
+  if (r.length > 0)
+    desc = [desc substringToIndex: r.location];
+
+  // Unwrap lines and compress whitespace.
+  {
+    NSString *result = @"";
+    for (NSString *s in [desc componentsSeparatedByCharactersInSet:
+                          [NSCharacterSet whitespaceAndNewlineCharacterSet]]) {
+      if ([result length] == 0)
+        result = s;
+      else if ([s length] > 0)
+        result = [NSString stringWithFormat: @"%@ %@", result, s];
+      desc = result;
+    }
+  }
+
+  if (year)
+    desc = [year stringByAppendingString:
+                   [@": " stringByAppendingString: desc]];
+
+  if (yearp)
+    desc = year ? year : @"";
+
+FAIL:
+  if (! desc) {
+    if ([saverNames count] > 1)
+      desc = @"Oops, this module appears to be incomplete.";
+    else
+      desc = @"";
+  }
+
+  return desc;
+}
+
+- (NSString *) makeDesc:(NSString *)saver
+{
+  return [self makeDesc:saver yearOnly:NO];
+}
+
+
+
+/* Create a dictionary of one-line descriptions of every saver,
+   for display on the UITableView.
+ */
+- (NSDictionary *)makeDescTable
+{
+  NSMutableDictionary *dict = 
+    [NSMutableDictionary dictionaryWithCapacity:[saverNames count]];
+  for (NSString *saver in saverNames) {
+    [dict setObject:[self makeDesc:saver] forKey:saver];
+  }
+  return dict;
+}
+
+
+#endif // USE_IPHONE
+
+
 
 /* This is called when the "selectedSaverName" pref changes, e.g.,
    when a menu selection is made.
@@ -362,6 +1192,10 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
 }
 
 
+# ifndef USE_IPHONE
+
+/* Create the desktop window shell, possibly including a preferences button.
+ */
 - (NSWindow *) makeWindow
 {
   NSRect rect;
@@ -450,7 +1284,7 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
   
   rect.origin.x += rect.size.width * (count ? 0.55 : -0.55);
   
-  NSWindow *window = [[NSWindow alloc]
+  NSWindow *win = [[NSWindow alloc]
                       initWithContentRect:rect
                                 styleMask:(NSTitledWindowMask |
                                            NSClosableWindowMask |
@@ -459,66 +1293,238 @@ relabel_menus (NSObject *v, NSString *old_str, NSString *new_str)
                                   backing:NSBackingStoreBuffered
                                     defer:YES
                                    screen:screen];
-  [window setMinSize:[window frameRectForContentRect:rect].size];
-
-  [[window contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
+  [win setMinSize:[win frameRectForContentRect:rect].size];
+  [[win contentView] addSubview: (pbox ? (NSView *) pbox : (NSView *) sv)];
 
-  [window makeKeyAndOrderFront:window];
+  [win makeKeyAndOrderFront:win];
   
   [sv startAnimation]; // this is the dummy saver
 
   count++;
 
-  return window;
+  return win;
+}
+
+
+- (void) animTimer
+{
+  for (NSWindow *win in windows) {
+    ScreenSaverView *sv = find_saverView ([win contentView]);
+    if ([sv isAnimating])
+      [sv animateOneFrame];
+  }
 }
 
+# endif // !USE_IPHONE
+
 
-- (void)applicationDidFinishLaunching: (NSNotification *) notif
+- (void)applicationDidFinishLaunching:
+# ifndef USE_IPHONE
+    (NSNotification *) notif
+# else  // USE_IPHONE
+    (UIApplication *) application
+# endif // USE_IPHONE
 {
   [self listSaverBundleNames];
 
-  int n = ([saverNames count] == 1 ? 1 : 2);
-  NSMutableArray *a = [[NSMutableArray arrayWithCapacity: n+1] retain];
+# ifndef USE_IPHONE
+  int window_count = ([saverNames count] <= 1 ? 1 : 2);
+  NSMutableArray *a = [[NSMutableArray arrayWithCapacity: window_count+1]
+                        retain];
   windows = a;
+
   int i;
-  for (i = 0; i < n; i++) {
-    NSWindow *window = [self makeWindow];
+  // Create either one window (for standalone, e.g. Phosphor.app)
+  // or two windows for SaverTester.app.
+  for (i = 0; i < window_count; i++) {
+    NSWindow *win = [self makeWindow];
     // Get the last-saved window position out of preferences.
-    [window setFrameAutosaveName:
+    [win setFrameAutosaveName:
               [NSString stringWithFormat:@"XScreenSaverWindow%d", i]];
-    [window setFrameUsingName:[window frameAutosaveName]];
-    [a addObject: window];
+    [win setFrameUsingName:[win frameAutosaveName]];
+    [a addObject: win];
+    // This prevents clicks from being seen by savers.
+    // [win setMovableByWindowBackground:YES];
+  }
+# else  // USE_IPHONE
+
+# undef ya_rand_init
+  ya_rand_init (0);    // Now's a good time.
+
+  rotating_nav = [[[RotateyViewController alloc] initWithRotation:YES]
+                         retain];
+  [window setRootViewController: rotating_nav];
+  [window setAutoresizesSubviews:YES];
+  [window setAutoresizingMask: 
+            (UIViewAutoresizingFlexibleWidth | 
+             UIViewAutoresizingFlexibleHeight)];
+
+  nonrotating_nav = [[[RotateyViewController alloc] initWithRotation:NO]
+                          retain];
+  [nonrotating_nav setNavigationBarHidden:YES animated:NO];
+
+  /* We run the saver on a different UIWindow than the one the
+     SaverListController and preferences panels run on, because that's
+     the only way to make rotation work right.  We want the system to
+     handle rotation of the UI stuff, but we want it to keep its hands
+     off of rotation of the savers.  As of iOS 8, this seems to be the
+     only way to accomplish that.
+
+     Also, we need to create saverWindow with a portrait rectangle, always.
+     Note that [UIScreen bounds] returns rotated and scaled values.
+  */
+  UIScreen *screen = [UIScreen mainScreen];
+# ifndef __IPHONE_8_0                          // iOS 7 SDK
+  NSRect frame = [screen bounds];
+  int ss = [screen scale];
+# else                                         // iOS 8 SDK
+  NSRect frame = ([screen respondsToSelector:@selector(nativeBounds)]
+                 ? [screen nativeBounds]       //   iOS 8
+                 : [screen bounds]);           //   iOS 7
+  int ss = ([screen respondsToSelector:@selector(nativeScale)]
+            ? [screen nativeScale]             //   iOS 8
+            : [screen scale]);                 //   iOS 7
+# endif                                                // iOS 8 SDK
+  frame.size.width  /= ss;
+  frame.size.height /= ss;
+  saverWindow = [[EventCapturingWindow alloc] initWithFrame:frame];
+  [saverWindow setRootViewController: nonrotating_nav];
+  [saverWindow setHidden:YES];
+
+  /* This view is the parent of the XScreenSaverView, and exists only
+     so that there is a black background behind it.  Without this, when
+     rotation is in progress, the scrolling-list window's corners show
+     through in the corners.
+  */
+  backgroundView = [[[NSView class] alloc] initWithFrame:[saverWindow frame]];
+  [backgroundView setBackgroundColor:[NSColor blackColor]];
+
+  SaverListController *menu = [[SaverListController alloc] 
+                                initWithNames:saverNames
+                                descriptions:[self makeDescTable]];
+  [rotating_nav pushViewController:menu animated:YES];
+  [menu becomeFirstResponder];
+
+  application.applicationSupportsShakeToEdit = YES;
+
+
+# endif // USE_IPHONE
+
+  NSString *forced = 0;
+  /* In the XCode project, each .saver scheme sets this env var when
+     launching SaverTester.app so that it knows which one we are
+     currently debugging.  If this is set, it overrides the default
+     selection in the popup menu.  If unset, that menu persists to
+     whatever it was last time.
+   */
+  const char *f = getenv ("SELECTED_SAVER");
+  if (f && *f)
+    forced = [NSString stringWithCString:(char *)f
+                       encoding:NSUTF8StringEncoding];
+
+  if (forced && ![saverNames containsObject:forced]) {
+    NSLog(@"forced saver \"%@\" does not exist", forced);
+    forced = 0;
   }
 
-  if (n == 1) {
-    [self loadSaver:[saverNames objectAtIndex:0]];
-  } else {
+  // If there's only one saver, run that.
+  if (!forced && [saverNames count] == 1)
+    forced = [saverNames objectAtIndex:0];
 
-    /* In the XCode project, each .saver scheme sets this env var when
-       launching SaverTester.app so that it knows which one we are
-       currently debugging.  If this is set, it overrides the default
-       selection in the popup menu.  If unset, that menu persists to
-       whatever it was last time.
-     */
-    const char *forced = getenv ("SELECTED_SAVER");
-    if (forced && *forced) {
-      NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
-      NSString *s = [NSString stringWithCString:(char *)forced
-                              encoding:NSUTF8StringEncoding];
-      NSLog (@"selecting saver %@", s);
-      [prefs setObject:s forKey:@"selectedSaverName"];
-    }
+  NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
+
+# ifdef USE_IPHONE
+  NSString *prev = [prefs stringForKey:@"selectedSaverName"];
 
-    [self selectedSaverDidChange:nil];
+  if (forced)
+    prev = forced;
+
+  // If nothing was selected (e.g., this is the first launch)
+  // then scroll randomly instead of starting up at "A".
+  //
+  if (!prev)
+    prev = [saverNames objectAtIndex: (random() % [saverNames count])];
+
+  if (prev)
+    [menu scrollTo: prev];
+# endif // USE_IPHONE
+
+  if (forced)
+    [prefs setObject:forced forKey:@"selectedSaverName"];
+
+# ifdef USE_IPHONE
+  /* Don't auto-launch the saver unless it was running last time.
+     XScreenSaverView manages this, on crash_timer.
+     Unless forced.
+   */
+  if (!forced && ![prefs boolForKey:@"wasRunning"])
+    return;
+# endif
+
+  [self selectedSaverDidChange:nil];
+//  [NSTimer scheduledTimerWithTimeInterval: 0
+//           target:self
+//           selector:@selector(selectedSaverDidChange:)
+//           userInfo:nil
+//           repeats:NO];
+
+
+
+# ifndef USE_IPHONE
+  /* On 10.8 and earlier, [ScreenSaverView startAnimation] causes the
+     ScreenSaverView to run its own timer calling animateOneFrame.
+     On 10.9, that fails because the private class ScreenSaverModule
+     is only initialized properly by ScreenSaverEngine, and in the
+     context of SaverRunner, the null ScreenSaverEngine instance
+     behaves as if [ScreenSaverEngine needsAnimationTimer] returned false.
+     So, if it looks like this is the 10.9 version of ScreenSaverModule
+     instead of the 10.8 version, we run our own timer here.  This sucks.
+   */
+  if (!anim_timer) {
+    Class ssm = NSClassFromString (@"ScreenSaverModule");
+    if (ssm && [ssm instancesRespondToSelector:
+                      @selector(needsAnimationTimer)]) {
+      NSWindow *win = [windows objectAtIndex:0];
+      ScreenSaverView *sv = find_saverView ([win contentView]);
+      anim_timer = [NSTimer scheduledTimerWithTimeInterval:
+                              [sv animationTimeInterval]
+                            target:self
+                            selector:@selector(animTimer)
+                            userInfo:nil
+                            repeats:YES];
+    }
   }
+# endif // !USE_IPHONE
 }
 
 
+#ifndef USE_IPHONE
+
 /* When the window closes, exit (even if prefs still open.)
-*/
+ */
 - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication *) n
 {
   return YES;
 }
 
+# else // USE_IPHONE
+
+- (void)applicationWillResignActive:(UIApplication *)app
+{
+  [(XScreenSaverView *)view setScreenLocked:YES];
+}
+
+- (void)applicationDidBecomeActive:(UIApplication *)app
+{
+  [(XScreenSaverView *)view setScreenLocked:NO];
+}
+
+- (void)applicationDidEnterBackground:(UIApplication *)application
+{
+  [(XScreenSaverView *)view setScreenLocked:YES];
+}
+
+#endif // USE_IPHONE
+
+
 @end