UISwitchを任意のサイズで使いたい!!

エンジニアの德光です。 突然ですが、UISwitchって、OSのバージョンに応じてデザインが変わったりしますよ…

エンジニアの德光です。

突然ですが、UISwitchって、OSのバージョンに応じてデザインが変わったりしますよね。
それはまぁ良いんですが、サイズが固定なので使い勝手が悪かったりします。あと1px小さければ配置が揃うのに……とか。

UIButtonあたりを利用しつつまるっと自作したり、サイズ指定が有効なUISegmentedControlでなんとなく代用させても良いんですけど、いまいちお手軽じゃないし動きも違ってしまうので、あまりよろしくない。
ってなわけで今回は、標準のUISwitchの動きのまま、ちょっとした工夫で任意のサイズに対応させるUISwitchを作成してみようと思います。

UISwitchを元に実装する

今回も手軽さを追求して、カスタムクラスの.xibとかはなしで実装したいと思います。
標準サイズより小さなサイズも指定可能なUISwitchということで「UISwitchMini」にしていますが、実際には大きくもできますし、UI〜の接頭辞もどうかと思いますが……

とりあえず適当なプロジェクトを生成して、【ViewController.swiftに】下記のような記述を追加し、【Main.storyboard】で「UISwitch」を配置してクラスを「UISwitchMini」に変更して実行してみます。

class UISwitchMini: UISwitch {
    override func awakeFromNib() {
        super.awakeFromNib()
        print("[bounds:\(NSStringFromCGRect(self.bounds))] [frame:\(NSStringFromCGRect(self.frame))]")
    }
}

すると、「[bounds:{{0, 0}, {51, 31}}] [frame:{{0, 0}, {51, 31}}]」とかいうログが表示されるかと思います。(※iOS10の場合のUISwitch標準サイズが出力されてます)

結論からいうと、このタイミングでtransformのscaleを変更することで、任意のサイズのUISwitchを表示させることができちゃいます。
これで目的のほとんどは果たせているんですけれども、せっかくなので細かく動きなどを確認しつつ、さくっとお手軽かつ汎用的に利用できるようにしておきたいと思います。

動作を確認するため、いったん縦横50%を指定して実行してみます。

class UISwitchMini: UISwitch {
    override func awakeFromNib() {
        super.awakeFromNib()
        print("[bounds:\(NSStringFromCGRect(self.bounds))] [frame:\(NSStringFromCGRect(self.frame))]")
        self.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
        print("[bounds:\(NSStringFromCGRect(self.bounds))] [frame:\(NSStringFromCGRect(self.frame))]")
    }
}

これを実行してみると、出力されるログはこんな感じで。
実際の表示も小さくなっていますが、表示位置がずれちゃってますね。
これについては、まぁあとで簡単に対応させたいと思っています。
まずは、任意のサイズに指定できるようにしましょう。

[bounds:{{0, 0}, {51, 31}}] [frame:{{0, 0}, {51, 31}}]
[bounds:{{0, 0}, {51, 31}}] [frame:{{12.75, 7.75}, {25.5, 15.5}}]

横40px、縦20pxのスイッチにする

指定サイズに拡大(縮小)してやれば良いので、そのようなscale値を求めれば良いわけで。
「元サイズ×拡大率=表示サイズ」なわけなので、「拡大率=表示サイズ÷元サイズ」ですよね?

class UISwitchMini: UISwitch {
    override func awakeFromNib() {
        super.awakeFromNib()
        let dispSize: CGSize = CGSize(width: 40.0, height: 20.0)
        let scaleX: CGFloat = dispSize.width / self.bounds.size.width
        let scaleY: CGFloat = dispSize.height / self.bounds.size.height
        print("[bounds:\(NSStringFromCGRect(self.bounds))] [frame:\(NSStringFromCGRect(self.frame))]")
        self.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
        print("[bounds:\(NSStringFromCGRect(self.bounds))] [frame:\(NSStringFromCGRect(self.frame))]")
    }
}

