2014年11月15日土曜日

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が外れてしまう(全体が表示できる幅のボタンになる)のでやってはいけない。

0 件のコメント:

コメントを投稿