こんにちは。ざわかける!のざわ(@zw_kakeru)です。
Swiftで多画面対応(画面サイズに応じたコンテンツサイズの変更)を行う汎用メソッドを作成しました。
AutoLayoutよりも直感的で分かりやすく、私がいつも使っているメソッドとなっています。
なお、StoryBoardは使わずにコードのみの実装を想定しています。
画面サイズに応じたコンテンツサイズの変更
iOSアプリを作成する際、コンポーネントの位置をframe値のまま記述する(あるいはコンポーネントを何も考えずに配置する)と、画面サイズによって表示にズレが生じてしまいます。
調査してみると、AutoLayoutや制約の記述を行うことでこれらに対応させることができるみたいです。
しかし、AutoLayoutの記述は分かりにくく(私はあんまり理解していません)、View毎に数行程度の制約記述を行なっていてはどんどんコード量が増えていってしまいます。
何より直感的に記述することができないため、画面レイアウトを考えるときにとてもストレスになります。
かといってframe値をハードコーディングするのは、考えなければならないパターンが多すぎて現実的ではありません。
そこで、直感的かつ分かりやすく、誰でも簡単に他画面対応を行えるメソッドを作成しました。
私がいつも使っているメソッドです。
getFrameメソッド
func getFrame(fromX: CGFloat = 0, fromY: CGFloat = 0, toX: CGFloat = 100, toY: CGFloat = 100, viewFrame: CGRect) -> CGRect{
let fX = Int(fromX / 100 * viewFrame.width)
let fY = Int(fromY / 100 * viewFrame.height)
let tX = Int(toX / 100 * viewFrame.width)
let tY = Int(toY / 100 * viewFrame.height)
return CGRect(x: fX, y: fY, width: tX-fX, height: tY-fY)
}
getFrameメソッドと名付けたこのメソッドでは、親Viewとその親Viewに対して配置したい位置を割合(パーセント)で指定することで、その位置、大きさのCGRectを返してくれます。
配置したいViewのframeにこの返り値をそのまま使うだけで画面サイズに応じて拡大/縮小されたViewが生成できるので、もう多画面対応に頭を悩ませる必要もありません。
使い方
使い方はこんな感じです。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let viewA = UIView(frame: getFrame(fromX: 15, fromY: 20, toX: 85, toY: 80, viewFrame: self.view.frame))
viewA.backgroundColor = UIColor.blue
self.view.addSubview(viewA)
}
}
これを実行すると次のようになります。

このように、画面の縦と横を100とした時の比率(すなわちパーセント表記)を指定するので、とても直感的に値を記述することができます。
値はCGFloat型なので、小数点以下や負の値を指定することも可能となっています。
もちろん、追加したviewAを親としてさらに別のviewBを追加することもできます。この場合はviewAに対する位置を割合で指定します。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let viewA = UIView(frame: getFrame(fromX: 15, fromY: 20, toX: 85, toY: 80, viewFrame: self.view.frame))
viewA.backgroundColor = UIColor.blue
self.view.addSubview(viewA)
let viewB = UIView(frame: getFrame(fromX: 50, fromY: 50, toX: 100, toY: 90, viewFrame: viewA.frame))
viewB.backgroundColor = UIColor.green
viewA.addSubview(viewB)
}
}

このような入れ子構造も容易に記述可能です。(もちろんrootをviewFrameとして指定して、重ねて表示することもできます。)
getFrameメソッドでは端末自身の画面サイズを取得してviewサイズを返すので、各自の画面サイズに応じて動的にコンテンツが生成されます。
もうめんどくさい制約を毎回書く必要はありませんし、何より直感的で分かりやすいため慣れればこの記述で全て書くことができるようになってすごく便利です。
欠点
このgetFrameメソッドは直感的でとても使い勝手が良いのですが、もちろん欠点も存在します。
主なところでは、リスト表示に対応させるのが大変であるということです。
この場合は漸化式を作って変数によってframe値を計算して渡してあげなければなりません。
例えば画面一面にviewをしき詰めるような実装をする際は次のような記述になります。
(私が使っているコードの一部分を抜粋。)
var np: CGPoint = CGPoint(x: 0, y: 0)
for j in 0 ..< colNum {
for i in 0 ..< rowNum {
let newView = UIView(frame: getFrame(fromX: np.x+panelsWidth*CGFloat(i), fromY: np.y+panelsHeight*CGFloat(j), toX: np.x+panelsWidth*CGFloat((i+1)), toY: np.y+panelsHeight*CGFloat(j+1), viewFrame: frame))
self.addSubview(newView)
}
}
np.y += panelsHeight * CGFloat(colNum)
二重ループを使ってる時点で美しくありませんが、漸化式を用いてこれよりもっと複雑なviewを配置する場合も多々出てきます。
そういう時はAutoLayoutを使ってUIScrollViewやUIStackViewで記述する方が簡単かなと思います。
(だったら最初から全部AutoLayoutでええやんって話ですが。)
終わりに
今回紹介したgetFrameメソッドは私の完全オリジナルなので、他の強い開発者さんがどんな風に実装しているのか全く分かっていません。
画面サイズに応じたコンテンツサイズの変更なんて皆やってるはずなのに、解決策がAutoLayoutしか出てこないのは何でなんですかね。
強い人たちや企業さんたちは画面やView毎にいちいち制約の指定とかしてないと思ってるんですけど、どうやって記述してるんですか、、、
(もしくはAutoLayoutの書き方が一番合理的なのかもしれませんけど。)
誰か教えてください。
まあ結局AutoLayoutの理解から逃げてるだけなので、これを機に次の新しいプロジェクトからはgetFrameメソッドを封印してAutoLayoutの勉強をしてみようかなあと思っているところです。
曰く、「css記述と似たようなもの」「単なる行列(多くの場合は単なる連立方程式)って気づいたら簡単に書けるようになる」と強い人が言ってたので私も頑張ろうと決意しました。
以上です。