24/7 twenty-four seven

iOS/OS X application programing topics.

NSArrayやNSDictionaryからNSNullを効率よく取り除く

iOSアプリケーションでWeb APIから返ってきたJSONを処理するのにNSNullの扱いに困っていて、事前にNSNullを取り除いてしまうのが事故を防ぐための確実な方法なのですが、再帰的にすべての要素を検査する以外になにかいい方法がないかと思って考えていたらちょっとおもしろい方法を思いついたので書いてみました。


kishikawakatsumi/CollectionUtils · GitHub

↑ に含まれるCUCompactArrayとCUCompactDictionaryです。


NSArrayとNSDictionaryのサブクラスとして実装されていて、次のようにして生成します。
(普通にalloc/initを使って生成することも可能です)

NSArray *array = @[@"0", @"1", [NSNull null], @"2", [NSNull null], @"3"];
NSArray *compactArray = [array cu_compactArray];
//=> ["0", "1", "2", "3"]
NSDictionary *dictionary = @{@"one": @"1",
                             @"null": [NSNull null],
                             @"two": @"2",
                             @"three": @"3"};
NSDictionary *compactDictionary = [dictionary cu_compactDictionary];
 //=> {"one": "1", "two": "2", "three": "3"}


JSONオブジェクトに対して使われることを想定しているので、ネストしたコレクションに対しても有効です。

NSArray *array = @[@"0",
                   @"1",
                   [NSNull null],
                   @"2",
                   @{@"one": @"1",
                     @"null": [NSNull null],
                     @"two": @"2",
                     @"three": @"3"},
                   @"4"];
NSMutableArray *compactArray = [array cu_compactArray];
//=> ["0", "1", "2", {"one": "1", "two": "2", "three": "3"}, "4"]


動作の仕組みは、まず与えられたNSArrayかNSDictionaryの最初の階層についてだけ、NSNullをチェックして取り除きます。
ここで、チェックされるのはあくまでもネストした最初の階層についてだけです。

- (instancetype)initWithObjects:(const id [])objects count:(NSUInteger)cnt
{
    self = [super init];
    if (self) {
        NSNull *nul = [NSNull null];
        NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:cnt];
        for (NSUInteger i = 0; i < cnt; i++) {
            id object = objects[i];
            if (object && object != nul) {
                [mutableArray addObject:object];
            }
        }
        _original = mutableArray;
        
    }
    return self;
}


そして、元のNSArrayかNSDictionaryを保持して同様の振る舞いをしつつ、実際にネストしたNSArray/NSDictionaryにアクセスがあったときに、適宜同じようにNSNullを削除したラッパーを返すことで、最終的にすべてのネストされた要素からNSNullが取り除かれるという結果になります。

#pragma mark - primitive instance methods

- (NSUInteger)count
{
    return self.original.count;
}
- (id)objectAtIndex:(NSUInteger)index
{
    id object = self.original[index];
    if ([object isKindOfClass:[CUCompactArray class]] || [object isKindOfClass:[CUCompactDictionary class]]) {
        return object;
    }
    if ([object isKindOfClass:[NSArray class]]) {
        return [CUCompactArray arrayWithArray:object];
    }
    if ([object isKindOfClass:[NSDictionary class]]) {
        return [CUCompactDictionary dictionaryWithDictionary:object];
    }
    return object;
}


このようにすることで、最初にすべての要素をチェックする方法だとNSNullがたとえ1つも含まれていなかったり、その値にアクセスすることがなくても、同じだけ処理時間がかかっていたのですが、実際にアクセスのあったタイミングで必要なところだけ処理することで、パフォーマンスに対するペナルティは非常に小さくなります。

けっこうおもしろいアイデアかなと思っているのですがいかがでしょうか?

NSDateFormatterのパフォーマンスの話 #potatotips

クックパッド主催の第4回potatotipsでiOSのtipsとして日付のフォーマットをするときのパフォーマンスの話をしました。

きっかけ

きっかけは何気なくgistを眺めていたときに見つけたこれです。 ↓
Compare the date parsing performance of NSDateFormatter against SQLite's date parser for parsing an iOS 8601 date.

NSDateFormatter took  108.163 seconds
strptime_l took        21.656 seconds
sqlite3 took            7.096 seconds


そうそう、NSDateFomatterはけっこう遅いからフォーマットが固定だったりロケール関係ないときはstrptime_l使うよね、アップルのドキュメントにも載ってるし……

For date and times in a fixed, unlocalized format, that are always guaranteed to use the same calendar, it may sometimes be easier and more efficient to use the standard C library functions strptime_l and strftime_l.

Consider Unix Functions for Fixed-Format, Unlocalized Dates


と軽くスルーするところだったんですが、ちょっと気になる結果がありました。

sqlite3 took 7.096 seconds

sqlite3ってなんや!? しかも超速い!?

計測結果

ホンマかいな、ということで気になったので実際に試してみました。
試したコードは記事の最後に載せています。
先のgistに載っている結果は、100万件の日付の変換で(おそらく)シミュレータで実行した結果だと思いますが、私は実際のデバイスでやってみました。
使用したデバイスは、iPhone 4です。なぜiPhone 4で試したかというと、iOS 7の動くもっとも遅いデバイスで、たいていの現場でベンチマークとして(iPhone 4でそこそこ動けばOKみたいな)使われてるであろうという理由です。
件数は10,000件です。(100万件だとすごい時間かかるので。)

あと、CFDateFormatterRefもついでに測りました。

String => Date
(1回目)
NSDateFormatter     3.61784  seconds
CFDateFormatterRef  3.30721  seconds
strptime_l          0.655574 seconds
sqlite3             0.385894 seconds

(2回目)
NSDateFormatter     3.45504  seconds
CFDateFormatterRef  3.13984  seconds
strptime_l          0.654872 seconds
sqlite3             0.396366 seconds

(3回目)
NSDateFormatter     3.90896  seconds
CFDateFormatterRef  3.29913  seconds
strptime_l          0.67286  seconds
sqlite3             0.402761 seconds


同様の結果になりました。
NSDateFormatterとCFDateFormatterRefが一番遅く、strptime_lがそれより5〜6倍くらい速くて、sqlite3はさらに速いです。


NSDateFormatterは他の2つより複雑なことができるとはいえ、ちょっと遅すぎるような気がします。
今回測ったAPIはほぼすべてコードが公開されている(NSDateFormatterは無いけどCFDateFormatterRefは公開されている)ので時間があれば読んでみようかなと思います。

CFDateFormatter.c
strptime.c
SQLite Download Page

実際の使い分けについて

さて、それでは実際のケースでは常にNSDateFormatterを避けるべきかというと、それほど神経質になる必要はないと思います。
よくある例としてUITableViewCellに日付を表示する例を考えます。


↑ このような構成の画面はよくあると思いますが、1つのセルに日付は1つか2つの場合がほとんど(作成日、更新日)で、内部的に別の日付を使っている(並び替えなど)などがあったとしても多くて3つか4つでしょう。
その場合、NSDateFormatterを使ったとしても、1つの日付にかかる時間はiPhone 4で0.0004秒程度なので、60fpsの1フレームの時間が0.017秒くらいと考えると、日付処理だけが原因でスクロールがカクカクしたりすることは無いでしょう。


それよりは、日付表示などは国や地域によっても異なりますし、最近は相対表示がよく使われたりするなど、変わりやすいところなので、変更しやすかったり、他の人が読みやすいという点を優先したほうがいいと思います。

NSDateFormatterを避けたほうが良い場合

では積極的にNSDateFormatterを避けたほうが良いのはどのような場合かというと、ネットワーク系や特定のAPIを処理するライブラリだと思います。

例えば、下記のTwitter APIを使用して取得した200件JSONだと、私の場合512個の日付が含まれていました。

GET https://api.twitter.com/1.1/statuses/user_timeline.json?count=200&include_entities=true

Twitter APIの日付は"Thu Feb 13 04:00:25 +0000 2014"という形式なので、先ほど計測したものとは形が違いますが、
NSDateFormatterで512件の日付を処理するのに0.36秒ほどかかりました。(ちなみに先ほどの計測で使用した形式の日付なら512件で約0.2秒)
strptime_lで0.06秒です。

普通は通信とともにサブスレッドに処理を逃がすので、UIが固まることはないですが、初期表示がだいぶ遅くなってしまうことになるので、こういった場合はstrptime_lなどを使っていったほうが良さそうです。
特にAPIから返ってくる日付フォーマットがコロコロ変わることは普通なく、タイムゾーンはたいていはUTC固定なので、フォーマットを決め打ちできるので適用しやすいです。


参考事例としてもう一つ、MKNetworkKitに来たPull Requestを紹介します。
Replaced `NSDateFormatter` with faster `strptime_l` and `strftime_l` and fixed OS X compile errors. by Bo98 · Pull Request #230 · MugunthKumar/MKNetworkKit · GitHub

NSDateFormatterの処理は遅いのでstrptime_lと、strftime_lを使うように変更する、という内容です。


このような日付の処理が定型的でかつほぼ毎回つかわれるようなライブラリを作る場合は、効果が大きいので検討する価値は大いにあります。

おまけ: iPhone 5sで実行した結果 (10,000件)

参考までに、iPhone 5sで実行したときの結果も載せておきます。
iPhone 4で3秒以上かかっている処理が1秒かからずに終わっています。
次元の違う速さですね。

String => Date
(1回目)
NSDateFormatter     0.588108  seconds
CFDateFormatterRef  0.525327  seconds
strptime_l          0.128674  seconds
sqlite3             0.0242668 seconds

(2回目)
NSDateFormatter     0.57438   seconds
CFDateFormatterRef  0.523905  seconds
strptime_l          0.124121  seconds
sqlite3             0.0244989 seconds

(3回目)
NSDateFormatter     0.572262  seconds
CFDateFormatterRef  0.518393  seconds
strptime_l          0.119203  seconds
sqlite3             0.0247271 seconds

計測に使用したコード

#import "ViewController.h"

#import <mach/mach_time.h>
#import <time.h>
#import <xlocale.h>
#import "sqlite3.h"

typedef int64_t timestamp;

NSUInteger randomNumberInRange(NSUInteger start, NSUInteger end);

// Create a sample date using the ISO-8601 format.
// 2013-04-23T16:29:05Z
NSString *generateSampleDateString();
NSDate *generateSampleDate();

// Create an array of <count> dates in the ISO-8601 format.
NSArray *generateSampleDateStrings(NSUInteger count);

// Parse all given dates using NSDateFormatter
void parseDatesUsingNSDateFormatter(NSArray *dates);
void formatDatesUsingNSDateFormatter(NSArray *dates);

// Parse all given dates using strptime_l
void parseDatesUsingStrptime(NSArray *dates);
void formatDatesUsingStrptime(NSArray *dates);

// Parse all given dates using SQLite's strftime function
void parseStringToDateUsingSQLite(NSArray *dates);
void formatStringToDateUsingSQLite(NSArray *dates);

NSDate *parseDate(NSString *str);

static NSDateFormatter *dateFormatter = nil;
static CFDateFormatterRef dateFormatterRef = NULL;

NSArray *generateSampleDates(NSUInteger count)
{
    NSMutableArray *dates = [NSMutableArray array];
    
    for (int i = 0; i < count; i++) {
        [dates addObject:generateSampleDate()];
    }
    
    return dates;
}

NSArray *generateSampleDateStrings(NSUInteger count)
{
    NSMutableArray *dates = [NSMutableArray array];
    
    for (int i = 0; i < count; i++) {
        [dates addObject:generateSampleDateString()];
    }
    
    return dates;
}

NSString *generateSampleDateString()
{
    NSUInteger year = randomNumberInRange(1980, 2013);
    NSUInteger month = randomNumberInRange(1, 12);
    NSUInteger date = randomNumberInRange(1, 28);
    NSUInteger hour = randomNumberInRange(0, 23);
    NSUInteger minute = randomNumberInRange(0, 59);
    NSUInteger second = randomNumberInRange(0, 59);
    
    NSString *dateString = [NSString stringWithFormat:@"%lu-%02lu-%02luT%02lu:%02lu:%02luZ",
                            (unsigned long)year,
                            (unsigned long)month,
                            (unsigned long)date,
                            (unsigned long)hour,
                            (unsigned long)minute,
                            (unsigned long)second
                            ];
    
    return dateString;
}

NSDate *generateSampleDate()
{
    return parseDate(generateSampleDateString());
}

#pragma mark -

