プログラムを書こう!

実務や自作アプリ開発で習得した役に立つソフトウェア技術情報を発信するブログ

Objectiv-CのUITextViewで不可視文字を表示する。

この記事は2019年01月06日に投稿しました。

f:id:paveway:20190914064630j:plain

目次

  1. はじめに
  2. Objectiv-CのUITextViewで不可視文字を表示する
  3. おわりに

詳解 Objective-C 2.0 第3版

詳解 Objective-C 2.0 第3版

1. はじめに

こんにちは、iOSのエディタアプリPWEditorの開発者の二俣です。
今回はPWEditorで使用しているObjectiv-CのUITextViewで不可視文字を表示する方法についてです。

目次へ

2. Objectiv-CのUITextViewで不可視文字を表示する

Objective-CのUITextViewで不可視文字を表示する方法ですが、以下のサイトを参考にしました。

Display hidden characters in NSTextView

このサイトで紹介されている方法ですが、NSLayoutManagerクラスのdrawGlyphsForGlyphRange関数をオーバーライドして、該当の不可視文字(改行、タブ、半角スペース、全角スペース)の場合、代替文字に置き換えることで実現しています。

PWEditorはSwiftで実装していますが、

  • 行番号の表示
  • シンタックスハイライト

の機能はObjective-Cで実装されているCYRTextViewライブラリを利用しています。 不可視文字の表示処理はこのライブラリ内に実装するため、Objective-Cのまま組み込みました。

実装例

CYRLayoutManager.h
// ヘッダファイルは特に変更していません。

#import <UIKit/UIKit.h>

@interface CYRLayoutManager : NSLayoutManager

@end
CYRLayoutManager.m
#import "CYRLayoutManager.h"

// 行番号を表示する幅
static CGFloat kMinimumGutterWidth = 30.f;

// 不可視文字を表示した場合、Y方向にずれるためそのオフセット
static CGFloat kGlyphOffsetY = 8.0f;

@implementation CYRLayoutManager

- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
    
    // 不可視文字で置き換えるグリフを初期化します。
    // 改行文字(CR/LF)
    unichar crlf = 0x21B5;
    NSString *CRLF = [[NSString alloc] initWithCharacters:&crlf length:1];
    
    // タブ
    unichar tab = 0x2192;
    NSString *TAB = [[NSString alloc] initWithCharacters:&tab length:1];
    
    // 半角スペース
    unichar halfWidthSpace = 0x2423;
    NSString *HALF_WIDTH_SPACE = [[NSString alloc] initWithCharacters:&halfWidthSpace length:1];
    
    // 全角スペース
    unichar fullWidthSpace = 0x2B1A;
    NSString* FULL_WIDTH_SPACE = [[NSString alloc] initWithCharacters:&fullWidthSpace length:1];

    // UITextViewで表示する文字列を取得します。
    NSString *docContents = [[self textStorage] string];
    
    // 不可視文字の文字色(目立たないようにライトグレーにしています)
    UIColor* invisibleFontColor = [UIColor lightGrayColor];
    
    // 不可視文字用のフォントを作成します。
    // フォント名(self.fontName)とフォントサイズ(self.fontSize)は、このクラスの初期化時に
    // 呼び出し側から引数で指定しています。
    UIFont* font = [UIFont fontWithName:self.fontName size:self.fontSize];

    // 不可視文字の属性を作成します。
    NSDictionary *invisibleAttr = [[NSDictionary alloc] initWithObjectsAndKeys:
                             invisibleFontColor, NSForegroundColorAttributeName,
                             font, NSFontAttributeName,
                             nil];
    
    // 通常文字か判断するフラグ(TRUE:通常文字/FALSE:不可視文字)
    BOOL isNormalChar = TRUE;
    
    // 不可視文字の置き換えを行う開始位置と終了位置を取得します。
    int start = (int)glyphsToShow.location;
    int end = (int)NSMaxRange(glyphsToShow);
    for (int i = start; i < end; i++) {
        // 開始位置から終了位置までの繰り返します。
        // 不可視文字以外は既存の設定で表示するため、設定済みの属性を取得します。
        NSDictionary* prevAttr = [self.textStorage attributesAtIndex:i longestEffectiveRange:nil inRange:[self.textStorage editedRange]];
        UIColor* prevColor = prevAttr[NSForegroundColorAttributeName];
        NSDictionary *normalAttr = [[NSDictionary alloc] initWithObjectsAndKeys:
                                    prevColor, NSForegroundColorAttributeName,
                                    font, NSFontAttributeName,
                                    nil];
        
        // 不可視文字かどうかで処理を振り分けます。
        unichar uc = [docContents characterAtIndex:i];
        isNormalChar = TRUE;
        NSString* glyph = [[NSString alloc] initWithCharacters:&uc length:1];
        switch (uc) {
            case 0x2028:
            case 0x2029:
            case '\n':
            case '\r':
                // 改行
                if (self.isNewline) {
                    glyph = CRLF;
                    isNormalChar = FALSE;
                }
                break;
                
            case '\t':
                // タブ
                if (self.isTab) {
                    glyph = TAB;
                    isNormalChar = FALSE;
                }
                break;
                
            case ' ':
                // 半角スペース
                if (self.isHalfWidthSpace) {
                    glyph = HALF_WIDTH_SPACE;
                    isNormalChar = FALSE;
                }
                break;
                
            case 0x3000:
                // 全角スペース
                if (self.isFullWidthSpace) {
                    glyph = FULL_WIDTH_SPACE;
                    isNormalChar = FALSE;
                }
                break;

            default:
                // 上記以外、そのまま表示します。
                break;
        }

        if ([glyph length]) {
            CGPoint glyphPoint = [self locationForGlyphAtIndex:i];
            CGRect glyphRect = [self lineFragmentRectForGlyphAtIndex:i effectiveRange:NULL];
            CGFloat glyphOffsetX = 0.0;
            // 行番号を表示する/しないでX方向の位置を調整します。
            if (self.lineNumber) {
                glyphOffsetX = kMinimumGutterWidth;
            }
            glyphPoint.x += glyphRect.origin.x + glyphOffsetX;
            // Y方向がずれるため調整します。
            glyphPoint.y = glyphRect.origin.y + kGlyphOffsetY;
            if (isNormalChar) {
                // 通常文字の場合、通常文字の属性で文字を表示します。
                [glyph drawAtPoint:glyphPoint withAttributes:normalAttr];
            
            } else {
                // 不可視文字の場合、不可視文字の属性で文字を表示します。
                [glyph drawAtPoint:glyphPoint withAttributes:invisibleAttr];
            }
        }
    }
}
@end

目次へ

3. おわりに

不可視文字の表示昨日は、ユーザからも要望もあり前々から実現しようと思っていましたが実現できないでいました。
今回参考になるサイトを見つけ、CYRTextViewライブラリへの組み込み方もわかったため、対応しました。

【IT派遣テクノウェイブ】

Objective-C超入門― ゼロからしっかり学べるiPhoneプログラミング 改訂第3版

Objective-C超入門― ゼロからしっかり学べるiPhoneプログラミング 改訂第3版

紹介している一部の記事のコードはGitlabで公開しています。
興味のある方は覗いてみてください。

目次へ


私が勤務しているニューラルでは、主に組み込み系ソフトの開発を行っております。
弊社製品のハイブリッドOS Bi-OSは高い技術力を評価されており、特に制御系や通信系を得意としています。
私自身はiOSモバイルアプリウィンドウズアプリを得意としております。
ソフトウェア開発に関して相談などございましたら、お気軽にご連絡ください。

また一緒に働きたい技術者の方も随時募集中です。
興味がありましたらご連絡ください。

EMAIL : info-nr@newral.co.jp / m-futamata@newral.co.jp
TEL : 042-523-3663
FAX : 042-540-1688

目次へ