分类存档: OS X & iOS

[笔记] Objective-C 中模拟泛型及 Xcode 格式化代码插件

记录一些 iOS & OS X 开发过程中学到或有趣的东西。

Objective-C 中模拟泛型

今天看到的一个辅助代码,可以在 Objective-C 代码中添加泛型的支持:

泛型是程序设计语言的一种特性。允许程序员在强类型程序设计语言中编写代码时定义一些可变部份,那些部份在使用前必須作出指明。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。(via )

泛型一般出现在 .NET、Java 或 C++ 的代码里面。

在 Objective-C 里面,像数组之类的容器,在取单个元素时,返回类型都是 id,这个时候如果要使用返回对象的一些属性,就需要先转换一下类型,或者使用 getter 方法来获取。

如果用了泛型之后,就很方便了,可以直接以 arr.lastObject.prop 的方式来访问。

使用了上面这个代码使 Objective-C 支持泛型后,就可以像下面这样写代码了:

generics example

这个辅助代码是一个大宏,给 Objective-C 添加泛型支持是以以下几个方面来做到的,例如给 MyClass 添加泛型支持来说明:

  1. 定义一个 MyClass 的 Protocol
  2. 给各个容器类(例如 NSArray、NSMutableArray、NSSet)添加 Category,重新定义各个容器操作方法的返回值和参数类型
    1. 获取单个元素的方法返回值类型修改为 MyClass *
    2. 返回容器的方法返回值类型修改为 NSArray<myclass> *
    3. 参数类型同样处理

这样在使用 NSMutableArray</myclass><myclass> *arr 这样一个变量时,如果添加的元素不是 MyClass * 类型,就会发出警告了。

不过因为 Objective-C 在编译时并不会强制要求变量类型一致,所以如果传递一个非 MyClass *类型的参数,也是可以编译通过的。

当然这个库只能说是在一定程度上方便编写代码而已,至于用不用还是见仁见智了,每次要写 NSArray</myclass><myclass *> 也不见得更方便。

Xcode 代码格式化插件 BBUncrustifyPlugin

插件地址:

这个插件算是对 uncrustify 的一个快捷引用了。

可能会比 Xcode 自带的 Re-indent 功能强大一点点 :)

另外,还有一个图形化的参数配置工具,可以用来配置 uncrustify 那好几百项目的参数:UncrustifyX

参考资料

  1. http://iosdevelopertips.com/objective-c/generics-in-objective-c.html
  2. https://github.com/tomersh/Objective-C-Generics
  3. https://github.com/benoitsan/BBUncrustifyPlugin-Xcode
  4. http://zh.wikipedia.org/wiki/%E6%B3%9B%E5%9E%8B
  5. http://uncrustify.sourceforge.net

—EOF—

在 Sandboxed Mac App 中嵌入第三方可执行文件

之前开源了 一个 gitstats 的 GUI 应用 GitStatX,在提交到 GitHub (GitStatX) 之后,又准备提交到 Mac App Store。

在提交到 Mac App Store 之后,出现了一些问题,程序中包含的第三方可执行文件没有签名,导致苹果拒绝了提交的程序包:

App sandbox not enabled – The following executables must include the “com.apple.security.app-sandbox” entitlement with a Boolean value of true in the entitlements property list. Refer to the App Sandbox page for more information on sandboxing your app.

  • GitStatX.app/Contents/Resources/git/bin/git
  • GitStatX.app/Contents/Resources/gnuplot/gnuplot

但是 GitStatX 所包含的 git 以及 gnuplot,并不是我程序中的代码,也没有 Xcode 工程去使用一个 entitlements 文件来指定它为启用 sandbox 状态。

所幸在网上搜索的时候,找到了可以使用 codesign 工具来进行签名的方法。

检查可执行文件是否启用 sandbox

codesign --display --entitlements - ./commandlinetool

给可执行文件签名并启用 sandbox

先在命令行工具同目录创建一个 entitlements.plist:

< ?xml version="1.0" encoding="UTF-8"?>
< !DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true></true>
    <key>com.apple.security.inherit</key>
    <true></true>
</dict>
</plist>

这里设置了 com.apple.security.app-sandbox 为 true 来启用 sandbox。

然后使用 codesign 进行签名:

codesign -s "3rd Party Mac Developer Application: Your Name" --entitlements ./entitlements.plist ./commandlinetool

