Hatena::ブログ(Diary)

24/7 twenty-four seven Twitter

mail address

Follow me on twitter.

iPhone アプリケーション

Hatena touch LDR touch TV Listings LCD Clock LCD Clock Lite MyWebClip MyWebClip LITE Japan Subway Route Map こころくろっく 英辞郎 on the WEB for iPhone(アルク)
i-Radio くるりんぱ性格診断 英辞郎検索ランキング(アルク) kotobank - コトバンク miil COOKPAD クックパッド のせる トイカメラ - TiltShiftGen2

iPad アプリケーション

LCD Clock HD 「超」整理手帳 for the iPad N+Note for NICOLA

Windows 8 Store アプリケーション

クックパッド

共著

2014-04-08

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つも含まれていなかったり、その値にアクセスすることがなくても、同じだけ処理時間がかかっていたのですが、実際にアクセスのあったタイミングで必要なところだけ処理することで、パフォーマンスに対するペナルティは非常に小さくなります。

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

2014-02-14

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に日付を表示する例を考えます。

f:id:KishikawaKatsumi:20140214182059p:image:w320


↑ このような構成の画面はよくあると思いますが、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

2014-02-11

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をプロジェクトに用意しておきます。

最低限、下記のような構成になっていればいいです。

f:id:KishikawaKatsumi:20140211182640p:image:w320


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

f:id:KishikawaKatsumi:20140211182641p:image:w480


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

f:id:KishikawaKatsumi:20140211182633p:image:w768

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

実際は完全に移行したわけではなくて、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」の証明書と秘密鍵です。

f:id:KishikawaKatsumi:20140211164414p:image:w480

f:id:KishikawaKatsumi:20140211164413p:image:w480


そして、上記のコマンドの前に、一時的なキーチェーンを作成してこれらの各種証明書を追加します。

また、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#

2014-02-03

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


サンプルコード

kishikawakatsumi/JavaScriptBridge ? GitHub

kishikawakatsumi/downloadable-ios-apps ? GitHub


関連記事

JavaScriptでiOSアプリが書けるライブラリJavaScriptBridgeを公開しました - 24/7 twenty-four seven

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

FxOS用に記述したWebアプリをJavaScriptCoreを使ってiOSに移植してみた - Qiita

2014-01-17

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



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


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


f:id:KishikawaKatsumi:20140117022010p:image:w480


↑ 起動時のオプション引数は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の値が配列なので配列を渡す必要があり、配列のプロパティリストでの表現形式はカッコで囲む必要があるからです。

2014-01-15

One-line fix for UITextView on iOS 7

【参考】untitled - iOS 7 のテキスト入力欄(UITextView)の問題について

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]));
    }
}

まっとうな不具合の修正や代替のTextViewライブラリなど

2014-01-04

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;

2013-12-22

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

2013-11-18

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

2013-11-08

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

20131108031636 20131108031636


より詳しい記事はこちら ↓

iOS 7はアクセスガイド(Guided Access)が便利にカスタマイズできるって知ってました?(1/2) - iOS 7 徹底解剖 - Mobile Touch - モバイル/タブレット開発者およびデザイナー向け情報ポータル


アクセスガイドとは

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の更新や対応する処理をします。


以上です。いかがでしょうか。

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

2013-11-07

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

f:id:KishikawaKatsumi:20131107010714p:image:w512


入力に親指シフト(NICOLA)配列のキーボードが使えるエディタアプリです。

iOSではシステムキーボードを置き換える`inputView`プロパティというAPIが提供されているので、それを利用しています。

なので、どこでも親指シフトのキーボードが使えるというわけではなく、このアプリを使う場合だけ、ということになります。


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


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


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


現状はまだまだ機能も少ないのでこれから改善していきたいと考えています。

アプリ内に意見を投稿するフォームも用意していますので要望などいただけたらうれしいです。

よろしくお願いします。


20131107010430 N+Note for NICOLA - Katsumi Kishikawa

2013-10-31

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、サーバーサイドともに積極的に募集中です。

興味のあるかたは私までお声がけください。

2013-10-28

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

参考

iOS 7 で使えるフォント名一覧 - Over&Out その後


出力の方法

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


参考

SteveX Compiled » Blog Archive » Downloadable Fonts in iOS 7

no title

2013-10-24

チュートリアルなどでUIPageControlを使うときは標準のアクションに対応するのを忘れずに


f:id:KishikawaKatsumi:20131024034422p:image


UIPageControlはiPhoneのホーム画面でも使われている、今何ページ目かを示すUIControlのサブクラスです。

最初のiOSからあって、特徴的なUIなのでフリックでページをめくる画面ではこれを使って現在のページを示すのが定番になっています。

特に最近では初回起動時のチュートリアル画面でよく使われます。


ただ、意外と経験のあるひとが書いたものでも、このコンポーネントがタップによって値が変わるコントロールであることを忘れているのをけっこう見ます。

これを忘れると、UIPageControlのドットのところをタップすると、ドットの場所は変わるのに画面は変わらないので、ちょっとマヌケな感じになってしまいます。


UIPageControlはUISliderなどと同様にUIControlのサブクラスなので、基本的にユーザーの操作によって値が変わるコントロールです。

見た目に特徴があるので、つい装飾のためだけのコンポーネントだと思ってしまいますが、タップによってページを切り替えることを想定されています。


↓ 下記はアップルが提供しているUIPageControlの使い方を示すサンプルコードです。

UISliderなどと同様に、タップで値が変わったときはUIControlEventValueChangedのイベントが発生するので、そのイベントに対応したアクションで処理をします(このサンプルコードでは`changePage:`)。


PageControl

- (void)gotoPage:(BOOL)animated
{
    NSInteger page = self.pageControl.currentPage;
 
    // load the visible page and the page on either side of it (to avoid flashes when the user starts scrolling)
    [self loadScrollViewWithPage:page - 1];
    [self loadScrollViewWithPage:page];
    [self loadScrollViewWithPage:page + 1];
    
    // update the scroll view to the appropriate page
    CGRect bounds = self.scrollView.bounds;
    bounds.origin.x = CGRectGetWidth(bounds) * page;
    bounds.origin.y = 0;
    [self.scrollView scrollRectToVisible:bounds animated:animated];
}
 
- (IBAction)changePage:(id)sender
{
    [self gotoPage:YES];    // YES = animate
}

ただ、実際はUIPageControlのドットのところがタップできることを知らないひとも多いので、チュートリアルなどで単に装飾のために使ってるのであれば、`pageControl.userInteractionEnabled = NO;`などとして、タップに反応しないようにしてしまうのも手かと思います。

2013-09-27

