こんにちは、uni! です。
今回は Xcode の AutoLayout による
配置の練習問題を挙げてみようと思います。
Xcode を学習中の方は
問題文を読んだ後に
自分ならどう実装するかどうかを
頭の中でぼんやり描いてから
答え合わせを読んで頂けると幸いです。
問題文
今回は次のような見た目を作成したいと思います。
図1は iPhone の縦画面です。
図2は iPhone の横画面です。
図3は iPad の縦画面です。
図4は iPad の横画面です。
文字が上にあって
下にボタンが 6個 並んでいる
タイトル画面で使いそうな見た目になります。
よろしければ
「自分ならどう実装するか」
をイメージして頂けると幸いです。
各ボタンは端末の画面サイズに関わらず
同じ形を維持して下さい。つまり
・iPhone 縦画面
・iPhone 横画面
・iPhone 縦画面で画面分割を行い横に長い状態(チェック可能ならば)
・iPad 縦画面
・iPad 横画面
・iPad 縦画面で画面分割を行い横に長い状態(チェック可能ならば)
でボタンの縦横比が変わらないようにして下さい。
可能ならば SafeArea 内にボタンが
100% の確率で収まる方法の
イメージをお願いします。
問題文はここまでです。
ここからは答え合わせです。
といっても自己流の配置方法かもしれないため
皆さんのイメージされた方法と違う方法が解説されても
皆さんがイメージされた方法が間違いと言いたいわけではありません。
皆さんのイメージされた方法と違う方法が紹介されたならば
いつもの方法とは違う新鮮な感覚を感じながら
流し読みして頂けると幸いです。
作業は大まかに 2工程 あります。
工程1 の概要
工程1 では 図5 ~ 図8 のような配置になる
View を作成します。
これは 「19:39」 の比率の View を
SafeArea からはみ出ないように画面中央に
なるべく大きく配置する作業になります。
これ以降は View の範囲を分かりやすくするために
目標の View の背景色を変更しつつ解説を行います。
結論から言うと今回のレスポンシブ対応は
背景を画面全体に配置して
この View の上に各オブジェクトを配置することで
レスポンシブに対応する形になります。
工程2 の概要
工程2 では 工程1 で配置した View 上に
各オブジェクトを比率で配置します。
図13 ~ 図16 のような配置になるよう
View を配置することを最終目標にします。
これらの View を配置してしまえば
あとはボタンを View 上に配置するだけになります。
工程1
まずは 画面上に View を追加します。
追加したら以下の制約を付与して下さい。
・ SafeArea の左右中央に配置する(Priority: 1000)
・ SafeArea と同じ width にする(Priority: 999)
・ SafeArea 以下の width にする(Priority: 1000)(Constant: “≦”)
・ SafeArea の上下中央に配置する(Priority: 1000)
・ SafeArea と同じ height にする(Priority: 999)
・ SafeArea 以下の height にする(Priority: 1000)(Constant: “≦”)
・ Aspect Radio の Multipiler を 19:39 にする(Priority: 1000)
「SafeArea」との関係は
同じ内容の「親」との関係を作った後に
「親」を「SafeArea」に置き換えると楽に付与できます。
例えば「SafeArea の左右中央に配置する」を付与したい場合は
View を選択
→ Horizontally in Container を選択
→ 制約をダブルクリック
→ Secoond Item を SafeArea にする
の流れで行うと楽です。
工程2-A
次に 図9 ~ 図12 のように
View を配置していきます。
まずは 工程1-A で配置した View の上に
Vertical Stack View を配置して下さい。
配置したら次の制約を設定して下さい。
・親の左右中央に配置する(Priority: 1000)
・親の上下中央に配置する(Priority: 1000)
・width を親の width と同じにする(Priority: 998)
・height を親の height と同じにする(Priority: 998)
工程2-B
次に 工程2-A で配置した Vertical Stack View の上に
図9 ~ 図12 の形になるように View を 7個 配置します。
7個 の View を追加して
各 View に次の制約を追加して下さい。
・width を親の width と同じにする(Priority: 997)
さらに各 View に
以下の制約を追加して下さい。
・ 上から1番目の View に
「 height を親の height と同じにする(Priority: 997)(Multiplier: 40/100) 」
・ 上から2番目の View に
「 height を親の height と同じにする(Priority: 997)(Multiplier: 15/100) 」
・ 上から3番目の View に
「 height を親の height と同じにする(Priority: 997)(Multiplier: 5/100) 」
・ 上から4番目の View に
「 height を親の height と同じにする(Priority: 997)(Multiplier: 15/100) 」
・ 上から5番目の View に
「 height を親の height と同じにする(Priority: 997)(Multiplier: 5/100) 」
・ 上から6番目の View に
「 height を親の height と同じにする(Priority: 997)(Multiplier: 15/100) 」
・ 上から7番目の View に
「 height を親の height と同じにする(Priority: 997)(Multiplier: 5/100) 」
工程2-C
次に 工程2-B で配置した 7個 の View の
上から 2番目 4番目 6番目 の View に対して
図17 の形になるように 5個 の View を配置します。
図17 の青色の View に
ボタンを配置することで Finish です。
まずは 工程2-B で配置した 7個 の View の
上から 2番目 4番目 6番目 の View に
Horizontal Stack View を追加します。
そしてその Horizontal Stack View に以下の制約を追加します。
・親の左右中央に配置する(Priority: 1000)
・親の上下中央に配置する(Priority: 1000)
・width を親の width と同じにする(Priority: 996)
・height を親の height と同じにする(Priority: 996)
工程2-D
次に 工程2-C で配置した Horizontal Stack View の上に
先ほど紹介した 5個 の View を配置します。
5個 の View を追加して
各 View に次の制約を追加して下さい。
・height を親の height と同じにする(Priority: 995)
・親の上下中央に配置する(Priority: 1000)
さらに各 View に
以下の制約を追加して下さい。
・ 上から1番目の View に
「 width を親の width と同じにする(Priority: 995)(Multiplier: 15/100) 」
・ 上から2番目の View に
「 width を親の width と同じにする(Priority: 995)(Multiplier: 30/100) 」
・ 上から3番目の View に
「 width を親の width と同じにする(Priority: 995)(Multiplier: 10/100) 」
・ 上から4番目の View に
「 width を親の width と同じにする(Priority: 995)(Multiplier: 30/100) 」
・ 上から5番目の View に
「 width を親の width と同じにする(Priority: 995)(Multiplier: 15/100) 」
作業は以上になります
まとめ
この練習問題はいかがでしたでしょうか?
この練習問題は
「ボタンの形状を変えずに作成する」
「配置が荒ぶらないように配置する」
という点を両立するのに苦戦するかと思われます。
とはいえ、このような見た目の配置は
決してめずらしくないと思われるため
配置できるに越したことはないと思われます。
この「比率による配置」を実際に体験して頂けたならば
個人的に嬉しい限りです。
繰り返しになりますが
この方法は自己流の方法かもしれません。
この記事を参考にすることで万が一発生してしまった不利益に関して
我々は責任を負わないものとさせて下さい。何卒よろしくお願いします。
参考資料として
1時間程度で作成したそれっぽい見た目の内容を置いておきます。
https://drive.google.com/file/d/1945K61MtT4R67VVmL5-jlUGmP5JIJ-qh/view?usp=sharing
かなりの長文になってしまいましたが
最後まで閲覧頂きありがとうございました。
おまけ
SwiftUI で、この配置方法を再現してみました。
SwiftUI の、配置の考え方とは異なるかもしれませんが
一つの考え方として新鮮な感覚で流し読みして頂けると幸いです。
現時点ではセーフエリアを考慮しておりませんので
ご注意下さい。
考慮する場合は上下に余裕を残すとよいかもしれません。
自己流のやり方ですが
フォントサイズは width / 20 等を入力すると
端末によってフォントサイズが変わりません。
ただしこの方法でフォントサイズを指定する場合は
pow(width, 0.9) / 15 など
値の微調整を行う必要があるかもしれません。
ちなみに私は、この方法で この画面 を再現するのに
2時間 かかりました。(必要な画像を用意した状態からスタートしました)
https://drive.google.com/file/d/1YhPgvTfNzl1ujYiEQavySoXEjZeseo0g/view?usp=sharing
以下、ボタンを6個並べる画面の
ソースコードです。
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { bodyView in
getFirstView(bodyView.size.width, bodyView.size.height)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
/**
* 画面全体 View
*
* 9:19.5 をなるべく大きく切り取って
* そこに getMainContent を配置
*/
func getFirstView(_ width: CGFloat, _ height: CGFloat) -> some View {
//端末が 9:19.5 以上に縦長の場合
if 19.5 / 9.0 <= height / width
{
let myWidth = width
let myHeight = width * 19.5 / 9.0
return AnyView (
VStack(spacing: 0){
//上の余白
VStack(spacing: 0){}
.frame(width: width, height: ((height - myHeight) / 2))
//メインコンテンツ
getMainContent(myWidth, myHeight)
.frame(width: myWidth, height: myHeight)
//下の余白
VStack(spacing: 0){}
.frame(width: width, height: ((height - myHeight) / 2))
}
.frame(width: width, height: height)
)
}
else
{
let myWidth = height / 19.5 * 9.0
let myHeight = height
return AnyView (
HStack(spacing: 0) {
//左の余白
HStack(spacing: 0){}
.frame(width: ((width - myWidth) / 2), height: height)
//メインコンテンツ
getMainContent(myWidth, myHeight)
.frame(width: myWidth, height: myHeight)
//右の余白
HStack(spacing: 0){}
.frame(width: ((width - myWidth) / 2), height: height)
}
.frame(width: width, height: height)
)
}
}
/**
* 画面比率 9:19.5 のメインの配置
*/
func getMainContent(_ width: CGFloat, _ height: CGFloat) -> some View {
return VStack(spacing: 0) {
//上の空間
HStack{}
.frame(width: width, height: (height / 12 * 4.2))
//ボタンのある横1列
getHorizontalLine(width, height / 12 * 2, 0)
//ボタンとボタンの間の余白
HStack{}
.frame(width: width, height: (height / 12 * 0.6))
//ボタンのある横1列
getHorizontalLine(width, height / 12 * 2, 1)
//ボタンとボタンの間の余白
HStack{}
.frame(width: width, height: (height / 12 * 0.6))
//ボタンのある横1列
getHorizontalLine(width, height / 12 * 2, 2)
//下の空間
HStack{}
.frame(width: width, height: (height / 12 * 0.6))
}
.frame(width: width, height: height)
}
/**
* 横1列
*/
func getHorizontalLine(_ width: CGFloat, _ height: CGFloat, _ lineID: Int) -> some View{
return HStack(spacing: 0){
//左の余白
ZStack{}.frame(width: (width / 12 * 1.3), height: height)
//左のボタン
getButtonView(width / 12 * 4, height, lineID * 2)
//ボタンとボタンの間の余白
ZStack{}.frame(width: (width / 12 * 1.4), height: height)
//右のボタン
getButtonView(width / 12 * 4, height, lineID * 2 + 1)
//右の余白
ZStack{}.frame(width: (width / 12 * 1.3), height: height)
}
.frame(width: width, height: height)
}
/**
* ボタン部分のView
*/
func getButtonView (_ width: CGFloat, _ height: CGFloat, _ buttonID: Int) -> some View {
return Button(action: {buttonAction(buttonID)}){
Text("button")
.foregroundColor(Color.white)
.font(.system(size: width / 5))
.fontWeight(.bold)
}
.frame(width: width, height: height).background(.blue)
}
/**
* ボタンの処理
*/
func buttonAction(_ buttonID: Int) {
switch(buttonID){
case 0:
print("button0 が押されました")
case 1:
print("button1 が押されました")
case 2:
print("button2 が押されました")
case 3:
print("button3 が押されました")
case 4:
print("button4 が押されました")
case 5:
print("button5 が押されました")
default:
print("予期せぬボタンが押されました")
}
}