void parseDatesUsingNSDateFormatter(NSArray *dates)
{
    if (dateFormatter == nil) {
        dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"];
        [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
    }
    
    for (NSString *dateString in dates) {
        NSDate *date = [dateFormatter dateFromString:dateString];
    }
}

#pragma mark -

void parseDatesUsingCFDateFormatter(NSArray *dates)
{
    if (dateFormatterRef == NULL) {
        dateFormatterRef = CFDateFormatterCreate(NULL, NULL, kCFDateFormatterNoStyle, kCFDateFormatterNoStyle);
        CFDateFormatterSetFormat(dateFormatterRef, CFSTR("yyyy-MM-dd'T'HH:mm:ss'Z'"));
        CFTimeZoneRef timeZone = CFTimeZoneCreateWithTimeIntervalFromGMT(NULL, 0);
        CFDateFormatterSetProperty(dateFormatterRef, kCFDateFormatterTimeZone, timeZone);
    }
    
    for (NSString *dateString in dates) {
        CFDateRef date = CFDateFormatterCreateDateFromString(NULL, dateFormatterRef, (__bridge CFStringRef)dateString, NULL);
    }
}

#pragma mark -

void parseDatesUsingStrptime(NSArray *dates)
{
    struct tm  sometime;
    const char *formatString = "%Y-%m-%dT%H:%M:%SZ";
    for (NSString *dateString in dates) {
        strptime_l(dateString.UTF8String, formatString, &sometime, NULL);
        NSDate *date = [NSDate dateWithTimeIntervalSince1970: timegm(&sometime)];
    }
}

#pragma mark -

void parseDatesUsingSQLite(NSArray *dates)
{
    sqlite3 *db = NULL;
    sqlite3_open(":memory:", &db);
    
    sqlite3_stmt *statement = NULL;
    sqlite3_prepare_v2(db, "SELECT strftime('%s', ?);", -1, &statement, NULL);
    
    for (NSString *dateString in dates) {
        sqlite3_bind_text(statement, 1, dateString.UTF8String, -1, SQLITE_STATIC);
        sqlite3_step(statement);
        timestamp value = sqlite3_column_int64(statement, 0);
        NSDate *date = [NSDate dateWithTimeIntervalSince1970:value];
        
        sqlite3_clear_bindings(statement);
        sqlite3_reset(statement);
    }
}

#pragma mark -

NSDate *parseDate(NSString *str)
{
    struct tm  sometime;
    const char *formatString = "%Y-%m-%dT%H:%M:%SZ";
    
    strptime_l(str.UTF8String, formatString, &sometime, NULL);
    NSDate *date = [NSDate dateWithTimeIntervalSince1970: timegm(&sometime)];
    return date;
}

NSUInteger randomNumberInRange(NSUInteger start, NSUInteger end)
{
    NSUInteger span = end - start;
    return start + arc4random_uniform(span);
}

double MachTimeToSecs(uint64_t time)
{
    mach_timebase_info_data_t timebase;
    mach_timebase_info(&timebase);
    return (double)time * (double)timebase.numer / (double)timebase.denom / 1e9;
}

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    printf("%s\n", [[UIDevice currentDevice] name].UTF8String);
    
    const NSUInteger count = 10000;
    
    printf("%lu count\n", (unsigned long)count);
    
    NSArray *dates;
    
    uint64_t begin;
    uint64_t end;
    
    printf("String => Date\n");
    dates = generateSampleDateStrings(count);
    
    begin = mach_absolute_time();
    parseDatesUsingNSDateFormatter(dates);
    end = mach_absolute_time();
    
    printf("NSDateFormatter     %g seconds\n", MachTimeToSecs(end - begin));
    
    begin = mach_absolute_time();
    parseDatesUsingCFDateFormatter(dates);
    end = mach_absolute_time();
    
    printf("CFDateFormatterRef  %g seconds\n", MachTimeToSecs(end - begin));
    
    begin = mach_absolute_time();
    parseDatesUsingStrptime(dates);
    end = mach_absolute_time();
    
    printf("strptime_l          %g seconds\n", MachTimeToSecs(end - begin));
    
    begin = mach_absolute_time();
    parseDatesUsingSQLite(dates);
    end = mach_absolute_time();
    
    printf("sqlite3             %g seconds\n", MachTimeToSecs(end - begin));
    
}

@end

CocoaPodsで導入しているライブラリのライセンス表記を自動的に作成する

CocoaPodsを利用している場合は、PodsディレクトリにPods/Pods-acknowledgements.plistまたはPods/Pods-acknowledgements.markdownが自動的に作成されていますので、それを利用して使用しているライブラリのライセンス表記を自動化できます。


一番簡単なのは設定アプリに項目を設けて表示することです。アプリケーションに画面を追加するわけではないのでお手軽です。


CocoaPodsのWikiにも同様の方法が載っています。


ライセンス表記は使用しているライブラリの構成が変わったときにだけ更新されればいいので、PodFileのpost_installフックで作成されるようにします。
これで`pod install`あるいは`pod update`したタイミングで自動的に更新されます。

post_install do | installer |
  require 'fileutils'
  FileUtils.cp_r('Pods/Pods-acknowledgements.plist', 'Ubiregi2/Settings.bundle/Acknowledgements.plist', :remove_destination => true)
end


アプリケーションのほうにはあらかじめ、入れ物としてのSettings.bundleをプロジェクトに用意しておきます。
最低限、下記のような構成になっていればいいです。


Acknowledgements.plistはそのまま設定アプリで表示できるように作られているので、Child PaneでAcknowledgements.plistを呼び出す設定にしておくだけです。


iPadの設定アプリで実際に表示したものが下記になります。

ユビレジのiPadアプリのCI環境をJenkinsからTravis CIに移行したときのまとめ


こちらの記事について、最新のTravis CIの環境(2014/4/15)ではコード署名に失敗する問題があります。
その問題の修正については下記の記事にまとめました。
Travis CIでipaを作るときのCode Signが失敗するのを修正したメモ - 24/7 twenty-four seven


実際は完全に移行したわけではなくて、Travis CIの有料プラン(プライベートリポジトリが使える)のフリートライアルを試しているところなのですが、しばらくはTravis CIでCIを動かすことにしたので、そのときの設定などをまとめます。


もともとは社内のサーバでJenkinsをホストしていて、それがダメということは全然ないのですが、社内でサーバをメンテナンスするのも面倒だし、ビルドスクリプトとかをポータブルな状態にしておくのは手元でサクッと実行できたりいろいろ都合が良さそうだと思い、試しにやってみることにしました。


Travis CIを選んだのは個人のOSSで使っていて慣れてるからと、Xcodeのビルド環境をホストしてくれるCIサービスの選択肢はそれほどないからです。
あと昨日知ったのですがCloudBeesiOS/MacのCIをサポートしてるということなので、こちらも試してみようかなと考えています。


やりたいことは

  • Githubにプッシュしたらそのタイミングでビルド&テストを実行
  • テストが成功したらTestFlightとCrittercismにアップロード
  • Pull Requestに対してもテストをしたい

の3点です。


副産物としてApple Developer Centerから最新のProvisioning Profileを自動的にダウンロードして使用する仕組みを作ったので、デバイスを追加したときに合わせてCI環境のProvisioning Profileをメンテナンスしなくてよくなりました。
ただ、この問題はエンタープライズのDeveloper Program(¥24,800/年)を購入するほうがスマートに解決できます(iOS Developer Enterprise ProgramのProvisioning Profileはデバイスの制限がなくなるので)。

ビルドスクリプトなど

ビルドスクリプトはrakeで書きました。
↓ Rakefileの全体はこちら

↓ .travis.ymlはこちら


Rakefileの設計は下記を参考にしました。


TestFlightへのアップロードは下記を参考にしました。


Crittercismへのアップロードは下記を参考にしました。


Apple Developer CenterからProvisioning Profileをダウンロードするスクリプトは下記を利用しました。


長いので要点だけ解説します。

ユニットテストの実行

テストの実行は54行目からの下記の部分です。

task :test do |t|
  DESTINATIONS.each do |destination|
    options = {
      sdk: 'iphonesimulator',
      workspace: WORKSPACE_DIR,
      scheme: SCHEME,
      configuration: 'Debug',
      destination: destination
    }
    options = join_option(options: options, prefix: "-", seperator: " ")
    sh "xcodebuild #{options} test | xcpretty -tc" do |ok, res|
      fail unless ok
    end
  end
end


コマンドラインで

$ rake test

とすると、下記のようなコマンドが実行されます。
DESTINATIONSにはとりあえずすべてのOSバージョンが書いてあるので、6.0, 6.1, 7.0, 7.0 (64 bit)のそれぞれのiPadシミュレータでテストが実行されます。

xcodebuild -sdk "iphonesimulator" -workspace "/Users/travis/build/ubiregiinc/ubiregi-client/Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Debug" -destination "name=iPad,OS=6.0" test | xcpretty -tc

アプリケーションのビルド

テストが成功したらTestFlightにAdHoc版としてアップロードするので、アプリケーションをビルドしてipaファイルを作ります。
ビルドは70行目〜、ipaファイルを作る部分にあたるのは147行目からです。

テストの場合とほぼ同じですが、後の工程でビルドされたappパッケージからipaファイルを作ったり、dSYMファイルをzipにしたりするので、OBJROOTとSYMROOTを指定してビルド先のディレクトリを扱いやすくしています。

また、ここでコード署名をしておかないと、ipaを作るときにAdHoc用のProvisioning Profileを埋め込むことができなかったので、署名だけしています。
Provisioning Profileまで指定すると今度はTestFlightにアップロードするときに失敗したのでProvisioning Profileの指定は空にしています。このへんは何度か設定を変えながらやってみたらできたという感じで、よくわかりません。

desc "Build application"
task :build do |t|
  options = {
    sdk: SDK,
    workspace: WORKSPACE_DIR,
    scheme: SCHEME,
    configuration: CONFIGURATION
  }
  options = join_option(options: options, prefix: "-", seperator: " ")
  settings = {
    OBJROOT: BUILD_DIR,
    SYMROOT: BUILD_DIR,
    CODE_SIGN_IDENTITY: DEVELOPER_NAME,
    PROVISIONING_PROFILE: ""
  }
  settings = join_option(options: settings, prefix: "", seperator: "=")
  sh "xcodebuild #{options} #{settings} clean build | xcpretty -c"
end

desc "Create .ipa file"
task :archive do |t|
  archive
end


トライ&エラーの内容を整理すると、

  • CODE_SIGN_IDENTITYを指定してPROVISIONING_PROFILEを空にすると下記のエラーでビルドが失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="iPhone Distribution: Ubiregi Inc. (Y7522692LT)" PROVISIONING_PROFILE="" clean build
Code Sign error: No matching codesigning identity found: No codesigning identities (i.e. certificate and private key pairs) matching “iPhone Distribution: Ubiregi Inc. (Y7522692LT)” were found.

CodeSign error: code signing is required for product type 'Application' in SDK 'iOS 7.0'
  • CODE_SIGN_IDENTITYとPROVISIONING_PROFILEを両方とも空にすると下記のエラーでビルドが失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="" PROVISIONING_PROFILE="" clean build
CodeSign error: code signing is required for product type 'Application' in SDK 'iOS 7.0'
  • `CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO`と指定して(ビルド時にコード署名しない)、PackageApplicationコマンドにsignオプションとembedオプションを指定すると、ビルドは成功するが下記のエラーでipaファイルの作成が失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO clean build  
xcrun -sdk iphoneos PackageApplication -v "./build/Release-iphoneos/Ubiregi2.app" -o "./build/ubiregiinc/ubiregi-client/build/Ubiregi2.ipa" -sign "iPhone Distribution: Ubiregi Inc. (Y7522692LT)" -embed "$HOME/Library/MobileDevice/Provisioning Profiles/Ubiregi AdHoc.mobileprovision"
error: Failed to read entitlements from '/var/folders/9x/jfhnrxj531v5427xj48tsdd00000gn/T/S6ueKkDErz/Payload/Ubiregi2.app'
  • `CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO`と指定して(ビルド時にコード署名しない)、PackageApplicationコマンドにembedオプションだけを指定すると、ipaファイルの作成は成功するが下記のエラーでTestFlightへのアップロードが失敗しました。
xcodebuild -sdk "iphoneos" -workspace "Ubiregi2.xcworkspace" -scheme "Ubiregi2-Release" -configuration "Release" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO clean build  
xcrun -sdk iphoneos PackageApplication -v "./build/Release-iphoneos/Ubiregi2.app" -o "./build/ubiregiinc/ubiregi-client/build/Ubiregi2.ipa" -embed "$HOME/Library/MobileDevice/Provisioning Profiles/Ubiregi AdHoc.mobileprovision"
'Invalid IPA: missing embedded provisioning profile. Are you sure this is an ad hoc?'


上記の結果から考えて、ビルド時にコード署名をして、アーカイブ時にProvisioning Profileを埋め込むという手順でうまくいきました。

ipaファイルの作成

ビルドされたappパッケージからipaファイルを作ります。
「アプリケーションのビルド」のところで述べたように、ここではTestFlightにアップロードするためのAdHoc用のProvisioning Profileを指定します。

def archive
  options = {
    o: IPA_FILE,
    embed: PROVISIONING_PROFILE
  }
  options = join_option(options: options, prefix: "-", seperator: " ")
  sh "xcrun -sdk #{SDK} PackageApplication #{APP_FILE} #{options}"
end


Provisioning Profileを埋め込むためには証明書と秘密鍵(cerファイルやp12ファイル)が必要なので、それらはキーチェーンからエクスポートして、あらかじめリポジトリに含めておきます。

apple.cerはキーチェーンに登録されている「Apple Worldwide Developer Relations Certification Authority」、dist.cerとdist.p12は今回はUbiregi Inc.の「iPhone Distribution」の証明書と秘密鍵です。


そして、上記のコマンドの前に、一時的なキーチェーンを作成してこれらの各種証明書を追加します。
また、Travis CIで実行する分には後始末は必要ないのですが、手元で実行するときには同じ名前のキーチェーンを作ろうとすると失敗するので、コマンドの実行後には先ほど作ったキーチェーンを削除します。

↓ こちらを参考にしました。
Deploy an iOS app to testflight using Travis CI