CoreTextを使って簡単に画像付きリッチテキストを表示できるSECoreTextViewに編集機能がつきました。

kishikawakatsumi/SECoreTextView ? GitHub

iOS/Macの両方で使えて、文字の選択やリンクのクリックに対応したテキストビューをテスト公開しました。 - 24/7 twenty-four seven



前に書いたSECoreTextViewに編集機能を実装しました (iOSのみ)。


SECoreTextViewはCoreTextを使って簡単にクリッカブルなリンクや画像付きのリッチテキストを表示できるテキストビューの代替実装としてのライブラリです。


以前のものはそこそこ簡単に豊かな表現ができるのでこれはこれでけっこう実用的だったと思います。

↓ このように画像を含めたテキストを表示したり、リンクはクリックに反応して任意の処理をすることができます。

画像に限らず、画面に表示できるものはボタンでもその他のビューでもブロックを渡して任意の描画をすることも可能です。


iOS ScreenShot 1 iOS ScreenShot 1


そんな感じで、表示のみなら標準のUITextViewやUIWebViewをがんばって使うよりは柔軟で取り回しやすいのでけっこう便利に使っていたのですが、だんだん表示だけでは物足りなくなってきたので編集できるようにしてみました。

iOS ScreenShot 1 20130927032539

これまでの実装に加えて、UITextInput/UIKeyInput Protocol を実装して、標準のテキストビューと同じようにキーボードや日本語変換システムの入力を処理しています。


たいていの画面に表示できるものは扱えるので、わりと万能なテキスト編集コンポーネントになったんじゃないかなとおもいます。

だいたいの動きがわかるムービーを作ったのでこちらもどうぞ。

SECoreTextView Demo on Vimeo


UITextInputの実装やCoreTextはいろいろおもしろかったので、技術的なところはまた機会をみて書こうと思います。

今後はパフォーマンス・チューニングやOS Xのほうの実装などを予定しています。あ、それと音声入力への対応ですね。


メッセージアプリやブログエディタなどに、応用がきいて使いやすいと思いますので、いろいろ実戦投入していただけるとうれしいです。

今のうちなら何かあったらけっこうすぐに私が対応できると思います。よろしくお願いします。

2013-08-12

複雑な正規表現を分かりやすくするライブラリ VerbalExpressions の Objective-Cバージョンを書きました

https://github.com/VerbalExpressions/ObjectiveCVerbalExpressions

↑ 本家にマージされました。

https://github.com/kishikawakatsumi/ObjectiveCVerbalExpressions


概要

VerbalExpressions はメソッドチェーンとわかりやすい名前を使って、正規表現を読みやすくしようという試みです。

↓ オリジナルはJavaScriptのライブラリのようです。

https://github.com/VerbalExpressions/JSVerbalExpressions


iOS Dev Weeklyの106号でObjective-Cの移植はまだ無いみたいに書いてあったので、やってみました。

(実際は2つほど先に書かれたものがありました)

↓ 私が書いた Objective-C 版のライブラリを使うと下記のように記述できます。

// Create an example of how to test for correctly formed URLs
VerbalExpressions *verEx = [VerbalExpressions instantiate:^(VerbalExpressions *ve) {
    ve.startOfLine(YES)
    .then(@"http")
    .maybe(@"s")
    .then(@"://")
    .maybe(@"www")
    .anythingBut(@" ")
    .endOfLine(YES);
}];

最初のインスタンス化は Blocks 付きのメソッドを使わずに普通にインスタンス化することもできます。

(alloc] init] や new を使ってもいいでしょう)

VerbalExpressions *verEx = [VerbalExpressions expressions];
verEx.startOfLine(YES).then(@"http").maybe(@"s").then(@"://").maybe(@"www").anythingBut(@" ").endOfLine(YES);

仕組みについて

通常 Objective-C とこういったメソッドチェーンを多用する、いわゆる「流れるようなインターフェース (fluent interface)」はあまり相性がよくありません。

例えば、別の人の書かれた Objective-C 版の VerbalExpressions ですが、普通にメソッドチェーンを使って書くと下記のようになります。

https://github.com/sakiwei/ObjectiveCVerbalExpressions

// url matches
VerbalExpressions *tester = [[[[[[[VerEX() startOfLine] then:@"http"] maybe:@"s"] then:@"://"] maybe:@"www."] anythingBut:@" "] endOfLine];

↑ 読みやすくないこともないですが、書きやすくはないですよね。

普通の Objective-C のメソッド呼び出しは両側にカッコを追加して行かなければならないので、チェーンを追加しようとすると最初に戻って開きカッコを追加したりしないといけないのでこのやり方だと、考えながら書くっていうのが難しいです。


なので今回は Blocks を利用して他のライブラリと同様にドットでチェーンできるようにしてみました。

VerbalExpressions *verEx = [VerbalExpressions expressions];
verEx.startOfLine(YES).then(@"http").maybe(@"s").then(@"://").maybe(@"www").anythingBut(@" ").endOfLine(YES);

方法としては VerbalExpressions クラスに自分自身を戻り値として返すブロックをプロパティとして定義しています。

@interface VerbalExpressions : NSObject

@property (nonatomic, readonly) VerbalExpressions *(^startOfLine)(BOOL enable);
@property (nonatomic, readonly) VerbalExpressions *(^endOfLine)(BOOL enable);
@property (nonatomic, readonly) VerbalExpressions *(^find)(NSString *value);
@property (nonatomic, readonly) VerbalExpressions *(^then)(NSString *value);
@property (nonatomic, readonly) VerbalExpressions *(^maybe)(NSString *value);
@property (nonatomic, readonly) VerbalExpressions *(^anything)();
@property (nonatomic, readonly) VerbalExpressions *(^anythingBut)(NSString *value);

プロパティに実装は下記のようになっていて、プロパティを参照するとブロックが実行されて正規表現が組み立てられるというしくみです。そしてこのブロックは自分自身のインスタンスを返すので、その戻り値に対してドットでチェーンできる、というように書かれています。

- (VerbalExpressions *(^)(NSString *))maybe
{
    return ^VerbalExpressions *(NSString *value) {
        value = [self sanitize:value];
        self.add([NSString stringWithFormat:@"(%@)?", value]);
        return self;
    };
}

この方法の課題としてはオーバーロードができないので、例えばオリジナルの JS ライブラリでは `startOfLine()` と `startOfLine(bool)` という 2 つのメソッドがあるのですが、Blocks のプロパティだとどちらも同じ名前になってしまうので、引数付きのものだけ用意されています。


実際に有用かどうかは使いどころによると思いますが、おもしろい試みだと思いますので、ぜひ使ってみてください。

