月度存档: 九月 2012

[Tips] 适配 iOS App 到 iPhone 5 和 iOS 6

iPhone 5 和 iOS 6 正式发布后,iOS 开发者们就要开始做 iPhone 5 和 iOS 6 的适配工作了 :)

在迁移我自己的几个 iOS App 过程中,发现了老的 Xcode 工程代码在 iOS 6 中无法响应屏幕旋转的问题,在这里记录一下。顺便记录一下怎样适配 iOS App 到 iPhone 5 给还不知道怎么做的朋友们。

适配 iPhone 5 的 4 寸屏幕


iOS App LetterBox Mode

iPhone 5 的屏幕分辨率为 640×1136,默认情况下,老的 iOS App 会以上下加两条黑边的模式来运行,程序实际占用的分辨率还是 640×960,保证了原有的 iOS App 在不修改的情况下也可以正常运行。

如果需要适配 iPhone 5 的 4 寸屏幕,是一件很简单的事,只需要添加一张对应 iPhone 5 启动图片即可。

iPhone 5 的分辨率为 640×1136,所以只需要添加一张分辨率为 640×1136 的启动图片到 iOS App 工程即可。启动图片需要命名为 Default-568h@2x.png,因为 iPhone 5 没有非 Retina 屏幕的型号,所以不需要 Default-568h.png 这样一个文件了。

添加了对应 iPhone 5 分辨率的启动图片后,iOS App 就会以 640×1136 的分辨率运行了。

当然了,如果 iOS App 里在计算 view 的 frame 时,使用了固定的 320×480 来计算的话,在显示时就会有一些问题,这时候就需要动态根据 view 的 frame 大小去计算,而不是使用 Hard Code。

另外,也可以灵活使用 autoresizingMask,这样在未来 iPhone 又有其他分辨率,又或者出现了 iPad mini 的时候更方便处理。

解决旧的 iOS App 在 iOS 6 中无法随屏幕旋转的问题

如果你的 iOS App 支持随屏幕旋转而旋转,但是到了 iOS 6 中,发现这个功能不生效了,那么就要检查一下是不是因为旧版本的 Xcode 创建工程的原因。

但是在 iOS SDK Release Notes for iOS 6.0 中的 UIKit 一节中说到“Autorotation is changing in iOS 6. In iOS 6, the shouldAutorotateToInterfaceOrientation: method of UIViewController is deprecated. In its place, you should use the supportedInterfaceOrientationsForWindow: and shouldAutorotate methods.”,并且详细解释中也说明了 iOS 6 中的改动:

More responsibility is moving to the app and the app delegate. Now, iOS containers (such as UINavigationController) do not consult their children to determine whether they should autorotate. By default, an app and a view controller’s supported interface orientations are set to UIInterfaceOrientationMaskAll for the iPad idiom and UIInterfaceOrientationMaskAllButUpsideDown for the iPhone idiom.

A view controller’s supported interface orientations can change over time—even an app’s supported interface orientations can change over time. The system asks the top-most full-screen view controller (typically the root view controller) for its supported interface orientations whenever the device rotates or whenever a view controller is presented with the full-screen modal presentation style. Moreover, the supported orientations are retrieved only if this view controller returns YES from its shouldAutorotate method. The system intersects the view controller’s supported orientations with the app’s supported orientations (as determined by the Info.plist file or the app delegate’s application:supportedInterfaceOrientationsForWindow: method) to determine whether to rotate.

The system determines whether an orientation is supported by intersecting the value returned by the app’s supportedInterfaceOrientationsForWindow: method with the value returned by the supportedInterfaceOrientations method of the top-most full-screen controller.

The setStatusBarOrientation:animated: method is not deprecated outright. It now works only if the supportedInterfaceOrientations method of the top-most full-screen view controller returns 0. This makes the caller responsible for ensuring that the status bar orientation is consistent.

For compatibility, view controllers that still implement the shouldAutorotateToInterfaceOrientation: method do not get the new autorotation behaviors. (In other words, they do not fall back to using the app, app delegate, or Info.plist file to determine the supported orientations.) Instead, the shouldAutorotateToInterfaceOrientation: method is used to synthesize the information that would be returned by the supportedInterfaceOrientations method.

说明中提到,在 iOS 6 中系统会通过查询 rootViewController 来判断是否可以旋转视图,但是在以前,使用 Xcode 创建一个工程时,通常会有以下代码来显示主窗口:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    // Add the tab bar controller's view to the window and display.
    [self.window addSubview:tabBarController.view];
    [self.window makeKeyAndVisible];
 
    return YES;
}

在这里,并没有设置 self.window 的 rootViewController,而且在 MainWindow.xib 中,也没在连接 window 的 rootViewController 到默认的 UITabBarController。

因此,我们有两种方法来解决这个问题:

1. 在代码中设置 rootViewController

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    // Add the tab bar controller's view to the window and display.
    self.window.rootViewController = tabBarController;
    [self.window addSubview:tabBarController.view];
    [self.window makeKeyAndVisible];
 
    return YES;
}

