定食屋おろポン

おろしポン酢と青ネギはかけ放題です

UITextView内のリンクをタップした時にUIActionSheetを出す

リンクやメールアドレス、住所などが含まれているUITextView内のリンクを有効にする

  • InterfaceBuilderで設定。Detectionの中から必要なものにチェックを入れる
  • コードで設定。dataDetectorTypesに必要なUIDataDetectorTypesを突っ込んでおけばOK

このどちらかで。
UIDataDetectorTypesの宣言はこんな感じ

enum {
    UIDataDetectorTypePhoneNumber   = 1 << 0,
    UIDataDetectorTypeLink          = 1 << 1,
    UIDataDetectorTypeAddress       = 1 << 2,
    UIDataDetectorTypeCalendarEvent = 1 << 3,
    UIDataDetectorTypeNone          = 0,
    UIDataDetectorTypeAll           = NSUIntegerMax
};
typedef NSUInteger UIDataDetectorTypes;

これを設定してあげると、勝手にリンクやメールを探してリンクを張ってくれます。

リンクをタップした時の挙動

ユーザがリンクをタップすると、勝手にSafariやMap、Mailなどを起動して内容を表示してくれます。便利ですね。
でも、自前のUIWebViewで開きたいとか、Safari起動するまえに確認画面出したいとか、そういった場合もあるかと思います。
リンクのタップをUITextViewDelegateで拾えるかとおもいきや、どうもUIApplicationのopenURLメソッドが呼ばれるようです。*1
仕方ないので、UIApplicationのサブクラスを作り、その中で-(BOOL)openURL:(NSURL*)urlをオーバーライドします。
実装はこんな感じ。

//  TVApplication.h
#import <UIKit/UIKit.h>

@interface TVApplication : UIApplication<UIActionSheetDelegate>
@end

//  TVApplication.m
#import "TVApplication.h"
#import "TVAppController.h"

@implementation TVApplication
{
    NSURL*     _url;
}

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    switch (actionSheet.tag) {
        // For opening url
        case 0:
            // Check url
            if (!_url) {
                break;
            }
            
            // Invoke super
            [super openURL:_url];
            
            // Release instance variable
            [_url release], _url = nil;

            break;
        default:
            break;
    }

}

- (BOOL)openURL:(NSURL *)url
{
    // Set url
    [_url release];
    _url = [[NSURL URLWithString:[url absoluteString]] retain];
    
    // Get confirmatory message
    NSString*   message;
    NSString*   scheme;
    scheme = _url.scheme;
    if ([scheme isEqualToString:@"http"] ||
        [scheme isEqualToString:@"https"] ||
        [scheme isEqualToString:@"ftp"]) {
        message = @"Safariで開く";
    }
    else if ([scheme isEqualToString:@"mailto"]) {
        message = @"メールを作成する";
    }
    else if ([scheme isEqualToString:@"maps"]) {
        message = @"マップを開く";
    }
    else {
        return NO;
    }
    
    // Create action sheet
    UIActionSheet*  sheet;
    sheet = [[UIActionSheet alloc] init];
    [sheet addButtonWithTitle:message];
    [sheet addButtonWithTitle:@"キャンセル"];
    sheet.cancelButtonIndex = sheet.numberOfButtons - 1;
    sheet.tag = 0;
    sheet.delegate = self;
    
    // Show sheet
    [sheet showInView:[TVAppController sharedController].window];
    
    // Release sheet
    [sheet release];

    return YES;
}

@end

そして、このカスタムUIApplicationをmain.mでセットしています。

//  main.m

#import <UIKit/UIKit.h>

#import "TVAppController.h"
#import "TVApplication.h"

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

ソースを追ってみる

    // Set url
    [_url release];
    _url = [[NSURL URLWithString:[url absoluteString]] retain];

インスタンス変数に値を突っ込んでretainしたいんだけなんだが、我ながら冗長な書き方になってしまっている気がします。

    // Get confirmatory message
    NSString*   message;
    NSString*   scheme;
    scheme = _url.scheme;
    if ([scheme isEqualToString:@"http"] ||
        [scheme isEqualToString:@"https"] ||
        [scheme isEqualToString:@"ftp"]) {
        message = @"Safariで開く";
    }
    else if ([scheme isEqualToString:@"mailto"]) {
        message = @"メールを作成する";
    }
    else if ([scheme isEqualToString:@"maps"]) {
        message = @"マップを開く";
    }
    else {
        return NO;
    }

で取ってきたスキームを見ています。今のところ、http, https, ftp, mailto, mapsは確認していますが、それ以外のスキームもあるかも知れません。
スキーム別に、アクションシートで表示するメッセージを作っています。

    // Create action sheet
    UIActionSheet*  sheet;
    sheet = [[UIActionSheet alloc] init];
    [sheet addButtonWithTitle:message];
    [sheet addButtonWithTitle:@"キャンセル"];
    sheet.cancelButtonIndex = sheet.numberOfButtons - 1;
    sheet.tag = 0;
    sheet.delegate = self;
    
    // Show sheet
    [sheet showInView:[TVAppController sharedController].window];
    
    // Release sheet
    [sheet release];

アクションシートを作るまでは至って普通かと思います。
UIActionSheetの -(void)showInView:(UIView)view に、[TVAppController sharedController].windowを渡しています。

  • (TVAppController*)sharedControllerはTVAppControllerのインスタンスを返します。Xcodeのテンプレで言う、AppDelegateクラスですね。この書き方で合っているかは自信が無いです。
[sheet showInView:[self.windows objectAtIndex:0]];

と書き換えても動きますが、どうなんでしょう。まだ勉強が足りません。

動作画面

ソースコード

あと、今回、githubでソースを公開してみました。
バージョン管理もまだ慣れていないんだけど、多分これで見れるはず。
TextViewLink/TextViewLink.xcodeproj at master · oropon/TextViewLink · GitHub

*1:[http://stackoverflow.com/questions/2543967/how-to-intercept-click-on-link-in-uitextview:title]