バグレポートや Pull Request もお待ちしています。

2013-06-06

OS X 10.8.4でXcodeでiOS Simulatorを実行したときにSIGABRTでアプリがクラッシュすることがある問題の暫定的な対処(修正済み)

先日のアップデートでOS Xを10.8.4にしてから、XcodeからアプリケーションをiOS Simulatorで実行したときにSIGABRTでアプリが起動せずにクラッシュすることが多くなってしまいました。

私の環境だとアプリを終了させてから再度実行、という手順だと数回に1回、実行中に⌘+Rで再起動という方法だと100%クラッシュしてしまいました。


Developer Forumsの情報からデバッガをLLDBからGDBに変更すると発生しないことを確認しました。

f:id:KishikawaKatsumi:20130606180337p:image:w400


もしくはいったん終了 (⌘+.) させてから再度実行 (⌘+R) する場合は数回に1回くらいの頻度だったので、少し面倒ですがそれでもいいかもしれません。

Xcode 4.6.3 で修正されました。

2013-06-05

UITextView でタップ可能なリンクをカスタマイズする

UITextView では dataDetectorTypes を設定することでデータタイプに応じて自動的にクリック可能なリンクとして表示してくれます。

例えば下記のようにしていすると、URLが含まれていた場合、タップ可能なリンクとして表示されます。

cell. tweetTextView. dataDetectorTypes = UIDataDetectorTypeLink;

f:id:KishikawaKatsumi:20130605020428p:image:w320


他にも次のようなデータタイプが用意されていて、電話番号、住所、イベント(日付や「今週」「今夜」など)っぽい文字列をリンクにすることができます。

typedef NS_OPTIONS(NSUInteger, UIDataDetectorTypes) {
    UIDataDetectorTypePhoneNumber   = 1 << 0,          // Phone number detection
    UIDataDetectorTypeLink          = 1 << 1,          // URL detection    
#if __IPHONE_4_0 <= __IPHONE_OS_VERSION_MAX_ALLOWED
    UIDataDetectorTypeAddress       = 1 << 2,          // Street address detection
    UIDataDetectorTypeCalendarEvent = 1 << 3,          // Event detection
#endif    

    UIDataDetectorTypeNone          = 0,               // No detection at all
    UIDataDetectorTypeAll           = NSUIntegerMax    // All types
};

ただ、任意の文字列をリンクにすることができなかったり、リンクをタップした時の処理があらかじめ決まったものに固定されている(Safariを開く、電話をかける、カレンダーに予定を登録する、など)など、実際に使ううえではかなり制限があります。

今回、こちらのリッチテキストビューライブラリ SECoreTextView を書くにあたって、たまたま使えそうなテクニックをいくつか発見したので紹介します。

ただし、それらのテクニックを使っても標準の UITextView で頑張れる限界はけっこうすぐ来るので、凝ったことをする場合には前述のライブラリなど別の手段で解決するのがいいと思います。


任意の文字列をタップ可能なリンクにする (iOS 6〜)

iOS 6 から UIKit のコンポーネントのいろいろなテキストに NSAttributedString が使えるようになりました。

UITextView にも attributedText というプロパティが追加され、スタイルを指定できるようになったのでそれを使います。


実は OS X には 10.0 の頃から NSLinkAttributeName という属性があります。

読んで字のごとく、文字列にリンク属性を付加します。


ただ NSLinkAttributeName はなぜか iOS には用意されていません。


しかし、たまたま発見したのですが NSLinkAttributeName 定数が宣言されていないだけで、NSLinkAttributeName が表す文字列を直接指定してみると iOS でも機能することがわかりました。


NSLinkAttributeName は文字列定数で @"NSLink" と定義されています。

そこで、次のように @"NSLink" + リンク文字列という形で属性を指定します。

↓ 下記の例は Twitter のツイートに含まれる @screen_name とハッシュタグをリンク属性として指定しています。

NSArray *textEentities = [TwitterText entitiesInText:text];
for (TwitterTextEntity *textEentity in textEentities) {
    if (textEentity.type == TwitterTextEntityScreenName) {
        NSString *screenName = [text substringWithRange:textEentity.range];
        [attributedString addAttributes:@{@"NSLink": screenName}
                                  range:textEentity.range];
    } else if (textEentity.type == TwitterTextEntityHashtag) {
        NSString *hashTag = [text substringWithRange:textEentity.range];
        [attributedString addAttributes:@{@"NSLink": hashTag}
                                  range:textEentity.range];
    }
}

cell.tweetTextView.attributedText = [[SETwitterHelper sharedInstance] attributedStringWithTweet:tweet];

↓ 実行結果は次のようになります。URLに加えてメンションやハッシュタグがリンクになっているのがわかるでしょうか。

各リンクはすべてタップ可能です。

f:id:KishikawaKatsumi:20130605022744p:image:w320


ただし、これらのリンクのうち、@"NSLink" を指定して作ったリンクについてはタップしても Safari を開いたりはしてくれません。(開く URL も無いので当然ですが)

かろうじて長押しすると次のようなアクションシートが表示されて文字列のコピーができる、というような動作をします。

(Open は何も起こらない)

f:id:KishikawaKatsumi:20130605023440p:image:w320


これでは実用にできませんので次にリンクの処理をカスタマイズする方法を紹介します。


リンクをタップしたときに任意の処理を実行する

Data Detector による自動リンク化ではあらかじめ決まった処理がシステムによって実行されるということは前に述べました。

また、@"NSLink" によるリンク化もそれだけでは実用的ではないことがわかりました。


そこで、既定の処理をフックすることでタップ時に任意の処理を実行するという方法を紹介します。

実は自動リンク化された URL のリンクをタップしたときは UIApplication の openURL: メソッドが呼ばれます。

つまり openURL: メソッドをオーバーライドすれば、そこで任意の処理を実行することができます。


次のように UIApplication のサブクラス Application を作成し、openURL: メソッドをオーバーライドします。

とりあえず、URLをログ出力するように変更します。

@interface Application : UIApplication

@end

@implementation Application

- (BOOL)openURL:(NSURL *)url
{
    NSLog(@"%@", url.absoluteString);
    return NO;
}

@end

アプリケーションクラスは UIApplicationMain 関数で指定されているので main.m を次のように変更します。

int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, @"Application", NSStringFromClass([AppDelegate class]));
    }
}

これでアプリケーションクラスは UIApplication ではなく、Application が使われるようになりました。


ここまでで URL のリンクをタップしたときは カスタマイズしたメソッドが呼ばれるようになるのですが、実は @"NSLink" でリンク化したメンションやハッシュタグのリンクは openURL: メソッドが呼ばれません。


