Swift Advent Calendar 2020の8日目の記事です
7日目の記事はこちらです→SwiftのArraySliceのおもしろい話
記事を開いてくれてありがとうございます。みんくると言います٩(🌀❛O❛🌀)۶
本記事ではカスタムビューに Text や Image などのビューモディファイアにないメソッドを適用する方法についてお話します!
環境
Xcode: Ver. 11.6
Swift: Ver. 5.1
では上手くいくことを確認しています(Swift 5.0 以上なら大丈夫だと思います!)
事前知識
簡単なざっくりとした説明です。知ってるよ!って方は飛ばしてください。
SwiftUI について
import SwiftUI // SwiftUI のフレームワークを使うときは必要(以降では省略します)
struct HelloWorld: View {
var body: some View {
Text("Hello World") // 文字を表示する
}
}
こんな感じでvar body: some View { }
のカッコ内で書いたようにデザインできます。文字を表示するText()
の他にも、円を表示するCircle()
や画像を表示するImage()
などのたくさんの部品を組み合わせてデザインしていきます。こうして作ったものやText()
などの部品のことをビューと呼んでいます。
↓上のコードの例(Text()
で文字を表示している)
カスタムビューについて
↓カスタムビューの例
struct CustomView: View {
let text: String // 表示させる文字のプロパティ
init(_ text: String) {
self.text = text
}
var body: some View {
Text(self.text) // 文字を表示する
.padding() // 余白をとる
.background( // 文字のビューの後ろに↓を表示する
RoundedRectangle(cornerRadius: 16) // 角丸四角の図形を生成する
.fill(Color.orange) // 図形をオレンジ色にする
)
}
}
このカスタムビューは、オレンジ色の角丸四角な図形の上に入力した文字を表示させます。
struct ContentView: View {
var body: some View {
VStack(spacing: 10) { // ←ビューを縦に並べる
CustomView("Hello World") // カスタムビューを使う
CustomView("Custom View")
CustomView("Natto Gohan")
}
}
}
ビューモディファイアについて
struct HelloWorld: View {
var body: some View {
Text("Hello World")
.font(.largeTitle) //文字を大きくする
.foregroundColor(.red) //文字を赤色にする
}
}
この(↑)ように"大きい"文字という文字の修飾を、Text().font()
のようなText()
を.font()
で修飾することで表現しています。
説明に使うカスタムビュー
本題に入る前に説明に使うカスタムビューを紹介します。
今回説明に使うのは、CustomView("表示したい文章")
と宣言すると "表示したい文章" を角丸四角な図形の上に表示してくれるカスタムビューです。
struct CustomView: View {
let text: String // 表示させる文字のプロパティ
init(_ text: String) {
self.text = text
}
var body: some View {
Text(self.text) // 文字を表示する
.padding() // 余白をとる
.background( // 文字のビューの背面に↓を表示する
RoundedRectangle(cornerRadius: 16) // 角丸四角の図形を生成する
.fill(Color.orange) // 図形をオレンジ色にする
)
}
}
デフォルトではオレンジ色の角丸四角な図形の上に入力した文字が表示されます。
struct ContentView: View {
var body: some View {
VStack(spacing: 10) { // ←ビューを縦に並べる
CustomView("Hello World") // カスタムビューを使う
CustomView("Custom View")
CustomView("Natto Gohan")
}
}
}
導入: 使う用途に合わせてカスタムビューを変化させたい!
実際にカスタムビューを使うとき、使う用途に合わせて変化させたいときがあります。
例えばテキストボックスのカスタムビューなら、文字の大きさを変えたりボックスの色を変えたりしたいみたいな感じです。
こういうときビューモディファイア使って変化させることができます。
struct ContentView: View {
var body: some View {
VStack(spacing: 10) {
CustomView("Hello World")
CustomView("Custom View")
.font(.largeTitle) // 文字を大きくする
CustomView("Natto Gohan")
.foregroundColor(.white) // 文字を白色にする
}
}
}
しかし、加えたい変化がビューモディファイアに存在しない場合この方法は使えません。ビューモディファイアにない「Text ビューのメソッドで変化を加えたい」みたいなときは、最初にカスタムビューを定義する時点でそのことを想定した設計にする必要があります。
本記事では Text, Image, Circle などのメソッドをカスタムビューに使用する方法について、文字の太さと図形の色を変化させれるように拡張することを通して解説します。
ちなみに文字の太さを変更するのは Text ビューの "fontWeight" メソッドで、図形の色を変更するのは Shape プロトコルの "fill" メソッドです。
方法1: イニシャライザでデザインを指定する
一番単純な方法は、デザインの状態を管理するプロパティをビューに持たせ、宣言時にデザインを指定させます。そしてその値を元にデザインを変更する間接的な方法です。
例えばカスタムビューに、
・ 文字の太さ → fontWeight
・ 図形の色 → boxColor
とそれぞれプロパティを作り、カスタムビュー宣言時にデザインを指定できるようにします。
struct CustomViewEx1: View {
let text: String
let fontWeight: Font.Weight? // 文字の太さのプロパティ
let boxColor: Color // 図形の色のプロパティ
// 変更点: イニシャライザでデザインを指定する
// * 指定しない場合はデフォルトの値が使われる
init(_ text: String, fontWeight: Font.Weight? = .regular, boxColor: Color = .orange) {
self.text = text
self.fontWeight = fontWeight // 指定された値 or デフォルトの値
self.boxColor = boxColor // 指定された値 or デフォルトの値
}
var body: some View {
Text(self.text)
.fontWeight(self.fontWeight) // 文字の太さを指定する
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(self.boxColor) // 図形の色を指定する
)
}
}
イニシャライザに引数を増やしカスタムビューの宣言時にデザインを指定できるようにします(指定しない場合はデフォルトの値が使われ同じデザインになります)。
struct ContentView: View {
var body: some View {
VStack(spacing: 10) {
CustomViewEx1("Hello World")
CustomViewEx1("Custom View", boxColor: .blue)
.font(.largeTitle)
CustomViewEx1("Natto Gohan", fontWeight: .black, boxColor: .black)
.foregroundColor(.white)
}
}
}
宣言時に引数で一気に指定するので変えたいデザインが多くなると煩雑になります。
方法2: インスタンスメソッドでデザインを指定する
方法1ではイニシャライザの引数で一気に指定するので、変えたいデザインが多くなると引数が多くなり分かりにくくなります。また、デフォルトで使いたいたいだけの人にも多くの引数が見えてしまうのも少し嫌なところです。
そこでカスタムビューの宣言時には文章の入力だけにし、デザインの変更はインスタンスメソッドから行う方法を紹介します。こちらの方法でもデザインの状態をプロパティで管理し、それをもとにデザインを指定するところは同じです。
struct CustomViewEx2: View {
let text: String
let fontWeight: Font.Weight? // 文字の太さのプロパティ
let boxColor: Color // 図形の色のプロパティ
// 1 変更点1: デフォルトのイニシャライザ
init(_ text: String) {
self.text = text
self.fontWeight = .regular // デフォルト値を指定する
self.boxColor = .orange // デフォルト値を指定する
}
var body: some View {
Text(self.text)
.fontWeight(self.fontWeight) // 文字の太さを指定する
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(self.boxColor) // 図形の色を指定する
)
}
}
// 2 変更点2
extension CustomViewEx2 {
// 2 変更点2: プロパティを変化させるためのイニシャライザ
// * アクセスレベルを private にすることで宣言時に見えない
private init(_ text: String, fontWeight: Font.Weight?, boxColor: Color) {
self.text = text
self.fontWeight = fontWeight
self.boxColor = boxColor
}
// 2 変更点2: フォントの太さを指定するメソッド
// 太さを引数として受け取り、それを使って再度カスタムビューを宣言する
func fontWeight(_ weight: Font.Weight?) -> Self {
CustomViewEx2(self.text, fontWeight: weight, boxColor: self.boxColor)
}
// 2 変更点2: 図形の色を指定するメソッド
// 色を引数として受け取り、それを使って再度カスタムビューを宣言する
func boxColor(_ color: Color) -> Self {
CustomViewEx2(self.text, fontWeight: self.fontWeight, boxColor: color)
}
}
ちょっと長いですが変更点は2箇所です。それぞれ解説します!
デフォルトのイニシャライザ
// 1 変更点1: デフォルトのイニシャライザ
init(_ text: String) {
self.text = text
self.fontWeight = .regular // デフォルト値を指定
self.boxColor = .orange // デフォルト値を指定
}
これはカスタムビューを宣言するときに使われるイニシャライザです。文章のみを入力として受け取り、受け取った文章と文字の太さと図形の色のデフォルトの値をそれぞれのプロパティに代入しています。これによりカスタムビューの使用者は宣言時は入力する文章のことだけ考えれば良くなります。
プロパティを変化させる方法
ビューのレイアウトを決めるvar body: some View { }
のカッコ内ではプロパティを変化させる "mutating function" は使えず、通常の方法でプロパティを変化させられません。そこで、直接プロパティを変化させるのではなく、変えたい値を使ってもう一度カスタムビューを宣言し、それを戻り値として返すということをします。こうすることで、間接的にプロパティを変化させられます。
// 2 変更点2: プロパティを変化させるためのイニシャライザ
// * アクセスレベルを private にすることで宣言時に見えない
private init(_ text: String, fontWeight: Font.Weight?, boxColor: Color) {
self.text = text
self.fontWeight = fontWeight
self.boxColor = boxColor
}
プロパティを変化させるためのイニシャライザです。このイニシャライザをインスタンスメソッドから呼び出して間接的にプロパティを変化させます。private init(略...)
とすることで外部からこのイニシャライザにアクセスできなくなるのでカスタムビューの宣言時に必要ない情報を隠すことができます。
// 2 変更点2: フォントの太さを指定するメソッド
// 太さを引数として受け取り、それを使って再度カスタムビューを宣言する
func fontWeight(_ weight: Font.Weight?) -> Self {
CustomViewEx2(self.text, fontWeight: weight, boxColor: self.boxColor)
}
// 2 変更点2: 図形の色を指定するメソッド
// 色を引数として受け取り、それを使って再度カスタムビューを宣言する
func boxColor(_ color: Color) -> Self {
CustomViewEx2(self.text, fontWeight: self.fontWeight, boxColor: color)
}
}
プロパティを変化させるためのメソッドです。引数でデザインを指定してもらいそれを使って再度カスタムビューを宣言します。このときイニシャライザの他の引数にはself.プロパティ名
を渡し変更が伝搬していくようにします。func メソッド名(略...) -> Self { }
と戻り値の型を Self (カスタムビューの型)にしているところも大事です。これによりカスタムビューのインスタンスメソッドを複数個連続して使用できます1。
struct ContentView: View {
var body: some View { // ←この内側では mutating function は使えない
VStack(spacing: 10) {
CustomViewEx2("Hello World")
CustomViewEx2("Custom View")
.boxColor(.blue)
.font(.largeTitle)
CustomViewEx2("Natto Gohan")
.fontWeight(.black) // 連続して
.boxColor(.black) // 使用できる
.foregroundColor(.white)
}
}
}
使用例です。宣言時の引数が1つになりビューの役割が明確化されました。また、ビューモディファイアと同じ記法でデザインを変化させているので統一感があります。
(同じことをしてるので上の例と全く同じ結果になっています)
比較
説明した2つの方法を比較すると、イニシャライザでデザインを指定する方法は、実装は簡単ですがカスタムビューの使用時にコードが複雑化してしまう可能性があります。また、インスタンスメソッドでデザインを指定する方法は、実装が少し面倒な分使用時はすっきりしています。
実装しやすさ | 使いやすさ | |
---|---|---|
方法1 | ○ | ✖︎ |
方法2 | ✖︎ | ○ |
コード補完の違い
2つの方法のコード補完の違いを見てみると、方法1では2種類の候補が出ているのに対し方法2では1つの候補のみ表示されています。使用方法の違いを見比べる
最後に変更前と2つの変更後の計3つのカスタムビューを並べて終わりたいと思います。
struct ContentView: View {
var body: some View {
VStack(spacing: 10) {
CustomView("Merry🎄")
CustomViewEx1("Christmas🎁", fontWeight: .black, boxColor: .green)
CustomViewEx2("(๑•ૅㅁ•๑)۶🍗")
.fontWeight(.black)
.boxColor(.red)
}
.font(.largeTitle)
.foregroundColor(.white)
}
}
まとめ
カスタムビューにビューモディファイア以外の Text などのメソッドを間接的に使用する方法を紹介しました。もし必要なときがきたら用途に合わせて使ってもらえたら嬉しいです。
質問やおかしな点がありましたらコメントしていただけると嬉しいです。
最後まで読んでくださりありがとうございました!٩(๑❛0❛๑)۶メリクリ
-
一つのメソッドのみしか使わないのなら戻り値の型は
some View
でも構いません ↩