縦横の拡大率をscaleY/ScaleXとして求めておき、「CGAffineTransform(scaleX: , y: )」で変形表示させています。

[bounds:{{0, 0}, {51, 31}}] [frame:{{0, 0}, {51, 31}}]
[bounds:{{0, 0}, {51, 31}}] [frame:{{5.5, 5.5}, {40, 20}}]

はい、ログを見てもサイズが (40, 20) となっているのがわかると思います。
(※だがしかし、(5.5, 5.5)だけ表示がずれてしまっています…これは、( (51-40)/2, (31-20)/2 ) ってこと。中心は同じだからですね )
ちなみに、boundsはtransformしても変化しないんですよね。
これ、UIAnimationで変形アニメーションするときとかに重要です。

サイズ指定しやすくする

とりあえず、dispSizeを変更することで任意のサイズでできますが、awakeFromNibタイミングで任意の値にしておきたいですよね。
ってなわけで、これをUIデザイン時に指定できるようにしておきましょう。
「@IBInspectable」というおまじないを書くと、InterfaceBuilder側でもプロパティ値が設定可能になるのです。
これで、いろんなサイズのスイッチを作成できるようになりました。

class UISwitchMini: UISwitch {
    @IBInspectable var dispSize: CGSize = CGSize(width: 51.0, height: 31.0) //iOS10での標準サイズをいれている
    override func awakeFromNib() {
        super.awakeFromNib()
        let scaleX: CGFloat = dispSize.width / self.bounds.size.width
        let scaleY: CGFloat = dispSize.height / self.bounds.size.height
        print("[bounds:\(NSStringFromCGRect(self.bounds))] [frame:\(NSStringFromCGRect(self.frame))]")
        self.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
        print("[bounds:\(NSStringFromCGRect(self.bounds))] [frame:\(NSStringFromCGRect(self.frame))]")
    }
}


こんな感じで、InterfaceBuilder側に「Disp Size」という欄が現れ、CGSizeとしての値を設定できるようになります。
@IBInspectableが対応している型はいくつかあるので、いろいろ試してみましょう 😉
(※「dispSize」という名前を「Disp Size」と変換表示してくれるんですねぇ)

 

配置のズレを吸収

さいごに、なんかframeのoriginがズレてる問題をサクッと解消しておきましょう。
これは簡単、表示サイズにしたUIViewをInterfaceBuilderで配置して、その中にUISwitchMiniを入れて中央表示制約をつけておけば感覚的にわかりやすくなると。
このとき、ひとつ噛ませた「サイズ変更スイッチの表示エリア」に対して「Clip To Bounds」をチェックしておくことで、エリア外の描画がIB上でも抑止されるため、デザイン確認がしやすくなるかとも思います。

▼ClipToBoundsなしの場合:

▼ClipToBoundsありの場合:

 

ここでおもむろに実行して表示を確認。。。っと、なんかずれてる気が。
ログを見てみると、それぞれUIViewをかましているのでframeのoriginも (0, 0) に…なってないよ…(1, 0)になってるじゃん!!
試しに右下の3つ目の表示エリアとスイッチのdispSizeを両方とも51*31に変更してみてもずれてる。
ってか、その状態でIB上で配置を確認した時点でずれてる。
いみがわからないよ。

まぁ、かならず1px右にずれるようなので、配置エリアに対するスイッチの「AlignCenterX」を「−1」補正してやって解消させることにしました。

しかし、一番時間がかかったのは WordPress の変な整形ルールと闘うことだったりして。。。
あー、タグそのまま書いてしまうのが楽だや。[display: none] とかも効くんだねぇ〜(^_^;;;
画像の回り込み解除とか、なぞの整形崩れとか、イミフなタグとか。。。
もっとまともな整形プラグインとか使わせてくれないかなぁ。。。HNFとかPukiWikiルールくらいで良いのなぁ(x_x)

※とりあえず、いろいろ諦めました