というのも @"NSLink" で単に文字列を指定した場合は自動的に applewebdata://[UUID]/[リンク文字列] のような URL としてリンク化されるので、applewebdata は開けるスキーマではないため、openURL: メソッドが呼ばれないのです。


ただし、ここまでわかっていれば問題は難しくありません。

要は開けるような URL にしてしまえばよいのです。


ということで @"NSLink" に対する値をちょっと変更して、適当なスキーマを付けてしまいます。

openURL: が呼ばれるもので、他にリンク文字列として使用されないものがいいでしょう。

ここではメンションに ftp を、ハッシュタグに maps を指定します。

NSArray *textEentities = [TwitterText entitiesInText:text];
for (TwitterTextEntity *textEentity in textEentities) {
    if (textEentity.type == TwitterTextEntityScreenName) {
        NSString *screenName = [text substringWithRange:textEentity.range];
        [attributedString addAttributes:@{@"NSLink": [NSString stringWithFormat:@"ftp:%@", screenName]}
                                  range:textEentity.range];
    } else if (textEentity.type == TwitterTextEntityHashtag) {
        NSString *hashTag = [text substringWithRange:textEentity.range];
        [attributedString addAttributes:@{@"NSLink": [NSString stringWithFormat:@"maps:%@", hashTag]}
                                  range:textEentity.range];
    }
}

これで、 メンションやハッシュタグのリンクをタップしたときも openURL: が呼ばれるようになり、スキーマによって何がタップされたのかも区別できるようになりました。

- (BOOL)openURL:(NSURL *)url
{
    NSString *scheme = url.scheme;
    if ([scheme hasPrefix:@"http"]) {
        // 通常のリンクの処理
        NSLog(@"%@", url.absoluteString);
    } else if ([scheme isEqualToString:@"ftp"]) {
        // メンションの処理
        NSLog(@"%@", url.absoluteString);
    } else if ([scheme hasPrefix:@"maps"]) {
        // ハッシュタグの処理
        NSLog(@"%@", url.absoluteString);
    }
    return NO;
}

リンクの書式を変更する (iOS 6〜)

せっかくいろいろなリンクを作れるようになったのですから、標準の青い文字色とアンダーラインでは物足りないですよね。

NSAttributedString はそもそもリッチテキストを表現するためのオブジェクトなので NSAttributedString でスタイルを指定することによってリンクの書式をカスタマイズすることができます。


メンションのリンクを赤色に、ハッシュタグをグレーの太字に変えてみます。

NSArray *textEentities = [TwitterText entitiesInText:text];
for (TwitterTextEntity *textEentity in textEentities) {
    if (textEentity.type == TwitterTextEntityScreenName) {
        NSString *screenName = [text substringWithRange:textEentity.range];
        [attributedString addAttributes:@{
         @"NSLink": [NSString stringWithFormat:@"ftp:%@", screenName],
         NSForegroundColorAttributeName: [UIColor redColor]}
                                  range:textEentity.range];
    } else if (textEentity.type == TwitterTextEntityHashtag) {
        NSString *hashTag = [text substringWithRange:textEentity.range];
        [attributedString addAttributes:@{
         @"NSLink": [NSString stringWithFormat:@"maps:%@", hashTag],
         NSForegroundColorAttributeName: [UIColor grayColor],
                    NSFontAttributeName: [UIFont boldSystemFontOfSize:14.0f]}
                                  range:textEentity.range];
    }
}

↓ 実行結果は下のようになります。メンションとハッシュタグの書式が変わっているのがわかるでしょうか。

もちろんタップ可能なのは変わりません。

f:id:KishikawaKatsumi:20130605031833p:image:w320


残念ながら、アンダーラインを消すことはできないようです。NSUnderlineStyleAttributeName をゼロに指定してみたのですが、効果はありませんでした。


それではここまでのコードを共有しておきます。

kishikawakatsumi/TextViewLinks ? GitHub


解決が難しい問題

ただ、ここまでがんばっても UITextView を使う以上解決が難しい問題が残ります。

例えば、今回のテーブルビューセルに使うような場合だと、テキストビューの置いてあるところはテキストビューにタッチが取られてセルの選択ができない(テキストビューの userInteractionEnabled を NO にするとセルの選択はできるようになるが、今度はリンクがタップできなくなる)ことや、セルの選択をしたときに文字がハイライト色に変わらない(highlightedAttributedText のようなプロパティがあれば…)、などがあります。


これらの問題についてもがんばれば何とかなりそうですが、それをやろうとすると、そろそろコストが釣り合わないかなという気がします。

なのでそれ以上凝ったことをしたり見た目にこだわるのであれば、下記のようなサードパーティのライブラリの使用を検討してみたらいいのではないかと思います。


SECoreTextView は Mac/iOS の両方で簡単にリッチテキストやクリック可能なリンクを扱うことのできるライブラリです。

さらに任意の画像やビューを文字列と同様に取扱うこともできますので UIWebView のライトウェイトな代替コンポーネントとしても使用することができます。

kishikawakatsumi/SECoreTextView ? GitHub

2013-05-27

第4回iphone_dev_jp 東京iPhone/Mac勉強会を開催しました

第4回 iphone_dev_jp 東京iPhone/Mac勉強会 : ATND


しばらく休んでいたのですが久しぶりにいつもおなじみのVOYAGE GROUPさんの会場をお借りして開催いたしました。


今回は幸運なことにEvernote本社からMac版EvernoteのUIのリニューアルの指揮をされましたJack Hirschさんにきていただき、背景や開発手法などをお話していただきました。

大勢のかたにきていただき、質問が飛び交う活発な会することができてよかったと思います。

Jack Hirschさんおよび、Evernoteのかたがたも参加者の熱意に感銘を受けたと言っていただけました。


通常の発表においても、興味深い内容の話ばかりで、参加していただいたかたには満足いただけたのではないでしょうか。


ホストしていただいたVOYAGE GROUPの @lesamoureuses さん、@huin さんはじめ、手伝っていただいたかた、発表してくださったかた、参加してくださったかた、どうもありがとうございました。


今度はまたハッカソンやりたいね、という話を聞いたので次はハッカソンかなーと思っています。

来月のiOS 7の発表次第でUIコンポーネントはいろいろ必要なものが出てくると思うので、iOS7時代のUIコンポーネントを書く、みたいなのがいいかなと思っています。


また、今回発表してくださった id:ninjinkun の所属する株式会社はてなでは毎年恒例のサマーインターンの募集が始まっています。


はてなのインターンシッププログラムは非常にしっかりした内容でレベルが高いことで有名で、はてなのインターンに来た学生が最終的に他の企業からひっぱりだこになってしまって困っているみたいな話もよく聞きます。

