2014年11月15日土曜日

UITableViewでのインデックス表示時の注意

UITableView

インデックス表示に重なる部分の下に来る要素(ボタンなど)は反応しないので注意。

実際に表示が重ならなくても、表示領域的に。


iOS7以降におけるUIButtonの挙動の問題について

iOS7以降では、UIButton.textを設定した時、UIButton.textLabel.frameが変更されるのは実際に表示されるタイミングになっている。すなわち、.text代入時にはtextLabelの表示幅や位置は取得できない。

通常それを知る必要はないが、UIButtonではtextLabelは基本的にセンタリングで表示される(titleEdgeInsetで補正は可能)ため、例えばボタンを表示上左寄せしたい場合にはtextLabel.frame.origin.x=0にしなければならない。ところが、.text代入直後は.textLabel.frameは{0,0,0,0}であり、この時点で代入しても無効になる。

ではどうするかというと、KVOで.textLabel.frameを監視して、その代入があったタイミングで補正する。

こんな感じ。

――――――――
UIButtonWithLabelAlignment.h
――――――――
#import <UIKit/UIKit.h>

// 左寄せと上寄せがORで設定できる
enum {
  kUIButtonWithLabelAlignment_normal=0,
  kUIButtonWithLabelAlignment_left =(1<<0),
  kUIButtonWithLabelAlignment_up   =(1<<1),
};

@interface UIButtonWithLabelAlignment : UIButton
@property (nonatomic) NSInteger align;
@end

――――――――
UIButtonWithLabelAlignment.m
――――――――

#import "UIButtonWithLabelAlignment.h"

#define OBSERVE_FRAME @"titleLabel.frame"

@interface UIButtonWithLabelAlignment()
{
  BOOL setKVO;
}
@end


@implementation UIButtonWithLabelAlignment

-(instancetype)initWithFrame:(CGRect)frame
{
  self=[super initWithFrame:frame];
  if (self) {
     [self addObserver:self forKeyPath:OBSERVE_FRAME options:
NSKeyValueObservingOptionNew context:NULL];
     setKVO=YES;
  }
  return self;
}

// KVOによる変更通知受信
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
  // 1つしか登録してないのでkeyPathのチェックは省略
   // 即解除;そうしないと以下でframeを操作するとまた発生してしまうから
  [self removeObserver:self forKeyPath:OBSERVE_FRAME];
  setKVO=NO;
   // 寄せる
  UIButton *btn=(UIButton *)object;
  CGRect frame=btn.titleLabel.frame;
  if (self.align&kUIButtonWithLabelAlignment_left) {
     frame.origin.x=0;
  }
  if (self.align&kUIButtonWithLabelAlignment_up) {
     frame.origin.y=0;
  }
  btn.titleLabel.frame=frame;
}

-(void)dealloc
{
  if (setKVO) {
   // 外れてないのが残ってたら外す
   self.titleLabel.frame=CGRectZero; // 上の通知でKVOを解除する
  // これでは解除処理が終了するまでにdeallocが終了してしまうためエラーが発生する
  // [self removeObserver:self forKeyPath:OBSERVE_FRAME];
  }
}


ついでに書いておくと、[UIButton sizeToFit]するとボタン幅が表示幅に合わされるので中央寄せ=左寄せで問題ないが、.textLabelにtruncateを設定していると、その表示幅はボタンの幅より必ず狭くなるのでセンタリングが起こる。これはtruncateが発生するラベルと発生しないラベルを並べた場合には、表示位置がずれて見えるということを意味する。なお、truncateが発生したボタンにsizeToFitをかけるとtruncateが外れてしまう(全体が表示できる幅のボタンになる)のでやってはいけない。

UILabelのfontサイズについて

UILabelのfontサイズは.fontのpointSizeなどで設定できるが、実はこの値はiOSによって動的に変更される。

adjustsFontSizeToFitWidth=YESでminimumScaleFactorを設定していた場合、.text(attributedText)の表示幅によってfontのサイズも変更される。


それはまだ理解しやすいがもう1つわかりにくい変化タイミングがある。


attributedText内でフォントを設定した場合、その先頭にかけられたフォントサイズになる。
ひょっとしたら、フォントそのものも変更されるかもしれない。


// 全体にフォントを適用した文字列を作成
NSMutableAttributedString *astr = [[NSMutableAttributedString alloc]
initWithString:baseStr attributes:@{NSFontAttributeName:font}];


この場合、label.fontのサイズはここで指定したfontのそれになる。
問題はここから。このastrにさらに、先頭からlen文字に

NSInteger point=0; // 先頭
UIFont *zeroFont=[UIFont systemFontOfSize:0];
[astr addAttributes:@{NSFontAttributeName:zeroFont} range:NSMakeRange(point,len)];

とかしてフォント指定を重ねると、それがUILabel.fontのフォントサイズになる。
この場合フォントサイズは0なので、len文字は表示されない。
先頭ではなく文字列途中にかけた場合はフォントサイズにはならない。

このattributedTextの表示は正常に行われるが、後にそのUILabelにそのまま別の.textを代入したりするとフォントサイズ0なので全体が表示されないというバグが発生する。.fontでサイズを呼び出してもおかしい、ということになる。

そんなことするはずないと思われるかもしれないが、UITableViewCellで再利用する場合には有り得る話となる。


要注意。