记得把 “3rd Party Mac Developer Application: Your Name” 替换为实际的证书名称。

问题

在给 gnuplot 签名之后,提交到 Mac App Store,苹果还是会自动验证并发邮件说 gnuplot 没有签名,于是在本地直接导出 GitStatX.app,并检查了一下,发现 gnuplot 的 entitlements 又没有了,但是 git 的 entitlements 还是保留的。在 gnuplot 同目录下,有一个 _CodeSignature 目录,可能是在打包的时候会自动处理。

为了避免这个问题,我就把 gnuplot 也放到了一个 bin 目录下,然后再打包并检查,发现 gnuplot 已经是正确签名并且保留有 entitlements 的了。

当然,GitStatX 最终也正确提交到 Mac App Store,并且程序进入了 Waiting For Review 状态。

参考资料

  1. Mac OS app, sandbox with command line tool?
  2. Checking Code Signing and Sandboxing Status in Code
  3. Entitlement Key Reference

— EOF —

开源 GitStatX:一个 gitstats 的 GUI 应用程序

GitStatX 是一个 gitstatx 的 GUI 应用程序,用于方便在 Mac OS X 中使用 gitstats。

一般情况下,如果要在 Mac OS X 上使用 gitstats,需要自行安装 gnuplot,而这需要使用 macports 或者 homebrew,通常这会比较费时间,并且只能使用命令行来使用 gitstats 生成所对应 git 仓库的报告。

GitStatX 提供了一个 GUI 来使用 gitstats,具备以下功能:

  • 同时管理多个项目
  • 使用分组来归类各个项目
  • 标识项目类型
  • 自动生成报告
  • 导出仓库的活动报告

截图

项目主页

仓库地址

下载

查看所有下载

授权

本软件及代码以 GPLv3 授权发行。