def add_certificates
  sh "security create-keychain -p travis #{KEYCHAIN_NAME}"
  sh "security import ./certificates/apple.cer -k ~/Library/Keychains/#{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.cer -k ~/Library/Keychains/#{KEYCHAIN_NAME} -T /usr/bin/codesign"
  sh "security import ./certificates/dist.p12 -k ~/Library/Keychains/#{KEYCHAIN_NAME} -P #{KEY_PASSWORD} -T /usr/bin/codesign"
  sh "mkdir -p \"#{PROFILE_DIR}\""
  sh "cp \"./profiles/#{PROFILE_NAME}.mobileprovision\" \"#{PROFILE_DIR}\""
end

def remove_certificates
  sh "security delete-keychain #{KEYCHAIN_NAME}"
end

TestFlightとCrittercismにアップロード

ipaファイルをTestFlightに、dSYMファイルをCrittercismにアップロードします。
112行目からのタスクです。

task :crittercism => [DSYM_ZIP_FILE] do |t|
  fields = {
    dsym: "@#{DSYM_ZIP_FILE}",
    key: API_KEY,
  }
  fields = join_option(options: fields, prefix: "-F ", seperator: "=")
  sh "curl -sL -w \"%{http_code} %{url_effective}\\n\" https://api.crittercism.com/api_beta/dsym/#{APP_ID} #{fields} -o /dev/null"
end

desc "Upload IPA file and dSYM file to TestFlight and notify testers"
task :testflight => ["version:set_build_version", IPA_FILE, :crittercism] do |t|
  if ENV['TRAVIS_PULL_REQUEST'] != "false"
    puts "This is a pull request. No deployment will be done."
    next
  end
  if ENV['TRAVIS_BRANCH'] != "master"
    puts "Testing on a branch other than master. No deployment will be done."
    next
  end

  release_date = DateTime.now.strftime("%Y/%m/%d %H:%M:%S")
  release_notes = "Build: #{InfoPlist.marketing_and_build_version}\nUploaded: #{release_date}"

  fields = {
    file: "@#{IPA_FILE}",
    api_token: API_TOKEN,
    team_token: TEAM_TOKEN,
    notes: release_notes,
    notify: true,
    distribution_lists: DISTRIBUTION_LISTS
  }
  fields = join_option(options: fields, prefix: "-F ", seperator: "=")
  sh "curl -sL -w \"%{http_code} %{url_effective}\\n\" http://testflightapp.com/api/builds.json #{fields} -o /dev/null"
end


と、こんな感じでGithubにプッシュすると自動的にテストが実行されて、(masterブランチへのPushなら)TestFlightやCrittercismへのアップロードもやってくれるようになりました。

ビルドスクリプトはそれぞれrakeのタスクとして作ってあるので、手元でも簡単に

rake test

とテストを実行したり、

rake testflight

と簡単にTestFlightにアップロードすることができるようになりました。

注意点

Travis CIのデフォルトのOS Xの環境ではなぜか`pod install`がSegmentation faultで失敗しました。

$ pod install

Analyzing dependencies

/Users/travis/.rvm/gems/ruby-2.0.0-p247/gems/xcodeproj-0.14.1/ext/xcodeproj/xcodeproj_ext.bundle: [BUG] Segmentation fault

ruby 1.8.7 (2012-02-08 patchlevel 358) [universal-darwin12.0]


同様のエラー報告がIssueに登録されていて対処法はとりあえず`rvm: 1.9.3`としてrubyのバージョンを1.9.3に固定するとのことでした。
iOS - Pod Install Erroring Out · Issue #1657 · travis-ci/travis-ci · GitHub


1.9.3だとキーワード引数が使えないとかrakeを書くときにちょっと制限ができちゃいますが、とりあえずはそうしました。早く修正されるといいですね。

おまけ1:Provisioning Profileの自動ダウンロード

Jenkinsを使っているときからもあった問題に、Provisioning Profileのメンテナンスがあります。
デバイスを追加があったときに、CIのマシンが参照しているProvisioning Profile(リポジトリに含めるなどする)も更新する必要があるからです。

今回はそれも何とかしたいと思って、CIの実行時に毎回最新のものをダウンロードするようにしました。
177行目からのタスクになります。

namespace :profile do
  desc "Download provisioning profiles from Apple Developer Center"
  task :download do
    ruby "./scripts/apple_dev_center.rb -C ./scripts/apple_dev_center.config -d profiles -O /dev/null"
  end
end


下記のスクリプトを実行すると、AppleのWebサイトに接続して、Provisioning Profileをダウンロードしてprofilesディレクトリに保存します。
ipaファイルを作るときにはこのProvisioning Profileが使用されます。

ruby "./scripts/apple_dev_center.rb -C ./scripts/apple_dev_center.config -d profiles -O /dev/null"


Apple Developer CenterからProvisioning Profileをダウンロードするスクリプトは下記を利用しました。
lacostej/apple-dev · GitHub


上記のスクリプトはgemの体裁は整っているのですが、RubyGemsとしては登録されておらず、READMEに載っている使い方もbinのスクリプトを直接利用するように書いてあったので、gemとしてインストールしても使い方がよくわからなかったので、スクリプトごとリポジトリに含めて利用するようにしました。

おまけ2:バージョン番号とビルド番号の設定

TestFlightにアップロードするときにどのビルドかを区別するためにgitのハッシュがビルド番号に設定されるようにします。
257行目です。

desc "Sets build version to last git commit (happens on each build)"
task :set_build_version do
  rev = `git rev-parse --short HEAD`.strip
  puts "Setting build version to: #{rev}"
  InfoPlist.build_version = rev
end

ユビレジではバージョン番号のポリシーとして、「X.XX」という形式で、マイナー番号が偶数がリリース版、奇数ならベータ版という決まりにしているので、申請したらとりあえずマイナーを1つ上げるなどの操作をよくするので、ついでに手元でバージョン番号を簡単に操作できるようにしました。

例えば現在のバージョンが2.56とすると、

rake version:bump:minor

とすると、2.57になります。

rake version:bump:release

とすると、現在のバージョンが2.56でも2.57でも2.58になります。

このとき、ビルド番号はgitのハッシュではなくて、番号っぽくコミット数になるようにしています。
アプリ内にバージョンを表示しているのでこちらのほうが見栄えがなんとなくいいかなと思ってそうしています。

下記を参考にしました。
How to Automatically Update Xcode Build Numbers from Git | Objective C#

Conference with DevelopersでJavaScriptCore.frameworkとObjective-C Runtime APIについて話しました

年に1度のiOSデベロッパーのイベント「Conference with Developers」で話をしました。
JavaScriptCore.frameworkとObjective-C のRuntime APIという非常にマニアックな内容でしたが、まあまあわかるように伝えられたかなと思います。


話の内容は主に以下の3点です。

  • JavaScriptCore.frameworkの概要と使い方
  • Objective-C Runtime APIの活用方法
  • JavaScriptBridgeの紹介


伝えきれなかったことを補足しますと、JavaScriptBridgeはフルスクラッチで最初から最後まですべてJavaScriptでiOSアプリを書く、という用途のために作られたのではありません。


例としてそういうものを示しているのは、単に例は極端なほうがわかりやすいというだけの理由です。


どちらかというと、週ごとに変わるキャンペーン用画面とか、メンテナンス時に一時的に表示される画面など、単純に変更が頻繁なところや、一時的に表示する画面だけど様々なケースが考えられる、というところに部分的に利用するのが適していると思います。


(コンパイル時ではなく)実行時に文字列を評価して実行されるという強力な特性を「気軽に」利用できるという点が大きなメリットです。


というのも、JavaScriptCore.frameworkでJSからObjective-Cを扱う場合は、JSExportというJSに公開するインターフェースをクラスごとに準備する必要があるのと、delegateメソッドやUIViewControllerのviewDidLoadなどのフレームワークから呼ばれるメソッドに応答することができないという制限があるので、通常はコストメリットが割に合わないためです。
(使うクラスすべてのJSExportを用意するのは非常に面倒、delegateその他に応答できないとUIKitをまともに利用できない)


JavaScriptBridgeにはiOS SDKに含まれるほぼすべてのクラスに対してJSExportプロトコルが準備されていて、Runtime APIで適宜JSのメソッドにディスパッチすることでdelegateメソッドへの応答を可能にしているので、このライブラリを導入するだけであとはおもむろにJSで書き始めることができるようになります。


この手軽さこそが唯一最大の利点であると考えています。


あと、懇親会で話してるときに、子ども向けのプログラミング環境をiPadで提供する、というのはとても夢があって実用的な例だと思いました。
(やろうと思えば)Twitterクライアントくらいは作れちゃうわけですし、書いたそばから実行して動かせるというのはとても楽しいし、そもそも現状iOSでiOS Appを書ける環境はほぼないので普通に便利、と実現すればとてもおもしろい試みじゃないかなと思います。

資料

ビデオ(前半)



Video streaming by Ustream

ビデオ(後半)



Video streaming by Ustream

コマンドライン引数(Launch arguments)は思ったより簡単に使える



iOS/AndroidのTips共有会potatotipsでiOSの実行時引数は思ってるより簡単ベンリに扱えるんだよって話をしました。


potatotips (iOS/Android開発Tips共有会) 第3回



↑ 起動時のオプション引数はiOSだと上図のようにXcodeのスキーマで指定します。

int main(int argc, char * argv[])

↑ そして普通のUnixプログラムと同様にmain関数のargcに引数の個数が、argvに文字列の配列で入ってきます。
でもGUIの無いコマンドラインのユーティリティプログラムならともかく、iOSアプリでargvをParseして何かするとか面倒なだけだと思っていませんか?


詳しいことはスライドに書いたので端的に言うと、ある規則にしたがうとこの引数は自動的にParseされてNSUserDefaultsに格納されます。
つまりアプリケーションのどこでも特定のキーで値を取得することができるようになります。

ある規則とは次の2つです。

  • キーとなる値は`-`(ハイフン)で始まる
  • 2語以上の値はクオートで囲む
-key1 value1 -key2 'foo bar'

↑ 例えば上のように書くと、NSUserDefaultsに"key1", "key2"にそれぞれ"value1", "foo bar"という値が入ります。


また、この挙動を利用して既存の値を一時的に上書きすることもできます。

-AppleLanguages (en)

↑ 上記のように起動時引数を指定すると一時的に英語環境で起動されるのはよく知られたデバッグのTipsですが、これはNSUserDefaultsの`AppleLanguages`というキーの値を上書きしているのでそのような挙動になります。


起動時引数の値はNSUserDefaultsに永続化されるわけではありません。あくまでも起動してから終了するまでの一時的なものです。
そのような挙動はNSUserDefaultsの値が「ドメイン」という階層を持っていて、起動時引数はNSArgumentDomainという階層に格納され、その階層が一番最初に検索されるので、同じキーがあれば最初にヒットするので結果的に上書きされたことになるという仕組みです。


そして、意外に知られていないと思われるのが、ArrayやDictionaryのデータ構造を指定できるということです。


例えばArrayは次のようになります。

-arrayArg '( "foo", "bar", "baz" )'


Dictionaryはこうです。

-dictArg '{ "foo" = "bar"; "baz" = "qux"; }'


記法でわかるとおり、プロパティリストの表現形式です。
NSUserDefaultsの永続化先はプロパティリストなので納得ですね。


ArrayとDictionaryの組み合わせで、もっと複雑なデータを渡すこともできます(だいぶややこしくなりますが)。

上記のプロパティリストのフォーマットは古い記法ですが、今どきのXML形式も使えます。

-xmlArg “<dict><key>foo</key><string>bar</string><key>baz</key><string>qux</string></dict>"


起動時のタイミングでXMLのParseがなされているとか、それは本当に必要なのかという気がしますがとにかくそういう仕組みです。


これで、なぜ下のように書くと英語環境になるのかということのトリックがわかります。

-AppleLanguages (en)

第一引数の`-AppleLanguages`はハイフンで始まっているのでNSUserDefaultsにAppleLanguagesというキーで格納されます。
第二引数の`(en)`はプロパティリストの配列です。


NSUserDefaultsにはあらかじめAppleLanguagesというキーで設定で指定した優先順位で言語の配列が格納されています。

NSLog(@"%@", [[NSUserDefaults standardUserDefaults] dictionaryRepresentation]);

=>
{
    ...

    AppleLanguages =     (
        ja,
        en,
        fr,
        ...
    );

    ...

}


その値を上書きするために第一引数はハイフンで始まる必要があり、第二引数にカッコを付けるのは、もともとのAppleLanguagesの値が配列なので配列を渡す必要があり、配列のプロパティリストでの表現形式はカッコで囲む必要があるからです。

One-line fix for UITextView on iOS 7

iOS 7のUITextViewのバグを1行で直す裏ワザ

[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"UIDisableLegacyTextView"];

解説

iOS 7にはバグだらけのUITextViewのほかに_UICompatibilityTextViewというiOS 6以前のUITextView(と思われる)クラスがあり、
メモ(Notes.app)など同様の不具合が再現しない標準アプリは_UICompatibilityTextViewが使われている。


そしておそらくこのフラグによってUITextViewをインスタンス化したときにどちらが使用されるかが変わる。


以下のようにmain.mあたりで設定すると有効になる。

#import <UIKit/UIKit.h>
 
#import "AppDelegate.h"
 