id:ninjinkun によると今回はサーバーサイドだけでなくiPhoneアプリケーションのカリキュラムもあるということなので、両方をバランスよく経験できると貴重な機会だと思います。

参加資格のある学生のかたはぜひ応募してみると良いと思います。

株式会社はてな


発表内容と資料のまとめ

EasyStyleGKさんObjective-C atomicity
_ishkawaさんiOS5で動くUIRefreshControlの作り方
cocoponさんフラットデザインの話
novi_さんクライアント系iOSアプリのつくりかた(実装と開発プロセスの話)
Jack HirschさんEvernote のユーザエクスペリエンスアプローチ
ninjinkunさんNJKWebViewProgressについて
kishikawa katsumiMac/iOSのリッチテキストの表示について

↓ Slidrsで共有した資料

第4回 iphone_dev_jp 東京iPhone/Mac勉強会 - Slidrs


Tweetのまとめ

第4回 iphone_dev_jp 東京iPhone/Mac勉強会 #idevjp - Togetterまとめ


参加者のかたのリポートなど

「第4回 iphone_dev_jp 東京iPhone/Mac勉強会」に参加して来ました - 強火で進め

第4回 iphone_dev_jp 東京iPhone/Mac勉強会に参加しました

iOSとフラットデザインについて、第4回 iphone_dev_jp勉強会で発表してきました | ここぽんのーと

第4回 iphone_dev_jp 東京iPhone/Mac勉強会まとめ #idevjp - ぬんびりぶろぐ

EZ-NET: Objective-C の atomicity について iphone_dev_jp 東京勉強会で発表しました

2013-05-24

UIImageView で 'Aspect Fit (UIViewContentModeScaleAspectFit)' を指定したときの画像サイズを取得する

↓ 例えばこんなふうに UIImageView に 'Aspect Fit' を指定して表示させたときの領域を知りたいことってありますよね。

f:id:KishikawaKatsumi:20130524020123p:image:w320


がんばって計算してもいいのですが、AVFoundation.framework の次の関数で簡単に取得できます。

CGRect AVMakeRectWithAspectRatioInsideRect(CGSize aspectRatio, CGRect boundingRect);

ドキュメントによると、ムービーを CALayer に表示するときに領域にフィットさせるのに便利ということですが、画像に使っても便利です。


上記の画像だと UIImageView の大きさは 280x508 で、画像のサイズは 2047x1199 です。

そこで下記のコードを実行すると返ってくる CGRect は {{0, 171.997}, {280, 164.006}} でピッタリ画像の領域に一致します。

CGRect frame = AVMakeRectWithAspectRatioInsideRect(image.size, self.imageView.bounds);
UIView *overlayView = [[UIView alloc] initWithFrame:frame];
overlayView.layer.borderColor = [[UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:0.5f] CGColor];
overlayView.layer.borderWidth = 4.0f;

[self.imageView addSubview:overlayView];

f:id:KishikawaKatsumi:20130524021340p:image:w320


別の画像 (533x800) で実行した場合は {{0, 43.8687}, {280, 420.263}} になりました。

f:id:KishikawaKatsumi:20130524021335p:image:w320

2013-05-23

iPhone/iPadで画像をクロッピングするライブラリを公開しました

kishikawakatsumi/PEPhotoCropEditor ? GitHub

PEPhotoCropEditor は iPhone/iPad アプリに画像をクロッピングする機能を簡単に追加します。

UIは標準のPhotos.appに似ていておもしろい動きをします。

ScreenShot 1 [Movie 1


インストール

CocoaPodsでインストールできます。

pod 'PEPhotoCropEditor'

または、Lib/ ディレクトリと Resources/ ディレクトリのファイルをすべてプロジェクトにコピーして、下記のフレームワークをリンクしてください。

  • QuartzCore.framework
  • AVFoundation.framework

使い方

ビューコントローラを使う場合

(UINavigationController を使わなくても動きます。)

PECropViewController *controller = [[PECropViewController alloc] init];
controller.delegate = self;
controller.image = self.imageView.image;

UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:controller];
[self presentViewController:navigationController animated:YES completion:NULL];

ビューを直接使う場合
self.cropView = [[PECropView alloc] initWithFrame:contentView.bounds];
[self.view addSubview:self.cropView];

Cropping された画像を取り出す
デリゲートメソッドから
- (void)cropViewController:(PECropViewController *)controller didFinishCroppingImage:(UIImage *)croppedImage
{
    [controller dismissViewControllerAnimated:YES completion:NULL];
    self.imageView.image = croppedImage;
}

ビューのプロパティから
UIImage *croppedImage = self.cropView.croppedImage;

2013-04-22

iOS/Macの両方で使えて、文字の選択やリンクのクリックに対応したテキストビューをテスト公開しました。

kishikawakatsumi/SECoreTextView ? GitHub

iOS ScreenShot 1


OS X ScreenShot 1


SECoreTextView はリッチテキストの表示と文字の選択(現在はOS Xのみ)やリンクがクリック可能だったりするテキストビューです。

別のアプリでテーブルビューのセルにリンクを含むテキストを表示するのに、既存のものでMacで使えるいい感じのものが今ひとつ見つからなかったので書きました。

OS X で使うだけだとなんなので、せっかくだから iOS にも対応してみました。

UITableVIewやNSTableVIewのセルで使うと便利だと思います。

iOS のほうは半日くらいでちょちょっと書いただけなのでおかしなところが結構あると思うので見つけたら教えてください。

2012-12-29

iPhone の画面操作を録画するライブラリを公開しました。

kishikawakatsumi/ScreenRecorder ? GitHub

ScreenRecorder は iOS デバイスの画面を連続的にキャプチャして、動画に変換することで画面の操作を録画することができる機能をアプリケーションに追加します。

開発中のソフトウェアのユーザーテストなどに利用すると効果的です。


使い方

1. 以下のファイルをプロジェクトに追加します

  • Lib/SRScreenRecorder.h
  • Lib/SRScreenRecorder.m
  • Vendor/KTouchPointerWindow.h
  • Vendor/KTouchPointerWindow.m

2. 以下のフレームワークをリンクします

  • QuartzCore.framework
  • CoreVideo.framework
  • CoreMedia.framework
  • AVFoundation.framework

startRecording で録画を開始します。

デフォルトの設定は

  • バックグラウンドに入ったときに自動保存
  • 10 分ごとに自動保存、ファイルのローテート
  • 30FPSで録画
  • タッチ箇所の表示

となっています。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[SRScreenRecorder sharedInstance] startRecording];
    return YES;
}