相关代码

  • 使用 bootstrap 样式的 gitstats:

  • 修正可执行文件依赖库的脚本:
  • 联系我

    — EOF —

    Developer ID Application 在 Mac OS X 10.7.5 下不能启动的问题

    记录一下最近发布阿里旺旺 for Mac 3.0.1 时碰到的问题。

    阿里旺旺在发布到 labs.etao.com 时,为了兼容 Mac App Store 版本,也要加上 Sandbox 支持,所以 App 是使用 Developer ID Application 方式发布的。

    但是有一些人在拿到新版本后,却没办法运行程序,总是会提示以下错误:

    Exception Type:  EXC_BAD_INSTRUCTION (SIGILL)
    Exception Codes: 0x0000000000000001, 0x0000000000000000
    
    Application Specific Information:
    dyld: launch, running initializers
    /usr/lib/libSystem.B.dylib
    xpchelper reply message validation: code signature invalid
    The code signature is not valid: The operation couldn’t be completed. (OSStatus error -67061.)
    
    Application Specific Signatures:
    code signature invalid
    

    一开始以为是签名的问题,然后另外一个同事使用同样方式发布 Developer ID Application 继续不能运行。于是去网上找了一下这个问题,发现了一篇文章《OS X 10.7.5 FAILS TO LAUNCH CODE-SIGNED APPS 》讲到这个问题,说这个可能是因为苹果在 Mac OS X 10.7.5 中改了什么东西导致的,只要重签名一下 App 就可以了。

    打开终端,进入要签名的 App 所在目录,使用以下命令来重新签名:

    codesign -fs 'Developer ID Application' --prefix 'com.taobao' \
    --preserve-metadata=i,e,res,req --timestamp=none AliWangwang.app
    

    注意:如果你的 Keychain Access 里有多个 Developer ID Application 证书的话,需要把 Developer ID Application 替换成 Keychain Access 中完整的 Developer ID Application 证书的名称。

    重新签名之后的 Developer ID Application,就可以在 Mac OS X 10.7.5 上正常打开和使用了。

    参考资料

    1. OS X 10.7.5 FAILS TO LAUNCH CODE-SIGNED APPS

    — EOF —

    [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 —

    在 Ubuntu 中使用 GNUstep 搭建 Objective-C 开发环境

    GNUstep 介绍见

    安装相关程序

    直接 apt-get install 搞定。

    • build-essential
    • gnustep
    • gnustep-devel
    • gnustep-examples
    • gobjc
    • gobjc++

    设置环境变量

    为了方便使用 GNUstep 的各种编译工具,需要先在 .bashrc 中导入 GNUstep 的脚本。

    # Setup GNUstep
    GNUSTEP_MAKEFILES=/usr/share/GNUstep/Makefiles
    export GNUSTEP_MAKEFILES
    source $GNUSTEP_MAKEFILES/GNUstep.sh
    

    编写 Makefile

    使用 Makefile 来编译 Objective-C App,注意 Makefile 的文件名为 GNUmakefile

    include $(GNUSTEP_MAKEFILES)/common.make
    
    APP_NAME=HelloWorld
    HelloWorld_OBJC_FILES=test.m 
    
    include $(GNUSTEP_MAKEFILES)/application.make
    

    这个是编译出 Mac OS App Bundle 形式的目标,如果是编译命令行工具的话,可以使用如下 Makefile:

    include $(GNUSTEP_MAKEFILES)/common.make
    
    TOOL_NAME=HelloWorld
    HelloWorld_OBJC_FILES=test.m 
    
    include $(GNUSTEP_MAKEFILES)/tool.make
    

    在编译命令行工具时,AppKit 就不会自动链接进来了,使用 NSColor 这样的类就会有问题。

    HelloWorld

    一个最简单的 HelloWorld 代码:

    #import <foundation /Foundation.h>
    
    int main() {
        NSAutoreleasePool *pool = [NSAutoreleasePool new];
        NSLog(@"HelloWorld");
        [pool release];
        return 0;
    }
    

    保存为 HelloWorld.m,编写 Makefile:

    include $(GNUSTEP_MAKEFILES)/common.make
    
    APP_NAME=HelloWorld
    HelloWorld_OBJC_FILES=HelloWorld.m 
    
    include $(GNUSTEP_MAKEFILES)/application.make
    

    保存 Makefile 为 GNUmakefile,执行 make,会在当前目录生成 HelloWorld.app,使用 openapp 命令运行:

    openapp ./HelloWorld.app
    

    当然如果是编译成命令行工具的目标的话,会在当前目录的 obj 目录中生成 HelloWorld 可执行文件,这个可以直接从命令行运行:

    ./obj/HelloWorld
    

    参考资料

    1. http://www.gnustep.org
    2. http://www.qiongbupa.com/archives/678
    3. http://www.gnustep.org/resources/documentation/User/GNUstep/gnustep-howto_4.html#SEC8
    4. http://www.gnustep.it/nicola/Tutorials/WritingMakefiles/node6.html
    5. http://www.gnustep.it/nicola/Tutorials/index.html

    — EOF —

    在 Mac App 中支持全屏和文件版本控制

    前言

    在 Mac OS X 10.7 Lion 中,新增了两个特性对用户来说可能比较有用的特性,一个是全屏程序,一个是文件版本控制。

    全屏程序可以使用户集中注意力到一个程序上,并且隐藏菜单栏、Dock 等不相关的界面元素,使得工作区最大化,更加有效地利用有限的屏幕空间。

    文件版本控制可以让用户在不借助于 Time Machine、Git 或者 SVN 这些工具的时候,也可以轻松恢复文件到之前保存过的版本,防止一些误操作删除了重要内容。

    全屏程序

    全屏功能可以让程序以全屏方式显示,这样用户可以拥有最大的操作区域,并隐藏与程序无关的视觉元素。

    在全屏方式下,Dock 和菜单栏都会自动隐藏,可以通过将鼠标移动到屏幕边缘的方式来重新显示 Dock 或者菜单栏。

    目前全屏程序有一个问题是在使用双屏的情况下,有一个屏幕会无法使用,只能显示背景图片,而不能同时使用两个屏幕,或者将另外一个程序全屏在另一个屏幕。

    另外,全屏程序切换是使用水平动画切换,对我来说可能动作太大,不是很习惯,这是题外话了。

    要设置一个 App 支持全屏很简单,只需要设置对应 Window 的属性即可。

    Full Screen 有两种模式,分别为 NSWindowCollectionBehaviorFullScreenPrimaryNSWindowCollectionBehaviorFullScreenAuxiliary,其中多数时候只需要使用 NSWindowCollectionBehaviorFullScreenPrimary 即可。

    在设置 NSWindowCollectionBehaviorFullScreenPrimary 后,就会在相应的 Window 右上角显示一个全屏按钮,用户通过这个按钮就可以切换程序到全屏方式使用。

    全屏模式切换菜单

    有些时候,为了用户使用方便,需要设置一个快捷键,或者菜单来供用户进行全屏模式的切换,这时,只需要添加一个 NSMenuItem 到主菜单的 View 菜单中,将 selector 连接到 First Responder 的 toggleFullScreen: 即可。

    Cocoa 在运行时会自动寻找 View 菜单中 selector 为 toggleFullScreen: 的菜单项目,并根据当前窗体的显示情况来自动更新菜单标题为 Enter Full Screen 或 Exit Full Screen。

    文件版本控制

    在 Lion 中对普通用户来说,一个非常有用的特性就是文件版本控制了。在不需要 Time Machine,不需要 Git、SVN 等版本控制参与的情况下,只需要程序支持这个功能,就可以直接找回编辑过文件的以前版本。

    在文件版本控制之后,其实是自动保存功能在起作用,要实现文件版本控制,只需要 Mac App 实现自动保存即可,文件版本的控制会有框架来做,不需要开发者过多参与。

    对于 NSDocument based 的 Mac App 来说,要实现文件自动保存,一般情况下,只需要在 NSDocument 的子类中实现以下方法就行了:

    + (BOOL)autosavesInPlace {
        return YES:
    }
    

    当然,这是在 Mac App 中,是以 NSDocument 提供的诸如 - (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError 或者 - (NSFileWrapper *)fileWrapperOfType:(NSString *)typeName error:(NSError **)outError 来保存文件内容为基础,而不是自己去实现写文件内容到磁盘,那样的话就算有 autosaveInPlace 也是不行的。

    另外,同样需要使用 - (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError 等 NSDocument 提供的 API 来获取内容。

    另外,在 NSWindowDelegate 也有一些新的方法,可以控制程序在浏览文件的以往版本时,控制程序窗口的样式,例如大小,设置控件的状态等。例如,在默认情况下,浏览文件以往版本会改变当前文档窗口的大小,而如果不想修改文档窗口大小,只是缩放显示的话,可以在 window:willResizeForVersionBrowserWithMaxPreferredSize:maxAllowedSize: 方法中指定窗口要变化到的目标大小:

    - (NSSize)window:(NSWindow *)window willResizeForVersionBrowserWithMaxPreferredSize:(NSSize)maxPreferredFrameSize maxAllowedSize:(NSSize)maxAllowedFrameSize {
        NSSize winSize = [window frame].size;
        return winSize;
    }
    

    NSWindowDelegate 的 Managing Presentation in Version Browsers 一节中,还有另外一个 delegate 方法,可以得知当前文档窗口正在进入版本浏览,或者退出版本浏览等状态,从而可以设置界面控件,将一些无关的界面元素隐藏,使用户可以更加清晰的观察到文件不同版本之间的差别。

    要实现文件版本控制的支持,开发者并没有很多工作需要做,多数功能已经由框架实现。

    参考资料

    — EOF —

    iOS App 自定义 URL Scheme 设计

    在 iOS 里,程序之间都是相互隔离,目前并没有一个有效的方式来做程序间通信,幸好 iOS 程序可以很方便的注册自己的 URL Scheme,这样就可以通过打开特定 URL 的方式来传递参数给另外一个程序。

    例如在 iPad 上浏览网页,并且 iPad 已经安装了 淘宝 HD,那么就打开下面这个链接就会在淘宝 HD 中查看这个商品的详细信息,也可以方便的使用淘宝 HD 进行购买、收藏等操作。

    在淘宝 HD 中查看商品“2012西藏卓明谷方舟登舰卡(船票)”

    当然,如果你在 Mac OS X 中打开这个链接,或者在没有安装 淘宝 HD 的 iPad 中打开这个链接,会提示没有程序来打开这个链接。

    配置

    要为 iOS 程序添加自定义协议的支持是一件很方便的事,只需要在程序的 Info.plist 添加一个 URL types 节点就可以了。在这个节点里,可以设置这个程序所支持的自定义协议名称,像 http、ftp 这种,一般我们可以设置为程序英文名称,像淘宝客户端中就设置了 taobao,这样 taobao:// 这个形式的 URL 就会关联到淘宝客户端的 App。

    Info.plist config

    实现

    在 Info.plist 里面设置完 URL types 之后,就可以在程序中处理这类 URL 的打开请求了。

    在外部程序中,如果打开了指定自定义协议的 URL,程序中 application delegate 的 application:handleOpenURL: 方法就会被调用,在这个方法里,可以获取到触发这个方法的 URL,可以通过对这个 URL 进行判断,例如根据不同的 Host,不同的 Query String 来执行不同的动作。

    - (void)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
        NSLog(@"%@", [url absoluteString]);
    
        // 在 host 等于 item.taobao.com 时,说明一个宝贝详情的 url,
        // 那么就使用本地的 TBItemDetailViewController 来显示
        if ([[url host] isEqualToString:@"item.taobao.com"]) {
    
            // 这里只是简单地假设 url 形式为 taobao://item.taobao.com/item.htm?id=12345678
            // 先获取要查看的宝贝详情的 itemId
            NSString *itemId = [[url query] substringFromIndex:[[url query] rangeOfString:@"id="].location+3];
    
            // 使用本地 ViewController 来显示淘宝商品详情
            TBItemDetailViewController *controller = [[TBItemDetailViewController alloc] initWithItemId:itemId];
            [self.navigationController pushViewController:controller animated:YES];
            [controller release];
        }
    }
    

    淘宝 for iOS

    现在,淘宝 和 淘宝 HD 两个客户端都支持 taobao:// 协议,来打开特定的链接。目前已经支持的有:

    例如,想要在自己的程序中,使用淘宝客户端来显示一个淘宝商品的详情,以支持用户可以直接在 iPhone 上购买,收藏等,就可以使用下面的代码:

    - (void)showItemInTaobao4iOS:(NSString *)itemId {
        // 构建淘宝客户端协议的 URL
        NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"taobao://item.taobao.com/item.htm?id=%@", itemId]];
    
        // 判断当前系统是否有安装淘宝客户端
        if ([[UIApplication sharedApplication] canOpenURL:url]) {
            // 如果已经安装淘宝客户端,就使用客户端打开链接
            [[UIApplication sharedApplication] openURL:url];
        } else {
            // 否则使用 Mobile Safari 或者内嵌 WebView 来显示
            url = [NSURL URLWithString:[NSString stringWithFormat:@"http://item.taobao.com/item.htm?id=%@", itemId]];
            [[UIApplication sharedApplication] openURL:url];
        }
    }
    

    使用淘宝客户端来打开淘宝链接的好处就是可以让用户更加方便的去购买商品,而不需要再重新登录,或者把用户名密码给了第三方的网站而导致安全隐患。

    扩展性

    在淘宝客户端中,支持的 URL 往往是淘宝网站已经有的链接,这些链接的 QueryString 中所带的参数往往已经满足了使用本地代码显示内容的需要,但是为了扩展性考虑,就需要添加一些额外的参数,并且与原有 QueryString 中不冲突的参数名称。通过这些额外的参数,再实现客户端打开链接时更多的自定义行为。

    例如,如果在打开特定 URL,进行一些操作后需要再返回原来的程序,就会需要在 URL 中添加类似于 callback 这样的参数,这样在客户端处理完用户的操作后,可以将用户操作的结果返回给原来的程序,从而实现程序间的通信。

    示例:

    - (void)buyItemInTaobao4iOS:(NSString *)itemId {
        // 构建淘宝客户端协议的 URL
        NSString *format = @"taobao://item.taobao.com/item.htm?id=%@&_action=buy&_callback=myapp://taobaobuysuccess";
        NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:format, itemId]];
    
        // 使用淘宝客户端打开链接
        [[UIApplication sharedApplication] openURL:url];
    }
    

    注意:当前淘宝客户端并不支持这样的调用方式,这里仅是一个示例。

    当然,在使用这种方式实现程序间通信的时候,需要考虑检查一下来源 URL 的合法性,防止一些非法的调用造成用户的损失。

    结语

    通过自定义协议地支持,可以将 iOS 程序的一些功能和服务提供给外部程序,也可以实现 Web 和本地应用之间的互相调用。

    如果你的 iOS 程序有这些需求的话,那么就可以考虑在 iOS 程序中添加自定义协议的支持了。

    参考资料

    1. iOS Application Programming Guide: Implementing Custom URL Schemes

    — EOF —

    淘宝 for iOS 历程

    这是在 [淘宝2011技术嘉年华](http://developerclub.taobao.com/) 上讲的关于淘宝 for iOS 开发过程的演示文稿。

    View more presentations from ohdarling88.

    — EOF —