2. 在 MainWindow.xib 中连接 window 的 rootViewController

两种方法任意选择一种就行了。

当然使用新版本 Xcode 创建的工程应该是没问题的,而且新版本 Xcode 的工程模板通常会使用代码来创建 rootViewController,而且默认也不再生成 MainWindow.xib。

参考资料

  1. iOS SDK Release Notes for iOS 6

—EOF—

Mac OS X 中实现支持任意位置拖动的不规则窗体

Mac OS X 中实现支持任意位置拖动的不规则窗体

在开发 Mac OS App 时,根据视觉设计师的需要,有些时候会要实现一些不规则的窗口。一般不规则窗口都不包含 Mac OS X 标准窗口的标题栏,这样用户就没有办法通过拖动标题栏来拖动窗口。

为了能让用户拖动一个无标题栏的窗口,我们就需要自己去处理这个移动窗口的过程。另外,由于窗口有可能是不规则的,那么同样就没有办法拖动窗口边缘来改变窗口大小了,同样需要自己进行处理。

因为要处理鼠标点击事件,因此我们先创建一个继承于 NSWindow 的类 JWBorderlessWindow,以下代码都是此类的代码实现。

初步实现

//
//  JWBorderlessWindow
//
//  Created by Xu Jiwei on 12-9-3.
//  Copyright (c) 2012 xujiwei.com. All rights reserved.
//
 
@implementation JWBorderlessWindow
 
