NSScrollViewの高さを入力文字数に応じて変更する

# 概要

以下のような、入力した文字数に応じてNSScrollViewとウィンドウの高さが変わるようなものを作成する

![May-22-2019 17-02-00](https://i.imgur.com/b3aWVwS.gif)

# GitHub

https://github.com/pommdau/Qiita_Changeable_Height_TextView

# NSTextViewのカスタムクラスを作成・設定する

- 下記の通りクラスを割り当てておく

![-w1269](https://i.imgur.com/DJU8nTJ.jpg)

## テキストが変更されたときの処理

``` ObjectiveC
// テキストの変更時に呼ばれる
- (void)didChangeText
{
    [self updateScrollViewHeight];  // TextViewの含まれるNSSCrollViewのframeのheightを更新する
    [self.delegate ChangeableHeightTextViewDidChange];   // 呼び出し元のWindowControllerでウィンドウの高さを変更する
}
```

- `NSTextView`のメソッド`didChangeText`をoverrideする
    - [Changing size of NSTextView
](https://stackoverflow.com/questions/11196307/changing-size-of-nstextview)
- やることは下記の2点
- `NSTextView`の含まれる`NSSCrollView`の高さを更新する（このクラスで処理する）
    - （`NSTextView`は自動で大きさが変わるようになっているみたいで、こちらは更新しない）
- NSWindowControllerのdelegateメソッドを呼び出して、Windowのサイズを更新する（プロトコルを定義してdelegate先で処理する）
   
## NSSCrollViewの高さを更新する
``` ObjectiveC
static const int kInitialStringHeight     = 19; // 1行目の文字列の高さ
static const int kSingleByteStringHeight  = 14; // 非日本語文字列の高さ（FontSiZeが12のとき）
//static const int kMultiByteStringHeight   = 18; // 日本語文字列の高さ　（FontSiZeが12のとき）
static const int kMaximumLineNum          = 100; // この行数以上では高さを変更しない
```


- 以降の計算で使用するマジックナンバーを変数で定義しておく。
    - （原始的…他にいい方法はないものか）

``` ObjectiveC
/**
 @brief NSTextViewの含まれるNSSCrollViewのframeのheightを更新する
 */
- (void)updateScrollViewHeight {
    // Calculate height
    NSUInteger numberOfLines = [self numberOfLines];
    NSUInteger height = 0;
    if (numberOfLines <= kMaximumLineNum) {
        height = kInitialStringHeight + (numberOfLines - 1) * kSingleByteStringHeight;  // 1行目の高さは固定としている
        if (height < kInitialStringHeight) {
            height = kInitialStringHeight;  // 最低の幅を設定
        }
    }
    
    // Update height
    NSScrollView *scrollView = (NSScrollView *) self.superview.superview;
    NSRect frame      = scrollView.frame;
    frame.size.height = height;
    scrollView.frame  = frame; // 実際のNSScrollViewのframeを更新
}
```

-------------------------------------------------------------------

``` ObjectiveC
// Calculate height
NSUInteger numberOfLines = [self numberOfLines];
NSUInteger height = 0;
if (numberOfLines <= kMaximumLineNum) {
    height = kInitialStringHeight + (numberOfLines - 1) * kSingleByteStringHeight;  // 1行目の高さは固定としている
    if (height < kInitialStringHeight) {
        height = kInitialStringHeight;  // 最低の幅を設定
    }
}
```

- 行数を計算する関数を作成しておく。（後述）
- 行数がわかれば、あとは原始的に計算していく。
- 今回は英語の文字列のみを考慮しているが、日本語を含む場合は1行あたりの高さが変わってくるのでバランスを取る必要があるか。
    - [NSString 半角・全角の判定](https://qiita.com/yusuga/items/f182fa7a60c3f8c774ea)あたりを参考にしたら実装は可能そうではある。

``` ObjectiveC
// Update height
NSScrollView *scrollView = (NSScrollView *) self.superview.superview;
NSRect frame      = scrollView.frame;
frame.size.height = height;
scrollView.frame  = frame; // 実際のNSScrollViewのframeを更新
```

- `NSTextView`の含まれる`NSSCrollView`のframeを**ここで直接更新する**
- 計算した`height`を用いて、`NSScrollView`の`frame`の高さを更新する

``` ObjectiveC
self.superview.superview.frame.size.height = height;
```

- 当初は上記のように書いていたが、`Expression is not assignable`とエラーが出た
- [iOS開発：”Expression is not assignable.”エラー](https://eikatou.net/blog/1981.html)を参照してワンクッション置くように書いている。

> 「myLabel.frame」はプロパティへのアクセスで、「frame.size」は構造体メンバへのアクセス。これを混ぜているからエラーとなっている。

## NSTextViewの文字列の行数を計算する 

``` ObjectiveC
/**
 @brief NSTextViewの文字列の行数を計算する
 */
- (NSInteger)numberOfLines {
    NSLayoutManager *layoutManager = [self layoutManager];
    NSUInteger      numberOfLines  = 0;     // 行数のカウント用変数
    NSUInteger      numberOfGlyphs = [layoutManager numberOfGlyphs];
    NSRange         lineRange;  // 現在対象となっている行の、先頭の開始位置と文字数
    for (NSUInteger index = 0; index < numberOfGlyphs; numberOfLines++) {   // index: 現在見ている文字の位置
        (void) [layoutManager lineFragmentRectForGlyphAtIndex:index
                                               effectiveRange:&lineRange];
        index = NSMaxRange(lineRange);
    }
    
    // 最終行が改行の場合に計算されていないようなので、その補正をする
    NSString *text = self.textStorage.string;
    if (text.length != 0 && [[text substringFromIndex:text.length - 1] isEqualToString:@"\n"]) {
        numberOfLines++;
    }
    return numberOfLines;
}
```

 - 行数取得に関しては[Counting Lines of Text（Apple公式）](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextLayout/Tasks/CountLines.html#//apple_ref/doc/uid/20001810-CJBGBIBB)を参考にした。
 - コメントにも書いたが、最終行が改行の場合に計算が合わないのでその補正をしてやるといい。
     - [【Objective-C】NSStringの前後にある半角スペース、改行、アルファベット、数字をトリミングする方法。](https://wayohoo.com/programming/objective-c/nsstring-stringbytrimmingcharactersInset.html)

## Protocolの定義

- Windowの大きさを変えるために、`NSWindowController`用にプロトコルを定義する

``` ObjectiveC
@protocol ChangeableHeightTextViewDelegate <NSTextViewDelegate>
- (void)ChangeableHeightTextViewDidChange;  // テキストビューの内容が変更されたときによばれる
@end
```

- `<NSTextViewDelegate>`は`NSTextViewDelegate`を継承して新たなプロトコルを作成することを示す

``` ObjectiveC
@property (weak, nonatomic) id <ChangeableHeightTextViewDelegate> delegate;
```

![-w968](https://i.imgur.com/iSjy55S.jpg)

※上記のようにエラーが出るのだが、既存のDelegateを継承する場合のdelegateプロパティの書き方が間違っているのだろうか？

## NSTextViewの大きさを可視化する

``` ObjectiveC
- (void)drawRect:(NSRect)dirtyRect {
    [super drawRect:dirtyRect];
    
    // Drawing code here.
    // 見やすいようにNSTextViewの範囲を描画する
    NSRect bounds = [self bounds];
    [[NSColor greenColor] set];
    [NSBezierPath strokeRect:bounds];
}
```

- `NSTextView`の範囲がわかりやすいように描画を行っておく

# ウィンドウの大きさを変更する

``` ObjectiveC
// MARK:- ChangeableHeightTextViewDelegate Delegate Methods
// TextView内のテキストが変更されたときに呼ばれる
- (void)ChangeableHeightTextViewDidChange {
    NSRect windowFrame = self.window.frame;
    NSRect scrollViewFrame = _myTextView.superview.superview.frame;
    // 20:Windowのヘッダ幅、上下のマージン、
    // 20:（入力していってね）の高さ
    // 6 :固定テキストとNSSCrollViewの間隔
    windowFrame.size.height = (20 + 20 + 6 + 20 + 20) + scrollViewFrame.size.height;
    [self.window setFrame:windowFrame display:YES];
}
```

- ウィンドウの大きさは、`ScrollViewの高さ`+`その他要素の高さ`で計算する