いくつかの挙動は、設定で変更することができます。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    SRScreenRecorder *recorder = [SRScreenRecorder sharedInstance];
    recorder.frameInterval = 1; // 60 FPS
    recorder.autosaveDuration = 1800; // 30 minutes
    recorder.showsTouchPointer = NO; // hidden touch pointer
    recorder.filenameBlock = ^(void) {
        return @"screencast.mov";
    }; // change filename
    
    [recorder startRecording];
    
    return YES;
}

タッチ・ポインターの表示には @ さんの KTouchPointerWindow を利用しています。

no title

itok/KTouchPointerWindow ? GitHub

2012-11-08

Windows 8 StoreアプリケーションでGoogleAnalyticsを使う

kishikawakatsumi/GoogleAnalytics-for-WinJS ? GitHub

Windows 8の Store アプリケーションで GoogleAnalytics を使う場合、まだ公式の SDK はありませんが、サードパーティからすでにいくつかライブラリがリリースされているのでそれを使うのがいいと思います。

ただ JavaScript(WinJS) で書いた Metro Style アプリケーション用のものはまだ無いみたいなので Windows Runtime Component を使って WinJS から C# のライブラリを扱う方法でやってみました。


Google Analytics for WinRT - Home

NuGet Gallery | Google Analytics for WinRT (Windows 8 and 8.1) apps 2.5.0

↑ GoogleAnalytics にリクエストを送る Windows 8 用のライブラリはいくつかありますが、今回はソースコードも公開されていて実装もシンプルな Google Analytics for WinRT - Home を使いました。


Windows Runtime Component のプロジェクトをソリューションに追加する

↓ 詳しい方法は下記のドキュメントを見てください。

Walkthrough: Creating a simple component in C# or Visual Basic and calling it from JavaScript

チュートリアル : C# または Visual Basic での単純なコンポーネントの作成および JavaScript による呼び出し

  1. Solution Explorer でソリューションを右クリックして、Add > New Project... と選択します。
  2. Visual C# > Windows Runtime Component と選択して、プロジェクト名を適当につけて [OK] を押します。
  3. Solution Explorer でWindows Store アプリケーションのプロジェクトを右クリックして、Add Reference... を選択します。
  4. Solution > Projects と選択して、先ほどの Windows Runtime Component として作ったプロジェクトにチェックをつけて [OK] を押します。

f:id:KishikawaKatsumi:20121108142646p:image

↑ ここまででこんな感じになります。


GoogleAnalytics のライブラリをプロジェクトに追加する

先ほどの Windows Runtime Component のプロジェクトから利用できるように GoogleAnalytics のライブラリを追加します。

  1. Solution Explorer でプロジェクトを右クリックして、Add > New Folder と選択します。
  2. そのフォルダに 'Nascent.GoogleAnalytics.dll' をコピーします。
  3. Solution Explorer で Windows Runtime Component のプロジェクトを右クリックして、Add Reference... を選択します。
  4. [Browse...] ボタンを押して 'Nascent.GoogleAnalytics.dll' を選択します。
  5. 'Nascent.GoogleAnalytics.dll' にチェックをつけて [OK] を押します。

ライブラリの呼び出しをブリッジするクラスを書く

↓ こんな感じで。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Nascent.GoogleAnalytics;

namespace WindowsRuntimeGoogleAnalytics
{
    public sealed class Tracker
    {
        public string webPropertyId { get; set; }

        public Tracker(string id)
        {
            webPropertyId = id;
        }

        public void SetCustomVariable(int index, string name, string value)
        {
            AnalyticsTracker tracker = AnalyticsTracker.GetInstance(webPropertyId);
            tracker.SetCustomVariable(index, name, value);
        }

        public void TrackPageView(string page)
        {
            AnalyticsTracker tracker = AnalyticsTracker.GetInstance(webPropertyId);
            tracker.TrackPageView(page);
        }

        public void TrackEvent(string category, string action)
        {
            AnalyticsTracker tracker = AnalyticsTracker.GetInstance(webPropertyId);
            tracker.TrackEvent(category, action, null, null);
        }

        public void TrackEvent(string category, string action, string label)
        {
            AnalyticsTracker tracker = AnalyticsTracker.GetInstance(webPropertyId);
            tracker.TrackEvent(category, action, label, null);
        }
    }
}

JSのインターフェースを書く

↓ 直接呼んでもいいけどまとまってると何かと使いやすいのでこんな感じで。

(function () {
    "use strict";

    var Tracker = WinJS.Class.define(
        function (webPropertyId) {
            this.webPropertyId = webPropertyId;
            this.tracker = new WindowsRuntimeGoogleAnalytics.Tracker(webPropertyId);
        },

        {
            setCustomVariable: function (index, name, value) {
                var that = this;
                that.tracker.setCustomVariable(index, name, value);
            },

            trackPageView: function (page) {
                var that = this;
                that.tracker.trackPageView(page);
            },

            trackCurrentPageView: function () {
                var that = this;
                var page = Application.navigator.pageControl.uri.replace("ms-appx://" + Windows.ApplicationModel.Package.current.id.name.toLowerCase(), "");
                that.trackPageView(page);
            },

            trackEvent: function (category, action, label) {
                var that = this;
                that.tracker.trackEvent(category, action, label);
            }
        });

    WinJS.Namespace.define("GoogleAnalytics", {
        Tracker: new Tracker("UA-4291014-7")
    });
})();

WinJS から呼び出してみる

↓ たとえば、画面が表示されるたびにアクセスを記録するならこんな感じで。