int main(int argc, char * argv[])
{
    @autoreleasepool {
        [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"UIDisableLegacyTextView"];
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

JavaScriptでiOSアプリが書けるライブラリJavaScriptBridgeを公開しました

kishikawakatsumi/JavaScriptBridge · GitHub


前にiOS 7から導入されたJavaScriptCore.frameworkを使ってUIKit標準のクラスを操作する話を書きました。

JavaScriptだけでiOSのUIを書いてみる - 24/7 twenty-four seven


JavaScriptCoreを使ってJavaScriptからObjective-Cのオブジェクトを操作するのは、あらかじめJSExportプロトコルで公開しておく必要があるなど、あまり実用的ではありませんでしたが、このライブラリを導入すうることで、そういった事前準備がすでに済んでいる状態で使いはじめることができます。


また、JavaScriptからObjective-Cのクラスを定義できるなどいくつかの拡張を加えてあり、UIViewControllerのサブクラスで画面を定義したり、デリゲートに応答するオブジェクトを作ったり、UIKitの作法にしたがってアプリケーションを記述することができるようになっています。


使い方を見てもらえればわかりますが、全部JavaScriptで書くことを強制されることはなく、部分的に使用することもできます。
むしろ、ピンポイントで一部の画面だけJavaScriptを使用して書きたい、という場合に力を発揮するのではないかと思います。

応用の方法としては、キャンペーンや期間限定の画面などを増減できるようにしたり、変更可能にしておくといった方法や、プロトタイピングで、再ビルドやアプリケーションを終了することなく、動的に修正を確認したりなどに向いているかと考えています。


まだバージョン0.0.1のベータ版ですが、バグ報告、プルリクエストをお待ちしています。

使い方

もっとも簡単な例は下記になります。

#import <JavaScriptBridge/JavaScriptBridge.h>
...

// Retrieve the prepared context
JSContext *context = [JSBScriptingSupport globalContext];

// Add framework support if needed.
// ('Foundation', 'UIKit', 'QuartzCore' enabled by default.)
[context addScriptingSupport:@"MapKit"];
[context addScriptingSupport:@"MessageUI"];

// Evaluate script
[context evaluateScript:
 @"var window = UIWindow.new();"
 @"window.frame = UIScreen.mainScreen().bounds;"
 @"window.backgroundColor = UIColor.whiteColor();"
 @"window.makeKeyAndVisible();"
];


JSBScriptingSupportから取得したJSContextオブジェクトは標準のUIKitやFoundationのほとんどのクラス群がすでにJSExportプロトコルに適合している状態になっています。

JSContext *context = [JSBScriptingSupport globalContext];


必要に応じてJSExportに適合させるクラスを追加します。
Frameworkごとに名前を指定することでそのFrameworkに属するクラス群がJSExportに適合されます。
例えば下記のようにMapKitを指定すると、MKMapViewなどがJavaScriptから扱えるようになります。

[context addScriptingSupport:@"MapKit"];


必要なことはこれだけです。あとはおもむろにJavaScriptでアプリケーションを書き始めることができます。

// Evaluate script
[context evaluateScript:
 @"var window = UIWindow.new();"
 @"window.frame = UIScreen.mainScreen().bounds;"
 @"window.backgroundColor = UIColor.whiteColor();"
 @"window.makeKeyAndVisible();"
];

文法など

変数宣言

型名を取り除きます。

UILabel *label;
var label;
プロパティ

プロパティはほぼObjective-Cと同様でドットを使います。
Objective-Cではメソッド呼び出しの形式でプロパティにアクセスすることができますが、メソッドによるアクセスは今のところサポートしていません。

UISlider *slider = [[UISlider alloc] initWithFrame:frame];
slider.backgroundColor = [UIColor clearColor];
slider.minimumValue = 0.0;
slider.maximumValue = 100.0;
slider.continuous = YES;
slider.value = 50.0;
var slider = UISlider.alloc().initWithFrame(frame);
slider.backgroundColor = UIColor.clearColor();
slider.minimumValue = 0.0;
slider.maximumValue = 100.0;
slider.continuous = true;
slider.value = 50.0;
メソッド呼び出し

Objective-Cのカッコを使ったメソッド呼び出しは、ドット記法に置き換えます。
パラメータを取るメソッドについては、コロンを取り除き、パラメータのラベルをキャメルケースで連結してメソッド名にします。
そのあと、順番にパラメータを並べます。

UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
var window = UIWindow.alloc().initWithFrame(UIScreen.mainScreen().bounds);
クラス定義

次のように、JSB.defineメソッドを使うとObjective-Cのクラスが作成されます。
第1引数でクラスの宣言、第2引数にハッシュでメソッド名と処理のfunctionオブジェクトを渡します。
これを使うことでUITableViewのデリゲートメソッドに応答したり、UIViewControllerのサブクラスとして画面を作成したりできます。

var MainViewController = JSB.define('MainViewController : UITableViewController <UITableviewDataSource, UITableviewDelegate>', // Declaration
// Instance Method Definitions
{
  viewDidLoad: function() {
    self.navigationItem.title = 'UICatalog';
  },
  tableViewNumberOfRowsInSection: function(tableView, section) {
    return self.menuList.length;
  },
  tableViewCellForRowAtIndexPath: function(tableView, indexPath) {
    var cell = UITableViewCell.alloc().initWithStyleReuseIdentifier(3, 'Cell');
    cell.accessoryType = 1;
    cell.textLabel.text = self.menuList[indexPath.row]['title'];
    cell.detailTextLabel.text = self.menuList[indexPath.row]['explanation'];

    return cell;
  },
  tableViewDidSelectRowAtIndexPath: function(tableView, indexPath) {
    var targetViewController = self.menuList[indexPath.row]['viewController'];
    self.navigationController.pushViewControllerAnimated(targetViewController, true);
  }
});
モジュール

Node.js風の簡単なモジュールシステムを提供しています。
JSB.require('/path/to/module.js') と書くと別のファイルのモジュールが利用可能になります。
モジュールとして公開するにはJSB.exportsにオブジェクトを代入します。

ファイルを分けて書けるのでちょっと大きめのアプリケーションでもJavaScriptだけでけっこう書けます。
↓ このサンプルを見るとよくわかります。
JavaScriptBridge/Examples/UICatalog/UICatalog/js at master · kishikawakatsumi/JavaScriptBridge · GitHub

var ButtonsViewController = JSB.require('buttonsViewController');
var ControlsViewController = JSB.require('controlsViewController');
var WebViewController = JSB.require('webViewController');
var MapViewController = JSB.require('mapViewController');

var MainViewController = JSB.define('MainViewController : UITableViewController', {
  viewDidLoad: function() {
    self.navigationItem.title = 'UICatalog';
    ...
});

JSB.exports = MainViewController;

JavaScriptだけでiOSのUIを書いてみる

この投稿は iOS Advent Calendar 2013 - Qiita の22日目の記事です。

iOS 7から新しく追加されたJavaScriptCore.frameworkを使ってJavaScriptだけでUIを書いてみましょう。

JavaScriptCore.frameworkの基本 (Objective-C -> JavaScript)

まずJavaScriptCore.frameworkの基本的な使い方は次のようになります。

JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"a = 10;"];

JSValue *value = context[@"a"];
NSLog(@"%d", value.toInt32); // => 10

↑ まずJavaScriptの実行環境としてJSContextのインスタンスを作成します。
contextのevaluateScript:メソッドにスクリプトを渡すと実行されます。
そこで作成したオブジェクトはcontextからキーを指定して取り出すことができます。


このようにObjective-CからJavaScriptを呼び出すのは簡単です。

JavaScriptからObjective-Cを使う

JavaScriptからObjective-Cを呼び出すのはさらに少し準備が必要です。
JavaScriptから呼び出せるメソッドをあらかじめJSExportというプロトコルにしたがって公開しておきます。


例えば、JavaScriptでUIWindowのオブジェクトを操作したいという場合は次のようなプロトコルをあらかじめ定義します。

@protocol JSUIWindow <JSExport>

@property (nonatomic) CGRect frame;
@property (nonatomic) UIColor *backgroundColor;

+ (id)new;
- (void)makeKeyAndVisible;

@end


↓ そして上記のプロトコルに適合するUIWindowのサブクラスを定義します。

@interface JSUIWindow : UIWindow <JSUIWindow>

@end

@implementation JSUIWindow

@end


先ほどのJSUIWindowクラスのクラスオブジェクトをJSContextに登録します。

JSContext *context = [[JSContext alloc] init];
context[@"JSUIWindow"] = [JSUIWindow class];


これで、JavaScriptからJSUIWindowのメソッドを呼び出すことができるようになりました。
先ほどの手順でnewメソッドをJavaScriptから使えるように公開してあるのでJavaScriptからJSUIWindowをインスタンス化できます。

JSContext *context = [[JSContext alloc] init];
context[@"JSUIWindow"] = [JSUIWindow class];
[context evaluateScript:@"var window = JSUIWindow.new();"];

JSValue *value = context[@"window"];
NSLog(@"%@", value.toObject); // => <JSUIWindow: 0x8e2ce40; baseClass = UIWindow; frame = (0 0; 0 0); hidden = YES; gestureRecognizers = <NSArray: 0x8e2d3e0>; layer = <UIWindowLayer: 0x8e2cf60>>


ここまでやってみて、少し、いやかなり面倒だと思われたのではないでしょうか。
いちいちサブクラスを定義することなく直接UIKitの標準クラスをJavaScriptから扱えるようにならないものでしょうか。

標準クラスをJSExportに適合させる

実はObjective-CのランタイムAPIを使えば実行時にクラスをプロトコルに適合させることができます。

class_addProtocol([UIWindow class], @protocol(JSUIWindow));

JSContext *context = [[JSContext alloc] init];
context[@"UIWindow"] = [UIWindow class];
[context evaluateScript:@"var window = UIWindow.new();"];

JSValue *value = context[@"window"];
NSLog(@"%@", value.toObject); // => <UIWindow: 0x8b7ea00; frame = (0 0; 0 0); hidden = YES; gestureRecognizers = <NSArray: 0x8b7efa0>; layer = <UIWindowLayer: 0x8b7eb20>>

↑ 上記のコードは実行時にランタイムAPIの`class_addProtocol`を使ってUIWindowクラスをJSUIWindowプロトコルに適合させています。
contextに登録するクラスオブジェクトもUIWindowクラスです。作成されたインスタンスもUIWindowの直接のインスタンスであることがわかります。


ここまでできると、プロトコルの宣言も無くせないだろうかと考えるのですが、結論からいうとそれは不可能でした。
ランタイムAPIを使えば実行時にプロトコルを作成することもできるのですが、そうやって作成したプロトコルをJSContextに登録しても使えないか、クラッシュしてしまいました。

ただ、本家のWebKitのほうにもこの挙動はバグではないかということで報告されているようです。
Bug 122501 – Dynamically generated JSExport protocols added to a class results in a crash


↓ いちおう試したコードを載せておきます。

Class cls = [UIWindow class];
SEL sel = @selector(new);
Method method = class_getClassMethod(cls, sel);
const char *types = method_getTypeEncoding(method);

Protocol *proto = objc_allocateProtocol("JSUIWindow");
protocol_addProtocol(proto, objc_getProtocol("JSExport"));
protocol_addMethodDescription(proto, sel, types, YES, NO);
objc_registerProtocol(proto);

class_addProtocol(cls, proto);

JSContext *context = [[JSContext alloc] init];
context[@"UIWindow"] = cls;
[context evaluateScript:@"var window = UIWindow.new();"];

JSValue *value = context[@"window"];
NSLog(@"%@", value); => undefined


というわけで、現状ではあらかじめプロトコルを定義しておき、実行時に適合させることでSDKから提供されているクラスについてもJavaScriptから扱うことができるようになります。

JavaScriptだけでUIを書いてみる

↓ そのようにして、JavaScriptだけで記述したコードで作られた画面がこちらです。

20131223022848


全体のコードは下記になります。
一番下のapplication:didFinishLaunchingWithOptions:メソッドのところを読むと、JavaScriptだけで画面が書かれてるのがわかります。

引数のあるメソッドをJavaScriptから呼び出すときはコロンを取り除いて、キャメルケースに結合した名前にします。
(`JSExportAs`というマクロを使って別名を定義することもできます。)
またCGRectなど一部の構造体はハッシュを使って`framex = 10;` や `frame = {x: 20, y: 80, width: 200, height: 80};`のように簡単に設定することができるようになっています。


この例ではJavaScriptはハードコーディングしていますが、外部ファイルから読み込むようにしてもいいでしょう。
準備が少し大変ですが、JavaScriptのコンパイルする必要がないといったスクリプト言語の特性をうまく使うと、便利な場合も多いのではないでしょうか。

#import "AppDelegate.h"

@import JavaScriptCore;
@import ObjectiveC;

@protocol JSNSObject <JSExport>

+ (id)new;

@end

@protocol JSUIView <JSExport>

@property (nonatomic) CGRect frame;
@property (nonatomic) UIColor *backgroundColor;

+ (id)new;
- (void)addSubview:(UIView *)view;

@end

@protocol JSUIWindow <JSExport>

@property (nonatomic) CGRect frame;
@property (nonatomic) UIColor *backgroundColor;
@property (nonatomic) UIViewController *rootViewController;

+ (id)new;
- (void)makeKeyAndVisible;

@end

@protocol JSUILabel <JSExport>

@property (nonatomic) CGRect frame;
@property (nonatomic) UIColor *backgroundColor;
@property (nonatomic) UIColor *textColor;
@property (nonatomic) NSString *text;

+ (id)new;
- (void)addSubview:(UIView *)view;
- (void)sizeToFit;

@end

@protocol JSUIScreen <JSExport>

@property (nonatomic, readonly) CGRect bounds;

+ (UIScreen *)mainScreen;
- (void)makeKeyAndVisible;

@end

@protocol JSUIColor <JSExport>

+ (UIColor *)whiteColor;
+ (UIColor *)redColor;
+ (UIColor *)blueColor;

@end

@protocol JSUIViewController <JSExport>

@property (nonatomic) UIView *view;
@property (nonatomic) UINavigationItem *navigationItem;
@property (nonatomic) UITabBarItem *tabBarItem;

+ (id)new;
- (UIView *)view;

@end

@protocol JSUITabBarController <JSExport>

@property(nonatomic, copy) NSArray *viewControllers;

+ (id)new;

@end

@protocol JSUINavigationController <JSExport>

@property (nonatomic) NSArray *viewControllers;

+ (id)new;

@end

@protocol JSUINavigationItem <JSExport>

@property (nonatomic) NSString *title;

@end

@protocol JSUITabBarItem <JSExport>

+ (id)alloc;
- (id)initWithTabBarSystemItem:(UITabBarSystemItem)systemItem tag:(NSInteger)tag;

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    class_addProtocol([NSObject class], @protocol(JSNSObject));
    class_addProtocol([UIView class], @protocol(JSUIView));
    class_addProtocol([UILabel class], @protocol(JSUILabel));
    class_addProtocol([UIWindow class], @protocol(JSUIWindow));
    class_addProtocol([UIViewController class], @protocol(JSUIViewController));
    class_addProtocol([UINavigationController class], @protocol(JSUINavigationController));
    class_addProtocol([UINavigationItem class], @protocol(JSUINavigationItem));
    class_addProtocol([UITabBarController class], @protocol(JSUITabBarController));
    class_addProtocol([UITabBarItem class], @protocol(JSUITabBarItem));
    class_addProtocol([UIScreen class], @protocol(JSUIScreen));
    class_addProtocol([UIColor class], @protocol(JSUIColor));
    
    JSContext *context = [[JSContext alloc] init];
    context[@"NSObject"] = [NSObject class];
    context[@"UIView"] = [UIView class];
    context[@"UILabel"] = [UILabel class];
    context[@"UIWindow"] = [UIWindow class];
    context[@"UIViewController"] = [UIViewController class];
    context[@"UINavigationController"] = [UINavigationController class];
    context[@"JSUINavigationItem"] = [UINavigationItem class];
    context[@"UITabBarController"] = [UITabBarController class];
    context[@"UITabBarItem"] = [UITabBarItem class];
    context[@"UIScreen"] = [UIScreen class];
    context[@"UIColor"] = [UIColor class];
    
    [context evaluateScript:
     @"var window = UIWindow.new();"
     @"window.frame = UIScreen.mainScreen().bounds;"
     @"window.backgroundColor = UIColor.whiteColor();"
     @""
     @"var navigationController1 = UINavigationController.new();"
     @"var viewController1 = UIViewController.new();"
     @"viewController1.navigationItem.title = 'Make UI with JS';"
     @""
     @"var view = UIView.new();"
     @"view.backgroundColor = UIColor.redColor();"
     @"view.frame = {x: 20, y: 80, width: 200, height: 80};"
     @""
     @"var label = UILabel.new();"
     @"label.backgroundColor = UIColor.blueColor();"
     @"label.textColor = UIColor.whiteColor();"
     @"label.text = 'This is label.';"
     @"label.sizeToFit();"
     @""
     @"var frame = label.frame;"
     @"frame.x = 10;"
     @"frame.y = 10;"
     @"label.frame = frame;"
     @""
     @"view.addSubview(label);"
     @"viewController1.view.addSubview(view);"
     @""
     @"navigationController1.viewControllers = [viewController1];"
     @""
     @"var tabBarItem = UITabBarItem.alloc();"
     @"tabBarItem = tabBarItem.initWithTabBarSystemItemTag(1);"
     @"viewController1.tabBarItem = tabBarItem;"
     @""
     @"var tabBarController = UITabBarController.new();"
     @"tabBarController.viewControllers = [navigationController1];"
     @""
     @"window.rootViewController = tabBarController;"
     @"window.makeKeyAndVisible();"
     ];
    
    return YES;
}

@end

iOSアプリケーションでキーボードショートカットに対応する

↓ より丁寧な記事はこちらで公開しています。


iOS 7のSafariやメールでは外部キーボードを使用した際に利用できるできるショートカットが以前より充実したことが話題になりました。


iOS 7ではキーボードショートカットを実装するためのAPIが追加されているので、サードパーティのアプリケーションもキーボードショートカットに対応することができます。


特定のキーボードショートカットに応答するには下記のプロパティを実装します。

@property(nonatomic, readonly) NSArray *keyCommands


このプロパティは`UIResponder`クラスに`readonly`として宣言されているので、`UIView`のサブクラスか、`UIViewController`のサブクラスでgetterメソッドをオーバーライドするのがオーソドックスなやり方になります。
(ビューコントローラはデフォルトでFirst Responderになれないので、ビューコントローラに実装する場合は`canBecomeFirstResponder`で`YES`を返すのを忘れないようにしましょう。)


ここで返す値は`UIKeyCommand`クラスのインスタンスの配列です。
`UIKeyCommand`クラスはキーの入力の組み合わせを表すクラスでこちらもiOS 7から新たに導入されました。
`UIKeyCommand`のオブジェクトは専用のクラスメソッドを使用して作成します。

+ (UIKeyCommand *)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action;


例えば「コマンドキー+S」というショートカットに対応するには下記のようにします。

- (NSArray *)keyCommands
{
    return @[[UIKeyCommand keyCommandWithInput:@"s"
                                 modifierFlags:UIKeyModifierCommand
                                        action:@selector(handleCommand:)]];
}


`input`パラメータはキー押した時に入力される文字を指定します。
`modifierFlags`はコマンド(⌘)キーやオプション(⌥)キー、シフトキーなどの修飾キーの組み合わせをビットマスクで指定します。
例えば「コントロールキー+オプションキー+S」というショートカットを表す`UIKeyCommand`のインスタンスは下記になります。

- (NSArray *)keyCommands
{
    return @[[UIKeyCommand keyCommandWithInput:@"s"
                                 modifierFlags:UIKeyModifierControl | UIKeyModifierAlternate
                                        action:@selector(handleCommand:)]];
}


ここで注意すべきなのは`UIKeyModifierControl | UIKeyModifierAlternate`という組み合わせで表現されるのは「コントロールキーとオプションキーを両方押す」という組み合わせであって、コントロールキーまたはオプションキーをどちらか片方だけ押した場合は別のショートカットキーと扱われる点です。


`modifierFlags`を指定しなかった場合は単独のキー入力に対応することができます。
またエスケープシーケンスを利用して`@"\r"`や`@"\t"`をキー入力として指定するとEnterキーやタブキーの入力に応答することができます。


キー入力のうち矢印キーとエスケープキーの特別なキーについては専用の定数が用意されています。

UIKIT_EXTERN NSString *const UIKeyInputUpArrow         NS_AVAILABLE_IOS(7_0);
UIKIT_EXTERN NSString *const UIKeyInputDownArrow       NS_AVAILABLE_IOS(7_0);
UIKIT_EXTERN NSString *const UIKeyInputLeftArrow       NS_AVAILABLE_IOS(7_0);
UIKIT_EXTERN NSString *const UIKeyInputRightArrow      NS_AVAILABLE_IOS(7_0);
UIKIT_EXTERN NSString *const UIKeyInputEscape          NS_AVAILABLE_IOS(7_0);


これらを組み合わせると、例えば矢印キーでテーブルビューを選択したり、エスケープキーでキャンセル、タブキーで移動など一般のビジネスアプリケーションでよくあるキー操作を実装することができます。


下記はサンプルコードとしてほぼすべてのキー入力と修飾キーの組み合わせに反応して、押されたキーの組み合わせを画面に表示するプログラムです。参考にしてください。
https://github.com/kishikawakatsumi/KeyboardShortcuts


20131117181446 20131117181446

20131117181446 20131117181446

iOS 7のエンタープライズ向け新機能 Guided Access(アクセスガイド)のカスタマイズ

20131108031636 20131108031636

アクセスガイドとは

iOS 6から導入された機能にアクセスガイドがあります。
これは一時的にホームボタンを無効にして1つのアプリケーションしか使えないようにしたり(クラッシュしても自動的にそのアプリケーションが再起動する)、タッチ操作を部分的に無効にして触ってほしくないボタンなどを使えなくするなど、いわゆるキオスクモードを実現できる機能です。

展覧会や店舗などでナビゲーション端末として一般のひとに貸し出して使ってもらう場合などに利用されます。


iOS 7では上記の機能に加え、アプリケーションごとに制限を定義できるようになりました。

これによって、これまでの機能でできる制限だと、アプリケーションの作りによってはボタンの位置などでうまく制限がかけられなかったりすることがありましたが、その問題がアプリケーションの対応によって解決されます。
特に企業向けのアプリケーションを提供しているところにとっては魅力的な改善だと思います。

iOS 7から追加されたアクセスガイドのカスタマイズ

アプリケーション側で対応するにはiOS 7から追加されたAPI`UIGuidedAccessRestrictionDelegate`を使います。
`UIGuidedAccessRestrictionDelegate`のメソッドを`UIApplicationDelegate`に実装すると必要に応じてシステムから呼び出されます。


`UIGuidedAccessRestrictionDelegate`の定義は次のようになります。

@required
- (NSArray *)guidedAccessRestrictionIdentifiers;
- (void)guidedAccessRestrictionWithIdentifier:(NSString *)restrictionIdentifier didChangeState:(UIGuidedAccessRestrictionState)newRestrictionState;
- (NSString *)textForGuidedAccessRestrictionWithIdentifier:(NSString *)restrictionIdentifier;

@optional
- (NSString *)detailTextForGuidedAccessRestrictionWithIdentifier:(NSString *)restrictionIdentifier;

@end


このうち、`detailTextForGuidedAccessRestrictionWithIdentifier:`以外のメソッドはすべて実装する必要があります。

`guidedAccessRestrictionIdentifiers`ではアプリケーションで独自に定義した制限を示す識別子を文字列の配列で返します。
それぞれが区別できればどのようなものでも構いません。

例えば、これまでの例と同様に、設定画面へのアクセス、記事のシェア、および削除を禁止したいとします。
それぞれに適当な識別子を決めて下記のようなコードになります。

- (NSArray *)guidedAccessRestrictionIdentifiers
{
    return @[@"allow-settings", @"allow-share", @"allow-delete"];
}


次にそれぞれの制限に対応する簡潔な説明を定義します。

- (NSString *)textForGuidedAccessRestrictionWithIdentifier:(NSString *)restrictionIdentifier
{
    if ([restrictionIdentifier isEqualToString:@"allow-settings"]) {
        return NSLocalizedString(@"Access Settings", nil);
    } else if ([restrictionIdentifier isEqualToString:@"allow-share"]) {
        return NSLocalizedString(@"Allow Share", nil);
    } else if ([restrictionIdentifier isEqualToString:@"allow-delete"]) {
        return NSLocalizedString(@"Allow Delete", nil);
    }
    
    return nil;
}


必要なら詳しい説明を追加します。

- (NSString *)detailTextForGuidedAccessRestrictionWithIdentifier:(NSString *)restrictionIdentifier
{
    if ([restrictionIdentifier isEqualToString:@"allow-settings"]) {
        return NSLocalizedString(@"Allow change settings.", nil);
    } else if ([restrictionIdentifier isEqualToString:@"allow-share"]) {
        return NSLocalizedString(@"Allow share link to other services.", nil);
    } else if ([restrictionIdentifier isEqualToString:@"allow-share"]) {
        return NSLocalizedString(@"Allow delete entries.", nil);
    }
    
    return nil;
}


上記で定義した制限の有効・無効、あるいはアクセスガイド自体のオン・オフが切り替わったときには下記のデリゲートメソッドが呼ばれるので、そこでボタンを消したり、UIを更新したりします。
制限が有効になったのか無効になったのかの判断は`newRestrictionState`の値を使います。
アクセスガイド自体がオフになったときは制限を識別子についてこのメソッドがそれぞれ呼ばれて、`newRestrictionState`の値は`UIGuidedAccessRestrictionStateAllow`になっています。

- (void)guidedAccessRestrictionWithIdentifier:(NSString *)restrictionIdentifier didChangeState:(UIGuidedAccessRestrictionState)newRestrictionState
{
    if ([restrictionIdentifier isEqualToString:@"allow-settings"]) {
        if (newRestrictionState == UIGuidedAccessRestrictionStateAllow) {
            // Settingsボタンを表示する
        } else if (newRestrictionState == UIGuidedAccessRestrictionStateDeny) {
            // Settingsボタンを消す
        }
    } else if ([restrictionIdentifier isEqualToString:@"allow-share"]) {
        if (newRestrictionState == UIGuidedAccessRestrictionStateAllow) {
            // シェアボタンを表示する
        } else if (newRestrictionState == UIGuidedAccessRestrictionStateDeny) {
            // シェアボタンを消す
        }
    } else if ([restrictionIdentifier isEqualToString:@"allow-delete"]) {
        if (newRestrictionState == UIGuidedAccessRestrictionStateAllow) {
            // 削除機能を有効にする
        } else if (newRestrictionState == UIGuidedAccessRestrictionStateDeny) {
            // 削除機能を無効にする
        }
    }
}


また、いつでも`UIGuidedAccessRestrictionStateForIdentifier()`という関数(iOS 7から)を使って任意の制限が有効かどうかを調べることができます。

if (UIGuidedAccessRestrictionStateForIdentifier(@"allow-settings") == UIGuidedAccessRestrictionStateDeny) {
    // 設定画面へのアクセスが無効になっている場合の処理
}


アクセスガイド自体が有効か無効かは`UIAccessibilityIsGuidedAccessEnabled()`という関数でわかりますし、アクセスガイドの状態が変わったときに`UIAccessibilityGuidedAccessStatusDidChangeNotification`の通知を受けることもできます。(どちらもiOS 6から)。


ここまで記述した状態でアクセスガイドの画面を表示して「オプション」で表示される項目が次のようになります。

20131108031636


赤で示した部分が先ほど追加したアプリケーション独自の制限になります。
スイッチがオンの場合は許可、オフの場合は制限をかけるという意味になります。

上記の状態では記事の共有と削除を禁止し、設定画面へのアクセスは許可するということになります。
この状態でアクセスガイドを再開した場合、状態変化のデリゲートメソッドが呼ばれるので制限の変更に応じて適宜UIの更新や対応する処理をします。


以上です。いかがでしょうか。
特にビジネス用途で使われることを想定したアプリケーションでは強力に機能するのではないでしょうか。

親指シフトキーボードが使えるノートアプリ「N+Note」をリリースしました。


入力に親指シフト(NICOLA)配列のキーボードが使えるエディタアプリです。
iOSではシステムキーボードを置き換える`inputView`プロパティというAPIが提供されているので、それを利用しています。
なので、どこでも親指シフトのキーボードが使えるというわけではなく、このアプリを使う場合だけ、ということになります。


親指シフトは「同時シフト」という入力方法が一般的ということで、普通のシフトと違って、「同時に」押すという入力が主流のようです。なので、意外にキーボードのグラフィックを用意してキーを割り当てるだけではダメで、同時判定とかけっこう大変といえば大変でした。


実は私は親指シフトキーボードは使ったことはなくて、調べた以上のことはわからないのでFacebookの親指シフトユーザー会の方々にいろいろとサポートいただきました。なんとかリリースできる形になったのはそのおかげでありとても感謝しています。


作ってるときは勘違いしていたのですが、本格的に入力するにはBluetoothなどの外付けの親指シフトキーボードを使えばいいと思っていて、あくまでもそれと比べて外出先などで少し使う場合に慣れたもので打てればいい、くらいのニーズを満たすものがいいのかなと思ってたのですが、実はBluetoothの親指シフトキーボードは製品として存在しないということで、iOSデバイスで親指シフトが使える環境は他になかったとのことです。そういう話もあるので出せてよかったかなと思います。


現状はまだまだ機能も少ないのでこれから改善していきたいと考えています。
アプリ内に意見を投稿するフォームも用意していますので要望などいただけたらうれしいです。
よろしくお願いします。


20131107010430 N+Note for NICOLA - Katsumi Kishikawa

UUIDを少し短くするUUIDShortener

kishikawakatsumi/UUIDShortener · GitHub


ちょっと仕事でUUIDっぽい文字列を限られた幅の場所に表示する必要があったので書きました。

具体的にはレシートに識別子を印字したいという要件で、識別子はUUIDを振っているのでそれでいいのですが、レシートに印字するにはちょっと長すぎて2行になってしまうのでその点が問題でした。


そこでレシートの幅に収まるようにUUIDを別の表現に変換するのが`UUIDShortener`です。


実際に適用してみると下記のようになります。

20131101003300


上記のレシートに印字されている`NJ6NYLNKBRGSNCUF54Z53R4FVU`という文字列は、このライブラリを使って復元すると`6A7CDC2D-AA0C-4D26-8A85-EF33DDC785AD`というUUIDに復元されます。


UUIDは32桁の16進表記ですが、`UUIDShortener`を使って圧縮すると26桁のアルファベットと数字表記になります。
区切り記号も合わせると10桁ぶん短く表示できます。


使いかたは下記のようになります。

UUIDを圧縮する
/* Shorten UUID string */
NSUUID *UUID = [NSUUID UUID]; // => 40256F2F-3211-49CD-BC1F-DD5197D2F0F9
NSString *shortUUIDString = UUID.shortUUIDString;
NSLog(@"Short UUID:\t%@", shortUUIDString);
// => Short UUID:	    IASW6LZSCFE43PA73VIZPUXQ7E
圧縮した文字列から元のUUIDを復元する
/* Restore UUID string from short string */
NSString *restoredString = [NSUUID UUIDStringFromShortUUIDString:shortUUIDString];
NSLog(@"Restored UUID:\t%@", restoredString);
// => Restored UUID:	40256F2F-3211-49CD-BC1F-DD5197D2F0F9


仕組みですが、UUIDをいったんバイナリに戻し、それをBase32でエンコードしています。
もともと16進表記だったものが使える文字が増えるので結果的に短く表現できるということになります。


また、Base32はO(オー)、l(エル)と紛らわしい0(ゼロ)と1(イチ)があらかじめ除かれているので、検索などで識別子を入力する場合や、問題を調査するときに口頭で伝えてもらう場合などに都合がいいというのも良い点でした。大文字小文字を区別する必要がないというのも使い勝手がいいエンコード方式です。

参考

Base32 - Wikipedia, the free encyclopedia


もっと短くしたいという場合にはBase64Ascii85 (Base85)などを使えばいいかと思います。ただ、大文字小文字の区別ができたり、記号が入ったりするので入力や伝達が少し難しくなりますね。

エンジニア募集中

ユビレジでは現在エンジニアをiOS、サーバーサイドともに積極的に募集中です。
興味のあるかたは私までお声がけください。

追加ダウンロードフォントを含むiOS 7で使えるフォント一覧

出力の方法

iOS 7からは`kCTFontDownloadableAttribute`という属性が追加されているのでそれを利用します。
この方法で数えると、すべてのフォント数は283になりました。
iOS 7で新たに使えるようになった日本語のフォントとしてはOsakaとOsaka-等幅フォントがあります(追加ダウンロードフォント)。
かつてのMacの日本語システムフォントです。懐かしいですね。

+ (void)downloadableFonts
{
    NSDictionary *attributes = @{(id)kCTFontDownloadableAttribute : (id)kCFBooleanTrue};
    CTFontDescriptorRef fontDescriptor = CTFontDescriptorCreateWithAttributes((CFDictionaryRef)attributes);
    CFArrayRef matchedFontDescriptors = CTFontDescriptorCreateMatchingFontDescriptors(fontDescriptor, NULL);
    
    NSMutableDictionary *familyNames = [[NSMutableDictionary alloc] init];
    NSInteger numberOfFonts = 0;
    NSMutableString *text = [[NSMutableString alloc] init];
    for (UIFontDescriptor *fontDescriptor in (__bridge NSArray *)matchedFontDescriptors) {
        NSString *familyName = fontDescriptor.fontAttributes[UIFontDescriptorFamilyAttribute];
        NSString *displayName = fontDescriptor.fontAttributes[UIFontDescriptorVisibleNameAttribute];
        NSString *postscriptName = fontDescriptor.postscriptName;
        
        if (!familyNames[familyName]) {
            familyNames[familyName] = familyName;
            [text appendFormat:@"<b>%@</b>\n\n", familyName];
        }
        NSMutableDictionary *fontDict = [NSMutableDictionary dictionary];
        fontDict[@"displayName"] = displayName;
        fontDict[@"postscriptName"] = postscriptName;
        fontDict[@"descriptor"] = fontDescriptor;
        NSArray *languages = fontDescriptor.fontAttributes[@"NSCTFontDesignLanguagesAttribute"];
        fontDict[@"languages"] = [languages componentsJoinedByString:@", "];
        
        [text appendFormat:@"- %@ \"%@\" [%@]\n", postscriptName, displayName, [languages componentsJoinedByString:@", "]];
        
        numberOfFonts++;
    }
    
    NSLog(@"%@", text);
}

フォント一覧(日本語フォントは[ja])

AlFirat

  • Al-Firat "Al-Firat" [ar]

AlKhalil

  • Al-KhalilBold "Al-Khalil Bold" [ar]
  • Al-Khalil "Al-Khalil" [ar]

AlRafidain

  • Al-Rafidain "Al-Rafidain" [ar]

Al Bayan

  • AlBayan "Al Bayan Plain" [en]
  • AlBayan-Bold "Al Bayan Bold" [en]

Algiers

  • Algiers "Algiers" [ar]

AlRafidain AlFanni

  • AlRafidainAlFanni "Al-Rafidain Al-Fanni" [ar]

Al Tarikh

  • AlTarikh "Al Tarikh" [en]

Andale Mono

  • AndaleMono "Andale Mono" [en]

Apple Chancery

  • Apple-Chancery "Apple Chancery" [en]

Apple Braille

  • AppleBraille "Apple Braille" [en]
  • AppleBraille-Outline6Dot "Apple Braille Outline 6 Dot" [en]
  • AppleBraille-Outline8Dot "Apple Braille Outline 8 Dot" [en]
  • AppleBraille-Pinpoint6Dot "Apple Braille Pinpoint 6 Dot" [en]
  • AppleBraille-Pinpoint8Dot "Apple Braille Pinpoint 8 Dot" [en]

AppleGothic

  • AppleGothic "AppleGothic Regular" [ko]

AppleMyungjo

  • AppleMyungjo "AppleMyungjo Regular" [ko]

Apple SD Gothic Neo

  • AppleSDGothicNeo-Bold "Apple SD Gothic Neo Bold" [ko]
  • AppleSDGothicNeo-ExtraBold "Apple SD GothicNeo ExtraBold" [ko]
  • AppleSDGothicNeo-Heavy "Apple SD Gothic Neo Heavy" [ko]
  • AppleSDGothicNeo-Light "Apple SD Gothic Neo Light" [ko]
  • AppleSDGothicNeo-Medium "Apple SD Gothic Neo Medium" [ko]
  • AppleSDGothicNeo-Regular "Apple SD Gothic Neo Regular" [ko]
  • AppleSDGothicNeo-SemiBold "Apple SD Gothic Neo SemiBold" [ko]
  • AppleSDGothicNeo-Thin "Apple SD Gothic Neo Thin" [ko]
  • AppleSDGothicNeo-UltraLight "Apple SD Gothic Neo UltraLight" [ko]

Apple Symbols

  • AppleSymbols "Apple Symbols" [en]

Arial Black

  • Arial-Black "Arial Black" [en]

Arial

  • Arial-BoldItalicMT "Arial Bold Italic" [en]
  • Arial-BoldMT "Arial Bold" [en]
  • Arial-ItalicMT "Arial Italic" [en]
  • ArialMT "Arial" [en]

Arial Narrow

  • ArialNarrow "Arial Narrow" [en]
  • ArialNarrow-Bold "Arial Narrow Bold" [en]
  • ArialNarrow-BoldItalic "Arial Narrow Bold Italic" [en]
  • ArialNarrow-Italic "Arial Narrow Italic" [en]

Arial Unicode MS

  • ArialUnicodeMS "Arial Unicode MS" [en]

Ayuthaya

  • Ayuthaya "Ayuthaya" [en]

Baghdad

  • Baghdad "Baghdad" [en]

Bangla MN

  • BanglaMN "Bangla MN" [en]
  • BanglaMN-Bold "Bangla MN Bold" [en]

Basra

  • Basra-Bold "Basra Bold" [ar]
  • Basra "Basra" [ar]

Beirut

  • Beirut "Beirut" [en]

Big Caslon

  • BigCaslon-Medium "Big Caslon Medium" [en]

Book Antiqua

  • BookAntiqua "BookAntiqua" [en]
  • BookAntiqua-Italic "BookAntiqua-Italic" [en]
  • BookAntiqua-BoldItalic "BookAntiqua-BoldItalic" [en]
  • BookAntiqua-Bold "BookAntiqua-Bold" [en]

Bookman Old Style

  • BookmanOldStyle "Bookman Old Style" [en]
  • BookmanOldStyle-Italic "Bookman Old Style Italic" [en]
  • BookmanOldStyle-BoldItalic "Bookman Old Style Bold Italic" [en]
  • BookmanOldStyle-Bold "Bookman Old Style Bold" [en]

Brush Script MT

  • BrushScriptMT "Brush Script MT Italic" [en]

Chalkboard

  • Chalkboard "Chalkboard" [en]
  • Chalkboard-Bold "Chalkboard Bold" [en]

Comic Sans MS

  • ComicSansMS "Comic Sans MS" [en]
  • ComicSansMS-Bold "Comic Sans MS Bold" [en]

Corsiva Hebrew

  • CorsivaHebrew "Corsiva Hebrew" [en]
  • CorsivaHebrew-Bold "Corsiva Hebrew Bold" [en]

DecoType Naskh

  • DecoTypeNaskh "DecoType Naskh" [en]

Devanagari MT

  • DevanagariMT "Devanagari MT" [en]
  • DevanagariMT-Bold "Devanagari MT Bold" [en]

BiauKai

  • DFKaiShu-SB-Estd-BF "BiauKai" [zh-Hant]

Wawati SC

  • DFWaWaSC-W5 "Wawati SC Regular" [zh-Hans]

Wawati TC

  • DFWaWaTC-W5 "Wawati TC Regular" [zh-Hant]

Dijla

  • Dijla "Dijla" [ar]

Diwan Kufi

  • DiwanKufi "Diwan Kufi" [en]

Diwan Thuluth

  • DiwanThuluth "Diwan Thuluth" [en]

Farisi

  • Farisi "Farisi" [en]

Lantinghei SC

  • FZLTZHK--GBK1-0 "Lantinghei SC Demibold" [zh-Hans]
  • FZLTXHK--GBK1-0 "Lantinghei SC Extralight" [zh-Hans]
  • FZLTTHK--GBK1-0 "Lantinghei SC Heavy" [zh-Hans]

Lantinghei TC

  • FZLTZHB--B51-0 "Lantinghei TC Demibold" [zh-Hant]
  • FZLTXHB--B51-0 "Lantinghei TC Extralight" [zh-Hant]
  • FZLTTHB--B51-0 "Lantinghei TC Heavy" [zh-Hant]

Garamond

  • Garamond "Garamond " [en]
  • Garamond-Italic "Garamond Italic" [en]
  • Garamond-BoldItalic "Garamond Bold Italic" [en]
  • Garamond-Bold "Garamond Bold" [en]

Gujarati MT

  • GujaratiMT "Gujarati MT" [en]
  • GujaratiMT-Bold "Gujarati MT Bold" [en]

Gurmukhi MN

  • GurmukhiMN "Gurmukhi MN" [en]
  • GurmukhiMN-Bold "Gurmukhi MN Bold" [en]

Gurmukhi Sangam MN

  • GurmukhiSangamMN "Gurmukhi Sangam MN" [en]
  • GurmukhiSangamMN-Bold "Gurmukhi Sangam MN Bold" [en]

Hannotate SC

  • HannotateSC-W5 "Hannotate SC Regular" [zh-Hans]

Hannotate TC

  • HannotateTC-W5 "Hannotate TC Regular" [zh-Hant]
  • HannotateSC-W7 "Hannotate SC Bold" [zh-Hans]
  • HannotateTC-W7 "Hannotate TC Bold" [zh-Hant]

HanziPen SC

  • HanziPenSC-W3 "HanziPen SC Regular" [zh-Hans]

HanziPen TC

  • HanziPenTC-W3 "HanziPen TC Regular" [zh-Hant]
  • HanziPenSC-W5 "Weibei SC Bold" [zh-Hans]
  • HanziPenTC-W5 "HanziPen TC Bold" [zh-Hant]

Herculanum

  • Herculanum "Herculanum" [en]

Hiragino Sans GB

  • HiraginoSansGB-W3 "ヒラギノ角ゴ 簡体中文 W3" [zh-Hans]
  • HiraginoSansGB-W6 "ヒラギノ角ゴ 簡体中文 W6" [zh-Hans]

Hiragino Kaku Gothic Pro

  • HiraKakuPro-W3 "ヒラギノ角ゴ Pro W3" [ja]
  • HiraKakuPro-W6 "ヒラギノ角ゴ Pro W6" [ja]

Hiragino Kaku Gothic Std

  • HiraKakuStd-W8 "ヒラギノ角ゴ Std W8" [ja]

Hiragino Kaku Gothic StdN

  • HiraKakuStdN-W8 "ヒラギノ角ゴ StdN W8" [ja]

Hiragino Maru Gothic Pro

  • HiraMaruPro-W4 "ヒラギノ丸ゴ Pro W4" [ja]

Hiragino Maru Gothic ProN

  • HiraMaruProN-W4 "ヒラギノ丸ゴ ProN W4" [ja]

Hiragino Mincho Pro

  • HiraMinPro-W3 "ヒラギノ明朝 Pro W3" [ja]
  • HiraMinPro-W6 "ヒラギノ明朝 Pro W6" [ja]

YuMincho

  • YuMin-Medium "游明朝体 ミディアム" [ja]
  • YuMin-Demibold "游明朝体 デミボールド" [ja]

YuGothic

  • YuGo-Bold "游ゴシック体 ボールド" [ja]
  • YuGo-Medium "游ゴシック体 ミディアム" [ja]

Hoefler Text

  • HoeflerText-Ornaments "Hoefler Text Ornaments" [en]

Impact

  • Impact "Impact" [en]

InaiMathi

  • InaiMathi "InaiMathi" [en]

Iowan Old Style Black

  • IowanOldStyle-Black "Iowan Old Style Black" [en]
  • IowanOldStyle-BlackItalic "Iowan Old Style Black Italic" [en]

Iowan Old Style

  • IowanOldStyle-Bold "Iowan Old Style Bold" [en]
  • IowanOldStyle-BoldItalic "Iowan Old Style Bold Italic" [en]
  • IowanOldStyle-Italic "Iowan Old Style Italic" [en]
  • IowanOldStyle-Roman "Iowan Old Style Roman" [en]
  • IowanOldStyle-Titling "Iowan Old Style Titling" [en]

PilGi

  • JCfg "PilGi Regular" [ko]

HeadLineA

  • JCHEadA "HeadLineA Regular" [ko]

GungSeo

  • JCkg "GungSeo Regular" [en]

PCMyungjo

  • JCsmPC "PCMyungjo Regular" [ko]

Kannada MN

  • KannadaMN "Kannada MN" [en]
  • KannadaMN-Bold "Kannada MN Bold" [en]

Kefa

  • Kefa-Regular "Kefa Regular" [en]
  • Kefa-Bold "Kefa Bold" [en]

Khmer MN

  • KhmerMN "Khmer MN" [en]
  • KhmerMN-Bold "Khmer MN Bold" [en]

Khmer Sangam MN

  • KhmerSangamMN "Khmer Sangam MN" [en]

Kokonor

  • Kokonor "Kokonor Regular" [en]

Koufi Abjadi

  • KoufiAbjadi "Koufi Abjadi" [ar]

Krungthep

  • Krungthep "Krungthep" [en]

KufiStandardGK

  • KufiStandardGK "KufiStandardGK" [en]

Laimoon

  • Laimoon "Laimoon" [ar]

Lao MN

  • LaoMN "Lao MN" [en]
  • LaoMN-Bold "Lao MN Bold" [en]

Lao Sangam MN

  • LaoSangamMN "Lao Sangam MN" [en]

Apple LiGothic

  • LiGothicMed "Apple LiGothic Medium" [zh-Hant]

LiHei Pro

  • LiHeiPro "LiHei Pro" [zh-Hant]

LiSong Pro

  • LiSongPro "LiSong Pro" [zh-Hant]

Apple LiSung

  • LiSungLight "Apple LiSung Light" [zh-Hant]

Lucida Grande

  • LucidaGrande "Lucida Grande" [en]
  • LucidaGrande-Bold "Lucida Grande Bold" [en]

.Lucida Grande UI

  • .LucidaGrandeUI ".Lucida Grande UI" [en]
  • .LucidaGrandeUI-Bold ".Lucida Grande UI Bold" [en]

Malayalam MN

  • MalayalamMN "Malayalam MN" [en]
  • MalayalamMN-Bold "Malayalam MN Bold" [en]

Microsoft Sans Serif

  • MicrosoftSansSerif "Microsoft Sans Serif" [en]

Gurmukhi MT

  • MonotypeGurmukhi "Gurmukhi MT" [en]

Mshtakan

  • Mshtakan "Mshtakan" [en]
  • MshtakanBold "Mshtakan Bold" [en]
  • MshtakanBoldOblique "Mshtakan BoldOblique" [en]
  • MshtakanOblique "Mshtakan Oblique" [en]

Muna

  • Muna "Muna" [en]
  • MunaBold "Muna Bold" [en]
  • MunaBlack "Muna Black" [en]

Myanmar MN

  • MyanmarMN "Myanmar MN" [en]
  • MyanmarMN-Bold "Myanmar MN Bold" [en]

Myanmar Sangam MN

  • MyanmarSangamMN "Myanmar Sangam MN" [en]

Nadeem

  • Nadeem "Nadeem" [en]

Nanum Brush Script

  • NanumBrush "Nanum Brush Script" [ko]

Nanum Pen Script

  • NanumPen "Nanum Pen Script" [ko]

NanumGothic

  • NanumGothic "NanumGothic" [ko]
  • NanumGothicBold "NanumGothic Bold" [ko]
  • NanumGothicExtraBold "NanumGothic ExtraBold" [ko]

NanumMyeongjo

  • NanumMyeongjo "NanumMyeongjo" [ko]
  • NanumMyeongjoBold "NanumMyeongjoBold" [ko]
  • NanumMyeongjoExtraBold "NanumMyeongjoExtraBold" [ko]

New Peninim MT

  • NewPeninimMT "New Peninim MT" [en]
  • NewPeninimMT-Bold "New Peninim MT Bold" [en]
  • NewPeninimMT-BoldInclined "New Peninim MT Bold Inclined" [en]
  • NewPeninimMT-Inclined "New Peninim MT Inclined" [en]

Nisan

  • Nisan "Nisan" [ar]

Oriya MN

  • OriyaMN "Oriya MN" [en]
  • OriyaMN-Bold "Oriya MN Bold" [en]

Osaka

  • Osaka "Osaka" [ja]
  • Osaka-Mono "Osaka−等幅" [ja]

Plantagenet Cherokee

  • PlantagenetCherokee "Plantagenet Cherokee" [en]

PT Sans

  • PTSans-Regular "PT Sans" [en]
  • PTSans-Italic "PT Sans Italic" [en]
  • PTSans-Bold "PT Sans Bold" [en]
  • PTSans-BoldItalic "PT Sans Bold Italic" [en]

PT Sans Caption

  • PTSans-Caption "PT Sans Caption" [en]
  • PTSans-CaptionBold "PT Sans Caption Bold" [en]

PT Sans Narrow

  • PTSans-Narrow "PT Sans Narrow" [en]
  • PTSans-NarrowBold "PT Sans Narrow Bold" [en]

Raanana

  • Raanana "Raanana" [en]
  • RaananaBold "Raanana Bold" [en]

Raya

  • Raya "Raya" [ar]

Sana

  • Sana "Sana" [en]

Sathu

  • Sathu "Sathu" [en]

Savoye LET

  • SavoyeLetPlain "Savoye LET Plain:1.0" [en]

.Savoye LET CC.

  • .SavoyeLetPlainCC "Savoye LET Plain CC.:1.0" [en]

Hei

  • SIL-Hei-Med-Jian "Hei Regular" [zh-Hans]

Kai

  • SIL-Kai-Reg-Jian "Kai Regular" [zh-Hans]

Silom

  • Silom "Silom" [en]

Sinhala MN

  • SinhalaMN "Sinhala MN" [en]
  • SinhalaMN-Bold "Sinhala MN Bold" [en]

Somer

  • Somer "Somer" [ar]

Baoli SC

  • STBaoli-SC-Regular "Baoli SC Regular" [zh-Hans]

STFangsong

  • STFangsong "STFangsong" [zh-Hans]

STHeiti

  • STHeiti "STHeiti" [zh-Hans]

STIXGeneral

  • STIXGeneral-Bold "STIXGeneral-Bold" [en]
  • STIXGeneral-BoldItalic "STIXGeneral-BoldItalic" [en]
  • STIXGeneral-Italic "STIXGeneral-Italic" [en]
  • STIXGeneral-Regular "STIXGeneral-Regular" [en]

STIXIntegralsD

  • STIXIntegralsD-Bold "STIXIntegralsD-Bold" [en]
  • STIXIntegralsD-Regular "STIXIntegralsD-Regular" [en]

STIXIntegralsSm

  • STIXIntegralsSm-Bold "STIXIntegralsSm-Bold" [en]
  • STIXIntegralsSm-Regular "STIXIntegralsSm-Regular" [en]

STIXIntegralsUp

  • STIXIntegralsUp-Bold "STIXIntegralsUp-Bold" [en]
  • STIXIntegralsUp-Regular "STIXIntegralsUp-Regular" [en]

STIXIntegralsUpD

  • STIXIntegralsUpD-Bold "STIXIntegralsUpD-Bold" [en]
  • STIXIntegralsUpD-Regular "STIXIntegralsUpD-Regular" [en]

STIXIntegralsUpSm

  • STIXIntegralsUpSm-Bold "STIXIntegralsUpSm-Bold" [en]
  • STIXIntegralsUpSm-Regular "STIXIntegralsUpSm-Regular" [en]

STIXNonUnicode

  • STIXNonUnicode-Bold "STIXNonUnicode-Bold" [en]
  • STIXNonUnicode-BoldItalic "STIXNonUnicode-BoldItalic" [en]
  • STIXNonUnicode-Italic "STIXNonUnicode-Italic" [en]
  • STIXNonUnicode-Regular "STIXNonUnicode-Regular" [en]

STIXSizeFiveSym

  • STIXSizeFiveSym-Regular "STIXSizeFiveSym-Regular" [en]

STIXSizeFourSym

  • STIXSizeFourSym-Bold "STIXSizeFourSym-Bold" [en]
  • STIXSizeFourSym-Regular "STIXSizeFourSym-Regular" [en]

STIXSizeOneSym

  • STIXSizeOneSym-Bold "STIXSizeOneSym-Bold" [en]
  • STIXSizeOneSym-Regular "STIXSizeOneSym-Regular" [en]

STIXSizeThreeSym

  • STIXSizeThreeSym-Bold "STIXSizeThreeSym-Bold" [en]
  • STIXSizeThreeSym-Regular "STIXSizeThreeSym-Regular" [en]

STIXSizeTwoSym

  • STIXSizeTwoSym-Bold "STIXSizeTwoSym-Bold" [en]
  • STIXSizeTwoSym-Regular "STIXSizeTwoSym-Regular" [en]

STIXVariants

  • STIXVariants-Bold "STIXVariants-Bold" [en]
  • STIXVariants-Regular "STIXVariants-Regular" [en]

Kaiti SC

  • STKaiti-SC-Black "Kaiti SC Black" [zh-Hans]
  • STKaiti-SC-Bold "Kaiti SC Bold" [zh-Hans]

Kaiti TC

  • STKaiTi-TC-Bold "Kaiti TC Bold" [zh-Hant]
  • STKaiti-SC-Regular "Kaiti SC Regular" [zh-Hans]
  • STKaiTi-TC-Regular "Kaiti TC Regular" [zh-Hant]

Libian SC

  • STLibian-SC-Regular "Libian SC Regular" [zh-Hans]

Songti SC

  • STSongti-SC-Black "Songti SC Black" [zh-Hans]
  • STSongti-SC-Light "Songti SC Light" [zh-Hans]

Songti TC

  • STSongti-TC-Light "Songti SC Light" [zh-Hant]
  • STSongti-SC-Bold "Songti SC Bold" [zh-Hans]
  • STSongti-TC-Bold "Songti TC Bold" [zh-Hant]
  • STSongti-SC-Regular "Songti SC Regular" [zh-Hans]
  • STSongti-TC-Regular "Songti TC Regular" [zh-Hant]
  • STXihei "STXihei" [zh-Hans]

Xingkai SC

  • STXingkai-SC-Bold "Xingkai SC Bold" [zh-Hans]
  • STXingkai-SC-Light "Xingkai SC Light" [zh-Hans]

Yuanti SC

  • STYuanti-SC-Bold "Yuanti SC Bold" [zh-Hans]
  • STYuanti-SC-Light "Yuanti SC Light" [zh-Hans]
  • STYuanti-SC-Regular "Yuanti SC Regular" [zh-Hans]

Tahoma

  • Tahoma "Tahoma" [en]
  • Tahoma-Bold "Tahoma Bold" [en]

Tamil MN

  • TamilMN "Tamil MN" [en]
  • TamilMN-Bold "Tamil MN Bold" [en]

Telugu MN

  • TeluguMN "Telugu MN" [en]
  • TeluguMN-Bold "Telugu MN Bold" [en]

Waseem

  • Waseem "Waseem" [en]
  • WaseemLight "Waseem Light" [en]

Webdings

  • Webdings "Webdings" [en]

Weibei SC

  • Weibei-SC-Bold "Weibei SC Bold" [zh-Hans]

Weibei TC

  • Weibei-TC-Bold "Weibei TC Bold" [zh-Hant]

Wingdings

  • Wingdings-Regular "Wingdings" [en]

Wingdings 2

  • Wingdings2 "Wingdings 2" [en]

Wingdings 3

  • Wingdings3 "Wingdings 3" [en]

Yaziji

  • Yaziji "Yaziji" [ar]

Yuppy SC

  • YuppySC-Regular "Yuppy SC Regular" [zh-Hans]

Yuppy TC

  • YuppyTC-Regular "Yuppy TC Regular" [zh-Hant]

Zawra

  • Zawra-Bold "Zawra Bold" [ar]
  • Zawra-Heavy "Zawra Heavy" [ar]

Century Gothic

  • CenturyGothic "Century Gothic" [en]
  • CenturyGothic-Bold "Century Gothic Bold" [en]
  • CenturyGothic-Italic "Century Gothic Italic" [en]
  • CenturyGothic-BoldItalic "Century Gothic Bold Italic" [en]

Century Schoolbook

  • CenturySchoolbook "Century Schoolbook" [en]
  • CenturySchoolbook-Bold "Century Schoolbook Bold" [en]
  • CenturySchoolbook-Italic "Century Schoolbook Italic" [en]
  • CenturySchoolbook-BoldItalic "Century Schoolbook Bold Italic" [en]

Tw Cen MT

  • TwCenMT-Regular "Tw Cen MT" [en]
  • TwCenMT-Bold "Tw Cen MT Bold" [en]
  • TwCenMT-Italic "Tw Cen MT Italic" [en]
  • TwCenMT-BoldItalic "Tw Cen MT Bold Italic" [en]

iOS 6 or 7で「游ゴシック体」や「ヒラギノ丸ゴシック」を使う

20131027032814


OS XではMarvericks (10.9) から「游ゴシック体」と「游明朝体」が標準搭載されたことで話題になりましたが、実はiOSでも6以上からこれらのフォントが使用できます。


iOS 6から追加ダウンロードフォントという仕組みが導入され、游ゴシック体などのフォントはその追加ダウンロードフォントという形で提供されています。
アプリケーションからは必要に応じてフォントをダウンロードして利用します。

フォント一覧

iOS 6で使えるフォントはアップルの下記のページに記載されています。
このページの下の方に追加情報として記載されているフォントがダウンロードフォントになります。
iOS 6:フォントリスト (http://support.apple.com/kb/HT5484?viewlocale=ja_JP&locale=ja_JP)


上記のページに記載されている追加ダウンロードフォントのうち、日本語のフォントは以下の8つです。

  • Hiragino Kaku Gothic Std W8 (ヒラギノ角ゴ Std W8)
  • Hiragino Kaku Gothic StdN W8 (ヒラギノ角ゴ StdN W8)
  • Hiragino Maru Gothic Pro W4 (ヒラギノ丸ゴ Pro W4)
  • Hiragino Maru Gothic ProN W4 (ヒラギノ丸ゴ ProN W4)
  • YuGothic Bold (游ゴシック体 ボールド)
  • YuGothic Medium (游ゴシック体 ミディアム)
  • YuMincho Demibold (游明朝体 デミボールド)
  • YuMincho Medium (游明朝体 ミディアム)


このうちヒラギノに関してはProNとPro、StdNとStdの違いで2書体になってるので実質6書体ですね。
ヒラギノ丸ゴシックやヒラギノ角ゴシックのW8などは標準搭載のフォントとはかなり趣きが異なるので使いどころによって重宝します。


iOS 7ではさらに少しフォントが追加されているのですがそれについては別の記事で記載します。

APIについて

追加ダウンロードフォントを利用するには、必要に応じてダウンロードの処理をします。
ダウンロードには`CoreText.framework`の下記のAPIを利用します。

bool CTFontDescriptorMatchFontDescriptorsWithProgressHandler(
    CFArrayRef                          descriptors,
    CFSetRef                            mandatoryAttributes,
    CTFontDescriptorProgressHandler     progressBlock) CT_AVAILABLE(10_9, 6_0);

このAPIはドキュメントに解説が載っていないので、利用方法はヘッダファイルを見ます。


また、アップルから公式のサンプルコードが提供されていますのでこちらも参考になります。
DownloadFont - iOS Developer Library (https://developer.apple.com/LIBRARY/IOS/samplecode/DownloadFont/Introduction/Intro.html)


情報は少ないですが、特に難しいことはなく、1番目の引数にFont Descriptorの形でフォント情報を渡すと、適宜ブロックのコールバックが呼ばれるという仕組みです。


追加ダウンロードフォントについては、フォント名がわかっているのでフォント名からFont Descriptorを作ります。

NSDictionary *attributes = @{(id)kCTFontNameAttribute: fontName};
CTFontDescriptorRef fontDescriptor = CTFontDescriptorCreateWithAttributes((__bridge CFDictionaryRef)attributes);
NSArray *fontDescriptors = @[(__bridge id)fontDescriptor];
CFRelease(fontDescriptor);


そして、Font Descriptorを先ほどのAPIに渡せば、必要に応じてフォントをダウンロードし、アプリケーションで利用できるように登録するところまでやってくれます。
ダウンロードの進捗やトータルのサイズなどの情報はブロックの引数の`progressParameter`に入っています。
アプリケーションでダウンロード中のプログレスバーなどを表示するにはこのパラメータの値を利用します。


ダウンロードされたフォントはアプリケーションごとに保存されるわけではなく、すべてのアプリケーションで共有の場所に保存されます。
例えば下記の場所です。

file:///private/var/mobile/Library/Assets/com_apple_MobileAsset_Font/83f5ce0efa7a810b73a7231c0e107f2955f2c85c.asset/AssetData/Yu%20Gothic%20Bold.otf


つまり、あるアプリケーションでフォントがダウンロードされていれば(例えばiBooksなど)、別のアプリケーションではダウンロード処理はスキップしてそのフォントを利用できます。
ただ、一度ダウンロードされたフォントでもデバイスの空き容量によって削除されることがあるので、プログラムはそれを考慮して作成する必要があります。
また、すでに他のアプリケーションでフォントがダウンロードされていても、自分のアプリケーションで利用可能にするには上記のAPIを呼ぶ必要があります。
さらに、どのアプリケーションでダウンロードしたかにかかわらず、一度アプリケーションを終了すると利用登録が解除されてしまうので、次に起動したときには再度上記のAPIを呼ぶ必要があります。


まとめると、

  • 追加ダウンロードフォントはデバイス全体で共有される。
  • ダウンロードされたフォントは、自動的にシステムから削除されることがある。
  • フォントがダウンロード済みならダウンロード処理は自動的にスキップされる。
  • フォントが他のアプリケーションによってダウンロード済みであっても、自分のアプリケーションで利用可能にするにはこのAPIを呼ぶ必要がある。
  • 自分のアプリケーションでダウンロードしたフォントであっても、アプリケーションを終了したら利用登録が解除されるので、再度利用可能な状態にするにはこのAPIを呼ぶ必要がある。


ややこしいように感じますが、ダウンロードのAPIは必要ならダウンロードされ、ダウンロード済みならダウンロードはスキップされてロード処理だけをする、という動きをするので、要するに最初に利用しようとしたときにダウンロードのAPIを呼べばいいということになります。
ただ、単純にダウンロードのAPIを毎回呼ぶとすると、ダウンロードしたくないときにもダウンロードされてしまうので、必要に応じてダウンロードはキャンセルできるようにしたほうがいいでしょう。

フォントのダウンロードおよび利用登録

追加ダウンロードフォントを利用可能にする処理は下記のようになります。
ダウンロードの処理はフォントがどのアプリケーションによってもダウンロードされてないときのみ行われます。
フォントがダウンロード済みであっても、アプリケーションを起動しただけの状態では利用可能になっていないので、この処理をする必要があります。その場合、コールバックはダウンロードのステータスになることはなく、短時間で`kCTFontDescriptorMatchingDidFinish`の状態になります。

CTFontDescriptorMatchFontDescriptorsWithProgressHandler((__bridge CFArrayRef)fontDescriptors, NULL, ^bool(CTFontDescriptorMatchingState state, CFDictionaryRef progressParameter) {
    NSDictionary *parameter = (__bridge NSDictionary *)progressParameter;
    double progressValue = [parameter[(id)kCTFontDescriptorMatchingPercentage] doubleValue];
    
    if (state == kCTFontDescriptorMatchingDidBegin) { // 処理の開始に1度だけ呼ばれる
        dispatch_async( dispatch_get_main_queue(), ^ {
            // ダウンロードはサブスレッドで行われるのでUIの更新などはメインスレッドで行う
            ...
        });
    } else if (state == kCTFontDescriptorMatchingDidFinish) { // 処理の終了時に1度だけ呼ばれる
        dispatch_async( dispatch_get_main_queue(), ^ {
            UIFont *font = [UIFont fontWithName:fontName size:1.0f];
            // この時点でフォントが利用可能になる
            ...
        });
    } else if (state == kCTFontDescriptorMatchingWillBeginDownloading) {
        // フォントが未ダウンロードの場合のみ、ダウンロードの開始前に呼ばれる
        ...
    } else if (state == kCTFontDescriptorMatchingDownloading) {
        // ダウンロード中、ダウンロードの進捗によって適宜呼ばれる
        ...
    } else if (state == kCTFontDescriptorMatchingDidFinishDownloading) {
        // ダウンロード完了時に呼ばれる
        ...
    } else if (state == kCTFontDescriptorMatchingDidFailWithError) {
        // ダウンロードが失敗したときに呼ばれる
        ...
    }
    
    return (bool)YES;
});


ダウンロード済みのフォントがある場合にフォントのロードだけ行い、ダウンロードはしたくないという場合は、例えば下記のようにします。

CTFontDescriptorMatchFontDescriptorsWithProgressHandler((__bridge CFArrayRef)fontDescriptors, NULL, ^bool(CTFontDescriptorMatchingState state, CFDictionaryRef progressParameter) {
    if (state == kCTFontDescriptorMatchingDidFinish) {
        dispatch_async( dispatch_get_main_queue(), ^ {
            UIFont *font = [UIFont fontWithName:fontName size:1.0f];
            if (font) {
                if ([self.delegate respondsToSelector:@selector(fontDownloaderDidFinish:fontName:)]) {
                    [self.delegate fontDownloaderDidFinish:self fontName:fontName];
                }
            }
        });
    } else if (state == kCTFontDescriptorMatchingWillBeginDownloading) {
        return (bool)NO;
    }
    
    return (bool)YES;
});


`kCTFontDescriptorMatchingWillBeginDownloading`で`NO`を返しているのでそれ以上の処理は行われません。
もしダウンロード中のキャンセルをサポートする場合にも同様にすることで実現できます。


アップルのサンプルコードは主に中国語フォントを使っているので、日本語フォントについて簡単に試せるようにしたコードをGithubで公開しているので、よかったらご覧ください。

kishikawakatsumi/DownloadFont · GitHub