- (BOOL)canBecomeKeyWindow {
    return YES;
}
 
 
- (BOOL)canBecomeMainWindow {
    return YES;
}
 
 
- (NSRect)resizeAreaRect {
    const CGFloat resizeBoxSize = 20.0;
 
    // 窗口右下角 20x20 的区域为改变窗口的区域
    NSRect frame = [self frame];
    NSRect resizeRect = NSMakeRect(frame.size.width - resizeBoxSize, 0,
                                   resizeBoxSize, resizeBoxSize);
 
    return resizeRect;
}
 
 
// 处理鼠标按下事件
- (void)mouseDown:(NSEvent *)event {
    NSPoint pointInView = [event locationInWindow];
 
    // 判断是否点击在改变窗口大小区域
    BOOL resize = NSPointInRect(pointInView, [self resizeAreaRect]);
 
    NSWindow *window = self;
    NSPoint originalMouseLocation = [window convertBaseToScreen:[event locationInWindow]];
    NSRect originalFrame = [window frame];
 
    while (YES) {
        // 捕获鼠标拖动或鼠标按键弹起事件
        NSEvent *newEvent = [window nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
 
        if ([newEvent type] == NSLeftMouseUp) {
            break;
        }
 
        // 计算鼠标移动的偏移
        NSPoint newMouseLocation = [window convertBaseToScreen:[newEvent locationInWindow]];
        NSPoint delta = NSMakePoint(newMouseLocation.x - originalMouseLocation.x,
                                    newMouseLocation.y - originalMouseLocation.y);
 
        NSRect newFrame = originalFrame;
 
        if (!resize) {
            // 移动窗口
            newFrame.origin.x += delta.x;
            newFrame.origin.y += delta.y;
 
        } else {
            NSSize maxSize = [window maxSize];
            NSSize minSize = [window minSize];
 
            // 改变窗口大小
            newFrame.size.width += delta.x;
            newFrame.size.height -= delta.y;
 
            // 控制窗口大小在限制范围内
            newFrame.size.width = MIN(MAX(newFrame.size.width, minSize.width), maxSize.width);
            newFrame.size.height = MIN(MAX(newFrame.size.height, minSize.height), maxSize.height);
            newFrame.origin.y -= newFrame.size.height - originalFrame.size.height;
        }
 
        [window setFrame:newFrame display:YES animate:NO];
    }
}
 
@end

以上代码借鉴自 RoundWindow 示例代码。

这些代码就实现了点击窗口就可以拖动窗口,点击 resizeAreaRect 所返回的区域就可以改变窗口大小。

但是在用的时候,会发现一个问题,如果这个窗口上有控件的话,例如有按钮、文本框等控件,那么就鼠标点击在按钮上时,就无法拖动这个窗口了。

有些时候可能整个窗口是一个列表,没有空余的地方露出 Window 本身,那想要拖动窗口就会变得很麻烦了。

实现任意位置移动

因为 NSWindow 的 mouseDown: 方法,只会在点击在窗口本身上时,才会被调用,而如果点击在了按钮或者文本框上,这个鼠标事件就不会传递到 NSWindow 上。

根据《Cocoa Event Handling Guide》可以知道,如果有事件过来,会由 NSApp 分发到 NSWindow,再通过 NSWindow,使用 sendEvent: 方法来分发,那么我们就可以在个方法中,去捕获鼠标点击事件。

Path of a mouse event
via Cocoa Event Handling Guide

//
//  JWBorderlessWindow
//
//  Created by Xu Jiwei on 12-9-3.
//  Copyright (c) 2012 xujiwei.com. All rights reserved.
//
 
@interface JWBorderlessWindow : NSWindow {
    BOOL            mouseDraggedForMoveOrResize;
    BOOL            mouseDownInResizeArea;
    NSPoint         mouseDownLocation;
    NSRect          mouseDownWindowFrame;
}
@end
 
@implementation JWBorderlessWindow
 
- (BOOL)canBecomeKeyWindow {
    return YES;
}
 
 
- (BOOL)canBecomeMainWindow {
    return YES;
}
 
 
- (NSRect)resizeAreaRect {
    const CGFloat resizeBoxSize = 20.0;
 
    NSRect frame = [self frame];
    NSRect resizeRect = NSMakeRect(frame.size.width - resizeBoxSize, 0,
                                   resizeBoxSize, resizeBoxSize);
 
    return resizeRect;
}
 
 
- (void)handleMoveOrResize:(NSEvent *)event {
    NSWindow *window = self;
 
    //
    // Work out how much the mouse has moved
    //
    NSPoint newMouseLocation = [window convertBaseToScreen:[event locationInWindow]];
    NSPoint delta = NSMakePoint(newMouseLocation.x - mouseDownLocation.x,
                                newMouseLocation.y - mouseDownLocation.y);
 
    NSRect newFrame = mouseDownWindowFrame;
 
    if (!mouseDownInResizeArea) {
        //
        // Alter the frame for a drag
        //
        newFrame.origin.x += delta.x;
        newFrame.origin.y += delta.y;
 
    } else {
        NSSize maxSize = [window maxSize];
        NSSize minSize = [window minSize];
 
        newFrame.size.width += delta.x;
        newFrame.size.height -= delta.y;
 
        newFrame.size.width = MIN(MAX(newFrame.size.width, minSize.width), maxSize.width);
        newFrame.size.height = MIN(MAX(newFrame.size.height, minSize.height), maxSize.height);
        newFrame.origin.y -= newFrame.size.height - mouseDownWindowFrame.size.height;
    }
 
    [window setFrame:newFrame display:YES animate:NO];
}
 
 
- (void)sendEvent:(NSEvent *)event {
    // 处理单击事件,实现在窗口任意位置移动窗口
    if (event.type == NSLeftMouseDown) {
        mouseDraggedForMoveOrResize = NO;
        mouseDownLocation = [self convertBaseToScreen:[event locationInWindow]];
        mouseDownWindowFrame = [self frame];
        mouseDownInResizeArea = NSPointInRect([event locationInWindow], [self resizeAreaRect]);
 
        BOOL keepOn = YES;
 
        while (keepOn) {
            NSEvent *newEvent = [self nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)
                                                  untilDate:[NSDate distantFuture]
                                                     inMode:NSEventTrackingRunLoopMode
                                                    dequeue:NO];
            switch (newEvent.type) {
                case NSLeftMouseDragged:
                    // 处理鼠标移动事件
                    [self handleMoveOrResize:newEvent];
                    // 把事件从队列中删除
                    [self nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
                    mouseDraggedForMoveOrResize = YES;
                    break;
 
                case NSLeftMouseUp:
                    // 如果不是正在移动窗口或改变窗口大小,就把事件继续分发
                    if (!mouseDraggedForMoveOrResize) {
                        [super sendEvent:event];
                    }
                    keepOn = NO;
                    break;
 
                default:
                    keepOn = NO;
                    break;
            }
        }
 
    } else {
        [super sendEvent:event];
    }
}
 
@end

这时候又会有另外一个问题,虽然可以在窗口任意位置都可以拖动窗口,但是在 NSTextField 或者 NSTextView 获取焦点时,如果想用鼠标拖动光标来选择文本,是没办法做到的,在拖动的时候变成了在拖动整个窗口。

好吧,继续解决问题。

修正 NSTextField 及 NSTextView 兼容性

- (void)sendEvent:(NSEvent *)event {
    // 处理单击事件,实现在窗口任意位置移动窗口
    // 判断鼠标点击所在位置的 view,如果是 NSTextView,就不处理,直接继续传递事件
    NSView *targetView = [self.contentView hitTest:[event locationInWindow]];
    if (event.type == NSLeftMouseDown && ![targetView isKindOfClass:[NSTextView class]]) {
        // 省略,与前一段代码相同
 
    } else {
        [super sendEvent:event];
    }
}

完成,在 NSTextField 或者 NSTextView 获得焦点时,可以用鼠标来拖动选中内容了。

结语

下载文中示例程序:JWBorderlessWindowTest (39KB)

参考资料

  1. [http: //cocoawithlove.com/2008/12/drawing-custom-window-on-mac-os-x.html](http: //cocoawithlove.com/2008/12/drawing-custom-window-on-mac-os-x.html)
  2. Cocoa Event Handling Guide: Event Architecture – Event Dispatch

— EOF —