ready: function (element, options) {
    GoogleAnalytics.Tracker.trackCurrentPageView();

    var listView = element.querySelector(".groupeditemslist").winControl;
    listView.groupHeaderTemplate = element.querySelector(".headertemplate");
〜(略)〜

↓ 今回のサンプルコードはこちらです。

kishikawakatsumi/GoogleAnalytics-for-WinJS ? GitHub

2012-10-24

リンクするだけで iOS 6 で Google Map が使えるようになる YAMapKit を公開しました。

kishikawakatsumi/YAMapKit ? GitHub

YAMapKit は MapKit.framework と(ほぼ)互換性のある代替ライブラリです。

Google Maps Javascript API と UIWebView を利用して iOS 6 で Apple の標準地図の代わりに Google Map を使った表示ができます。

MapKit.framework と(ほぼ)互換性があるのでリンク先を差し替えるだけで動作します(たいていの場合は)。

あまりヘビーな利用には向きませんが、アプリケーションの中でちょっと MapKit を使って地図を表示したりピンを挿したりしているという場合に効果的です。


まだ未サポートの機能がたくさんあるので、手伝ってくれる方や、バグレポート、要望を歓迎します。

使い方

  1. MapKit.framework のリンクを外します。
  2. libMapKit.a をリンクします。
  3. CoreLocation.framework をリンクします。

できないこと

  • ジオコーディング(代わりに 'CLGeocoder' を使ってください)
  • カスタムビューのオーバーレイ表示(組み込みのオーバーレイ (MKPolylineView, MKCircleView など) しか使えません)
  • アノテーションのドラッグ&ドロップ
  • アノテーションのコールアウトを表示したあとで更新する

(たぶん他にもいっぱいあります)


利用例

f:id:KishikawaKatsumi:20121024221827p:image:w200 f:id:KishikawaKatsumi:20121024221820p:image:w200 f:id:KishikawaKatsumi:20121024221831p:image:w200

f:id:KishikawaKatsumi:20121024221835p:image:w200 f:id:KishikawaKatsumi:20121024221839p:image:w200 f:id:KishikawaKatsumi:20121024221843p:image:w200

2012-10-23

Objective-C でサブクラスのインスタンスから任意のスーパークラスのメソッドを呼ぶ

サブクラスのインスタンスからポリモーフィズムを無視して任意のスーパークラスのメソッドを呼びます。


↓ 下のように Shape クラスと Shape クラスを継承した Path クラス、および Path クラス を継承した Circle があります。

それぞれのクラスで draw メソッドをオーバーライドしています。

////////////////////////////////////////////////////////////////////////
#pragma mark - Shape
////////////////////////////////////////////////////////////////////////
@interface Shape : NSObject
@end

@implementation Shape

- (void)draw {
    NSLog(@"%@", @"Shape.");
}

@end

////////////////////////////////////////////////////////////////////////
#pragma mark - Path
////////////////////////////////////////////////////////////////////////
@interface Path : Shape
@end

@implementation Path

- (void)draw {
    NSLog(@"%@", @"Path.");
}

@end

////////////////////////////////////////////////////////////////////////
#pragma mark - Circle
////////////////////////////////////////////////////////////////////////
@interface Circle : Path
@end

@implementation Circle

- (void)draw {
    NSLog(@"%@", @"Circle.");
}

@end

↓ Circle クラスのインスタンスから draw メソッドを呼び出すと Circle クラスの draw メソッドが実行されて "Circle." と出力されます。

たまに多態性を無視してスーパークラスや、スーパークラスのさらにスーパークラスのメソッドを実行したいということってありますよね。

そういうときは 対象メソッドの IMP (メソッドを参照する関数へのポインタ) を使います。

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    Shape *shape = [[Circle alloc] init];
    [shape draw]; // => Circle.
    
    SEL selector = @selector(draw);
    
    void(*pathFunction)(id, SEL, ...) = (void(*)(id, SEL, ...))[Path instanceMethodForSelector:selector];
    pathFunction(shape, selector); // => Path.
    
    void(*shapeFunction)(id, SEL, ...) = (void(*)(id, SEL, ...))[Shape instanceMethodForSelector:selector];
    shapeFunction(shape, selector); // => Shape.
}

@end

↑ IMP の定義のままだと ARC が戻り値を retain しようとするので、戻り値が void の関数ポインタにキャストしています。


↓ 上記のコードの出力は下記になります。

2012-10-23 13:15:20.632 Monomorphism[50033:c07] Circle.
2012-10-23 13:15:20.633 Monomorphism[50033:c07] Path.
2012-10-23 13:15:20.633 Monomorphism[50033:c07] Shape.

サブクラスのインスタンスからポリモーフィズムを無視して任意のスーパークラスのメソッドが呼べました。

↓ 試したコードの全体を載せておきます。

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@end

////////////////////////////////////////////////////////////////////////
#pragma mark - Shape
////////////////////////////////////////////////////////////////////////
@interface Shape : NSObject
@end

@implementation Shape

- (void)draw {
    NSLog(@"%@", @"Shape.");
}

@end

////////////////////////////////////////////////////////////////////////
#pragma mark - Path
////////////////////////////////////////////////////////////////////////
@interface Path : Shape
@end

@implementation Path

- (void)draw {
    NSLog(@"%@", @"Path.");
}

@end

////////////////////////////////////////////////////////////////////////
#pragma mark - Circle
////////////////////////////////////////////////////////////////////////
@interface Circle : Path
@end

@implementation Circle

- (void)draw {
    NSLog(@"%@", @"Circle.");
}

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    Shape *shape = [[Circle alloc] init];
    [shape draw]; // => Circle.
    
    SEL selector = @selector(draw);
    
    void(*pathFunction)(id, SEL, ...) = (void(*)(id, SEL, ...))[Path instanceMethodForSelector:selector];
    pathFunction(shape, selector); // => Path.
    
    void(*shapeFunction)(id, SEL, ...) = (void(*)(id, SEL, ...))[Shape instanceMethodForSelector:selector];
    shapeFunction(shape, selector); // => Shape.
}

@end

2012-10-20

iOS 6.0 と iOS 5.x の両方で動作するアプリケーションをビルドする設定

iOS 4.0 と iPhone OS 3.x の両方で動作するアプリケーションをビルドする設定 - 24/7 twenty-four seven

↑ こちらも参考に

iOS 4.0 が登場したくらいのときに上の記事を書いて、仕組みは変わってないのですけど Xcode 4.x 系だと UI が変わってるので現在のやり方をまとめます。


ベース SDK と Deployment Target を設定する

プロジェクトの "Build Settings" で "Base SDK" を "Latest iOS" にします。

前にも書きましたが、ベース SDK は最新を指定したほうがいいです。

f:id:KishikawaKatsumi:20121020113008p:image:w600


プロジェクトの "Info" で "Deployment Target" をサポートする OS の最も低いバージョンにします。

(下の場合は iOS 5.0 以降で動作する。)

f:id:KishikawaKatsumi:20121020113320p:image:w600


今なら、Base SDK 6.0 でビルドして、Deployment Target を 5.0 または 5.1 にするのが効果的でしょうか。2世代サポートということで。


新しい Framework を Weak Link (Optional) に設定する

古い環境には含まれていない Framework をリンクしていると、Dynamic Linker がシンボルのロードに失敗してアプリケーションが起動しません。

その場合は、新しい OS にのみ存在する Framework を Weak Link に設定します(今回は Social.framework)。

f:id:KishikawaKatsumi:20121020113833p:image:w600

↑ リンクの設定はプロジェクトではなくターゲットに対してしか行えないので、ターゲットを選択して "Build Phases" > "Link Binary With Libraries" から設定します。

リンクするフレームワークを設定して、 Required > Optional に変更します。


OS のバージョンごとに処理を分岐する

古い API には存在しないセレクタを呼び出したり、クラスを参照したりするとクラッシュしますので必要に応じて処理を分岐します。

- (void)tweet:(id)sender
{
    Class clazz = NSClassFromString(@"SLComposeViewController");
    if (clazz) {
        SLComposeViewController *controller = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeTwitter];
        [self presentViewController:controller animated:YES completion:nil];
    } else {
        TWTweetComposeViewController *controller = [[TWLandscapeTweetComposeViewController alloc] init];
        [self presentViewController:controller animated:YES completion:nil];
    }
}

Auto Layout に注意

Auto Layout は iOS 6 以降しか使えません。Auto Layout を使用していると iOS 5.x などで起動時に関連のクラスが無くてクラッシュします。StoryBoard を新しく追加したときなどにうっかり Auto Layout のチェックをつけたままにしてしまったりするので注意しましょう。

f:id:KishikawaKatsumi:20121020114404p:image:w300

2012-10-05

iOS 6 では Supported interface orientations の順番に注意!

f:id:KishikawaKatsumi:20121005033214p:image:w480

最近の Xcode ではアプリケーションが対応しているデバイスの向きをターゲットの Summary 画面から GUI を用いて設定できるようになりましたが、ここから設定する場合はボタンを押す順番に注意する必要があります。

というのも、この画面で設定した内容は、Info.plist の Supported interface orientations (UISupportedInterfaceOrientations) に反映されるのですが、この項目は Array の値で順番が起動時の状態に影響するからなのです。


上記の画面の状態になるように、ボタンを左から順に押していった場合、Info.plist の UISupportedInterfaceOrientations は下記のようになります。これは新規プロジェクトを作成した場合のデフォルト値です。

f:id:KishikawaKatsumi:20121005033335p:image:w480


今度は同じ状態になるように、ボタンを「右から」順に押していきます。すると Info.plist は下記のようになります。

値の順番が変わっています。

f:id:KishikawaKatsumi:20121005034024p:image:w480


実はこの順番は起動時の画面の向きに関係していて、iOS 6 では一番先頭に指定されている画面の向きで起動することになります。

つまり、前の例では縦画面で起動するのですが、後の例では横向きで起動することになります。


別のキーに Initial interface orientation (UIInterfaceOrientation) というものがあって、こちらを指定すると初期状態を指定できそうですが、試したところ iOS 6 ではどうもこの値は無視されるようです。

2012-10-04

iOS 6 ではグループスタイルのテーブルビューの背景色がこっそり非推奨になっている。

f:id:KishikawaKatsumi:20121004223424p:image:w320


iOS 6 では上記のカラーを生成するメソッドがヘッダのコメントでひそかに deprecated になっています。

UIInterface.h

// Group style table view backgrounds can no longer be represented by a simple color.
// If you want to have a background in your own view that looks like the table view background,
// then you should create an empty table view and place it behind your content.
+ (UIColor *)groupTableViewBackgroundColor; // This method will be deprecated during the 6.0 seed program

ドキュメントの記載は変わってないのですが、実際に使ってみると今までは下記のコードでピンストライプのカラーが設定されていましたが、iOS 6 だと真っ黒になってしまいます。

self.view.backgroundColor = [UIColor groupTableViewBackgroundColor];

この背景色を使いたい場合は空のテーブルビューを設定しろということなので、iOS 6 から代替のコードは下記のようになります。

UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
[self.view addSubview:tableView];

2012-10-01

はやりのシンボルフォントを iOS で画像として扱える SymbolFontKit を公開しました。

kishikawakatsumi/SymbolFontKit ? GitHub

ScreenShot1

↑ シミュレータに表示されている画像やツールバーのボタン、タブバーのアイコンは全てフォントです。

シンボルフォントとは要するにアイコン画像などをフォント形式にしたものです。

Webだと最近のブラウザだとWebフォントが使えるので、利用者の環境にフォントがインストールされていなくても使えるので、最近は解像度非依存ということもあっていろいろな Github や Twitter などいろいろなサイトで利用されています。

シンボルフォントについて詳しくは下記のリンク先などを見てください。

Ligature Symbols 〜ほんとにべんりなフォントのはなし〜

【完全版】Ligature Symbols フォントセットの自作方法 - くらげだらけ

Ligature Symbols

で、フォントなのでベクターデータのためどんな解像度でもキレイに表示されることや、1つのアイコンで色違いや別のサイズを表示することが簡単だったり、はやっているので様々なデザインのシンボルフォントが使いやすいライセンスで入手できるなど、iOS でも利用できたら便利だと思って作りました。

使い方

  1. SFKImage.h/m をプロジェクトにコピーします。
  2. 利用したいフォントファイルをプロジェクトにコピーします。
  3. 上記でコピーしたフォントファイルのファイル名を UIAppFonts をキーにして Info.plist に追加します。

API は UIImage 互換なので UIImage と同様の使い方ができます。(実は現在は imageNamed: 以外のインスタンス化はできません)

SFKImage *image = [SFKImage imageNamed:@"print"];

インスタンス化した SFKImage オブジェクトは UIImage のインスタンスと同様に UIButton や UIImageView、UITabBarItem などに直接設定することができます。

self.imageView1.image = [SFKImage imageNamed:@"share"];

UITabBarItem *calendarTabBarItem = [[UITabBarItem alloc] initWithTitle:@"calendar" image:[SFKImage imageNamed:@"calender"] tag:1];
UITabBarItem *globeTabBarItem = [[UITabBarItem alloc] initWithTitle:@"globe" image:[SFKImage imageNamed:@"globe"] tag:2];
_tabBar.items = @[calendarTabBarItem, globeTabBarItem];

フォントのレンダリングは実際に画面に描画されるときまで遅延されるので、UIImage と違って、1つのインスタンスを途中で色や大きさを変えたりできます。

SFKImage *image = [SFKImage imageNamed:@"compass"];
image.size = CGSizeMake(20, 20);
image.color = [UIColor redColor];
self.imageView6.image = image;
    
image.size = CGSizeMake(40, 40);
image.color = [UIColor yellowColor];
self.imageView7.image = image;
    
image.size = CGSizeMake(80, 80);
image.color = [UIColor blueColor];
self.imageView8.image = image;

UIImage として振る舞うためにちょっと無茶をしているのでそのまま AppStore の審査に通るかどうかは「?」ですが近いうちに適当なアプリを提出して調査したいと思います。


SymbolFontKit は第1回iphone_dev_jp東京 iPhone/Mac Hackathon 〜みんなが幸せになるハッカソン〜の成果物です。