この記事はレコチョク Advent Calendar 2024の25日目の記事となります。
はじめに
メリークリスマス!
株式会社レコチョクでiOSアプリ開発をしている副山です。
2024年もあっという間に終わりを迎えようとしていますね。今年は地元の友だちからの結婚報告が増えてきて、幸せな気持ちを分けてもらえた1年でした。
そんな中、自分も「新しいことを始めてみよう」と思い立ち、SwiftUIで楽しみながら学べる企画を立ててみました。
現在業務で開発に携わっているiOSアプリでは、サポート対象の最低iOSバージョンがiOS 12に設定されており、新しい技術であるSwiftUIの導入が難しい状況です。そのため、業務内でSwiftUIを学ぶ機会がなく、ついつい後回しになってしまっていました。
独学でキャッチアップしようと考えていたものの、何から始めればいいか悩んでいました。そんなとき、以前見かけたSwiftUIを使って人気キャラクターを描く記事や動画を思い出しました。
「これなら楽しく学べそう!」と感じ、今回の企画を考えました。
どうせなら社内のマスコットキャラクター「レコチョクマ」を題材にしてみようと考え、広報担当の方に相談。
「会社名を出したブログで掲載する以上は、公開前に広報チェックをして、あまりにも似ていない場合は公開不可という判断になるかもしれません」という厳しい条件付きで使用許可をいただき、この企画がスタートしました。
レコチョクには「レコチョクマ」という、音楽をこよなく愛する白いクマのマスコットキャラクターがいます。のんびりマイペースな性格で、特技は昼寝と鼻歌です。
今回は、このレコチョクマの顔をSwiftUIで描いてみます。制作過程や実装のポイントを通して、SwiftUIの魅力もお伝えできればと思います。
最初にイメージしていた完成像
SwiftUIで描きはじめる前に、パワーポイントの図形を駆使して完成形のイメージを作りました。
あれ、、全然似てない…?
自分には絵心がないことを思い出しました。
最終的に広報の方にツッコまれるかやや心配になりましたが、とりあえず作ってみることにしました。
利用した環境
本記事では以下の環境を使用して、SwiftUIでレコチョクマを描きました。
- Xcode:16.1
- Swift:6.0
- iOS:18.1
- Xcode Previews:iPhone Dynamic Island
注意事項:
レコチョクマは株式会社レコチョクのオフィシャルキャラクターです。本記事で紹介した内容を参考にして、ご自身でレコチョクマを描いてみるのは大歓迎です。
ただし、作成した作品をインターネット上など不特定多数の方が閲覧できる場所へ公開することはお控えください。
お絵かきの進め方
顔の各パーツは次のような流れで作っていきます。
- どの形のViewを使用するか決める
- サイズを決める
- 色や枠線の太さなどを決める
- 位置や角度の調整をする
お絵かきの事前準備 - 色を定義
レコチョクマで使用されている黒色と、「レ」の形をした自慢の前髪のピンク色をあらかじめ定義します。
その他の白い部分は完全な白であり、 Color.white
をそのまま使用できるので定義していません。
/// カラー定義
extension Color {
static let recochokumaBlack = Color(red: 44 / 255, green: 46 / 255, blue: 53 / 255)
static let recochokumaPink = Color(red: 249 / 255, green: 56 / 255, blue: 126 / 255)
}
眉毛と目を作る
はじめに一番簡単そうな眉毛と目から作ることにしました。
眉毛の丸みを帯びた直線の棒の形を再現するため、 Capsule()
を使用しました。
/// 眉毛
struct Eyebrows: View {
var body: some View {
VStack {
Spacer()
.frame(height: 30)
HStack(spacing: 180) {
Capsule()
.frame(width: 55, height: 5)
.foregroundStyle(Color.recochokumaBlack)
.rotationEffect(.degrees(-5))
Capsule()
.frame(width: 50, height: 5)
.foregroundStyle(Color.recochokumaBlack)
.rotationEffect(.degrees(3))
.offset(x: 9, y: -10)
}
}
}
}
レコチョクマの目は正円ではなく楕円っぽく見えたので、 Ellipse()
を使用しました。
/// 目
struct Eyes: View {
var body: some View {
VStack {
Spacer()
.frame(height: 70)
HStack(spacing: 220) {
Ellipse()
.frame(width: 15, height: 15)
.foregroundStyle(Color.recochokumaBlack)
.offset(x: 2, y: 5)
Ellipse()
.frame(width: 15, height: 13)
.foregroundStyle(Color.recochokumaBlack)
.rotationEffect(.degrees(-20))
.offset(x: 10, y: -2)
}
}
}
}
作成した眉毛と目がXcode Previewsに表示されるように ContentView
に追加します。
これ以降作成したパーツは ZStack
内に追加していきます。
/// メインビュー
struct ContentView: View {
var body: some View {
ZStack {
Eyebrows()
Eyes()
// これ以降作成したパーツはここに追加
// ...
}
.frame(width: 500, height: 500)
}
}
顔(土台)を作る
レコチョクマの顔はやっかいなことにきれいな楕円ではありません。
パワーポイントで作ったレコチョクマは、楕円の図形を使用していますが、やっぱり顔の形が似ていません。
うまく再現できないかなと調べてみると、Pathを使用することで自在に表現できることがわかりました。
今回は顔の形を4つのカーブと1つの直線の組み合わせで表現することにしました。
顔を①から⑤までの点で分割して考えます。
初めに move(to:)
で描画開始位置を決めます。
次に addQuadCurve(to:control:)
を使用して2次ベジェ曲線を描きます。引数の to:
(終点)を指定し、 control:
(制御点)の位置を調整することで、滑らかで好みのカーブを描くことができます。
最後に⑤の点までカーブを描いたら、closeSubpath()
を使用して現在の点(⑤)から始点(①)まで直線で繋げ、図形を閉じます。
/// 顔(土台)
struct Face: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 185, y: 525)) // ① 描画開始位置
path.addQuadCurve(to: CGPoint(x: 85, y: 410), control: CGPoint(x: 85, y: 520)) // ② 2次ベジェ曲線
path.addQuadCurve(to: CGPoint(x: 240, y: 275), control: CGPoint(x: 100, y: 290)) // ③ 2次ベジェ曲線
path.addQuadCurve(to: CGPoint(x: 430, y: 410), control: CGPoint(x: 410, y: 280)) // ④ 2次ベジェ曲線
path.addQuadCurve(to: CGPoint(x: 325, y: 525), control: CGPoint(x: 430, y: 520)) // ⑤ 2次ベジェ曲線
path.closeSubpath() // ⑤と①を繋げる
}
.fill(Color.white)
.stroke(Color.recochokumaBlack, lineWidth: 6)
.position(x: 250, y: 120)
}
}
耳を作る
次に耳を作ります。
顔(土台)と同様にPathを使用しても良いですが、今回は楕円の角度を調整する形で作りました。
顔(土台)と耳がきれいに繋がるように、重なる部分を切り取ることがポイントです。
trim(from:to:)
を使用することで切り取りできます。
/// 耳
struct Ears: View {
var body: some View {
VStack {
HStack(spacing: 150) {
Capsule()
.trim(from: 0.23, to: 0.94) // 顔(土台)と耳の接点で切り取り
.fill(Color.white)
.stroke(Color.recochokumaBlack, lineWidth: 6)
.rotationEffect(.degrees(30))
.frame(width: 83, height: 75)
.offset(x: 7, y: -3)
Capsule()
.trim(from: 0.15, to: 0.82) // 顔(土台)と耳の接点で切り取り
.fill(Color.white)
.stroke(Color.recochokumaBlack, lineWidth: 6)
.rotationEffect(.degrees(130))
.frame(width: 80, height: 75)
.offset(x: 15, y: 12)
}
Spacer()
.frame(height: 190)
}
}
}
鼻を作る
次に鼻をシンプルな楕円形で作ります。
Pathでシンプルな楕円を描くとき、init(ellipseIn:)
を使うと簡単に書けました。
/// 鼻
struct Nose: View {
var body: some View {
VStack {
Spacer()
.frame(height: 77)
Path(ellipseIn: CGRect(x:0, y: 0, width: 50, height: 30))
.fill(Color.recochokumaBlack)
.frame(width: 50, height: 30)
}
}
}
口を作る
次に口を作ります。
レコチョクマの口の形は独特な形状をしています。Pathを使用して口の周囲を描き、overlay(alignment:content:)
で口の中央の縦線のViewを重ねました。
overlay(alignment:content:)
の代わりに ZStack
を使用して重ねても問題ありません。
/// 口
struct Mouth: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 185, y: 395))
path.addCurve(to: CGPoint(x: 325, y: 395), control1: CGPoint(x: 90, y: 270), control2: CGPoint(x: 410, y: 240))
}
.stroke(
Color.recochokumaBlack,
style: StrokeStyle(lineWidth: 6, lineCap: .round, lineJoin: .round)
)
.overlay {
Path { path in
let centerX = 250.0 + 2.0 // 楕円の中心X座標 (frame幅の半分 + 少し右にずらす)
path.move(to: CGPoint(x: centerX, y: 290))
path.addLine(to: CGPoint(x: centerX, y: 395))
}
.stroke(Color.recochokumaBlack, lineWidth: 6)
.rotationEffect(.degrees(-3))
}
}
}
前髪(レの形)を作る
次にレコチョクマが毎日セットはかかさない自慢の前髪を作ります。
SwiftUIでレコチョクマを描くうえで、「レ」の形を再現するのが最難関といっても過言ではありません。
リアルに再現したいので、顔(土台)と同様にPathを使用して作りました。
前髪を①から⑤までの点で分割して考えます。
move(to:)
で描画開始位置を決めます。今回は「レ」のハネの部分から描き始めます。
次に addQuadCurve(to:control:)
を使用して2次ベジェ曲線を描きます。
②〜⑤については、addLine(to:)
を使用して直線を描きます。
最後に、closeSubpath()
を使用して現在の点(⑤)から始点(①)まで直線で繋げ、図形を閉じます。
この手順を黒塗りのベース部分とピンクの部分で同様に行い、ピンクの部分の点の位置を調整すると出来あがります。
/// 前髪(レの形)
struct Bangs: View {
var body: some View {
// 黒塗りのベース部分
Path { path in
path.move(to: CGPoint(x: 282, y: 196)) // ① 描画開始位置
path.addQuadCurve(to: CGPoint(x: 250, y: 220), control: CGPoint(x: 270, y: 215)) // ② 2次ベジェ曲線
path.addLine(to: CGPoint(x: 250, y: 180)) // ③ 直線
path.addLine(to: CGPoint(x: 259, y: 178)) // ④ 直線
path.addLine(to: CGPoint(x: 258, y: 209)) // ⑤ 直線
path.closeSubpath() // ⑤と①を繋げる
}
.stroke(
Color.recochokumaBlack,
style: StrokeStyle(lineWidth: 11, lineCap: .round, lineJoin: .round)
)
.overlay {
// ピンクの部分
Path { path in
path.move(to: CGPoint(x: 273, y: 205)) // ① 描画開始位置
path.addQuadCurve(to: CGPoint(x: 253, y: 216), control: CGPoint(x: 270, y: 210)) // ② 2次ベジェ曲線
path.addLine(to: CGPoint(x: 253, y: 185)) // ③ 直線
path.addLine(to: CGPoint(x: 255, y: 184)) // ④ 直線
path.addLine(to: CGPoint(x: 254, y: 213)) // ⑤ 直線
path.closeSubpath() // ⑤と①を繋げる
}
.stroke(
Color.recochokumaPink,
style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round)
)
}
.rotationEffect(.degrees(-2)) // 「レ」の傾きを再現
}
}
レコチョクマの完成
ここまででレコチョクマが無事に完成しました!
広報の方に自身を持って披露できる出来栄えだと思います。
ただここで終わらないのがエンジニア魂です
本記事の公開日が2024年12月25日なので、クリスマス仕様にアレンジしていこうと思います。
サンタクロース帽を作る
クリスマスといえばサンタクロースだと思うので、レコチョクマにサンタクロースの帽子を被せます。
サンタクロースのイラストを画像検索して出てきたものを参考にして作りました。
赤い部分、ボンボンの部分、かぶる部分の3つのパーツに分かれています。
長くなるのですべては説明しませんが、赤い部分とかぶる部分で使用した addCurve(to:controlPoint1:controlPoint2:)
がポイントです。
これまで使用した addQuadCurve(to:control:)
は2次ベジェ曲線を描くのに対し、addCurve(to:controlPoint1:controlPoint2:)
は3次ベジェ曲線を描くときに使用します。
controlPoint
(制御点)が2つに増えることで、より自由度の高い形を作ることができます。
/// 帽子
struct Hat: View {
var body: some View {
ZStack {
// 赤い部分
Path { path in
path.move(to: CGPoint(x: 195, y: 135))
path.addCurve(
to: CGPoint(x: 315, y: 130),
control1: CGPoint(x: 230, y: -40),
control2: CGPoint(x: 440, y: 140)
)
path.addQuadCurve(to: CGPoint(x: 200, y: 125), control: CGPoint(x: 260, y: 100))
}
.fill(Color.red)
.stroke(Color.gray, lineWidth: 4)
// ボンボンの部分
Path { path in
path.addEllipse(in: CGRect(x: 340, y: 105, width: 25, height: 25))
}
.fill(Color.white)
.stroke(Color.gray, lineWidth: 4)
// かぶる部分
Path { path in
path.move(to: CGPoint(x: 195, y: 155))
path.addCurve(
to: CGPoint(x: 330, y: 160),
control1: CGPoint(x: 160, y: 100),
control2: CGPoint(x: 360, y: 100)
)
path.addQuadCurve(to: CGPoint(x: 195, y: 155), control: CGPoint(x: 260, y: 140))
path.closeSubpath()
}
.fill(Color.white)
.stroke(Color.gray, lineWidth: 4)
}
}
}
サンタクロース帽をかぶるとかわいくなってきました。
メッセージを表示する
レコチョクマだけだと味気ないので、「Merry Christmas!」というメッセージを表示させます。
異なる色の文字を重ねて、位置を少しずらすことで立体感を表現しました。
文字があるだけで急にポストカードっぽく見えてきました。
/// メッセージ
struct ChristmasMessage: View {
private let messageText = "Merry Christmas!"
var body: some View {
ZStack {
// 黒色の影
Text(messageText)
.font(.largeTitle)
.fontWeight(.bold)
.fontDesign(.monospaced)
.offset(x: -1, y: -1) // 少しずらす
.overlay(
// 白色の文字
Text(messageText)
.font(.largeTitle)
.fontWeight(.bold)
.fontDesign(.monospaced)
.foregroundColor(Color.white)
.offset(x: 1, y: 1)
)
// 赤色の文字
Text(messageText)
.font(.largeTitle)
.fontWeight(.bold)
.fontDesign(.monospaced)
.foregroundColor(Color.red)
}
.position(x: 250, y: 450)
}
}
雪を降らせる
サンタクロース仕様になった所で、最後に雪を降らせてみようと思います。
雪は白色でこのままだと背景の白色と被って見えないので、背景をクリスマスっぽい緑色に設定します。
画面全体の色を変更したいので、レコチョクマを描いた ZStack
をさらに ZStack
で入れ子にして色を設定します。
/// メインビュー
struct ContentView: View {
var body: some View {
ZStack {
Color.green
.ignoresSafeArea() // SafeAreaを無視して全画面に描画
ZStack {
Ears()
Face()
Hat()
Mouth()
Bangs()
Eyebrows()
Eyes()
Nose()
ChristmasMessage()
// SnowfallBackground() // 今から作る雪の背景
}
.frame(width: 500, height: 500)
}
}
}
次に、雪の1つ1つの情報(位置、大きさ、速さ)を表すデータを定義し、それを使って雪を降らせる仕組みを作ります。
自然な動きを表現するため、雪がランダムな位置から降り始め、大きさや降る速さもバラバラになるように設定しました。
/// 雪
struct Snowflake {
var position: CGPoint // 位置
let size: CGFloat // 大きさ
let duration: Double // 降る速さ
// ランダムな雪を生成
static func random() -> Snowflake {
Snowflake(
position: CGPoint(x: .random(in: 0...450),
y: .random(in: -400 ... -200)),
size: .random(in: 2...6),
duration: .random(in: 9...13)
)
}
}
この仕組みを利用して、雪を降らせる背景を作ります。
SwiftUIは宣言的なUIフレームワークのため、画面の見た目や動きを簡潔に記述できます。今回のコードでは、以下のSwiftUIの特徴を活用しました。
- 状態管理
@State
を使って雪の情報(位置、大きさ、速さ)を保持しています。この状態が変わると、SwiftUIが自動的に画面を再描画します。 - アニメーション
雪が自然に降るように、withAnimation
で動きを指定しました。repeatForever
を使うことで、雪が途切れることなく降り続けるように設定しています。 - 繰り返し描画
ForEach
を使用して、動的に複数の雪を描画しています。これは、SwiftUIで動的にViewを構成する場合によく用いられる方法で、ループ処理とビューの組み立てを同時に行える点が特長です。 - 非同期処理
task
修飾子を使い、非同期処理を記述しました。task
はSwiftUIのライフサイクルに統合されており、ビューの表示や非表示に応じてタスクが自動で管理されます。これにより、従来のDispatchQueue
を使用したコードよりも、SwiftUIの宣言的スタイルに適した記述が可能になります。また、Task.sleep
を用いてランダムな遅延を追加し、雪が自然なタイミングで降り始める演出を実現しました。
/// 雪の背景
struct SnowfallBackground: View {
@State private var snowflakes: [Snowflake] = (0..<50).map { _ in Snowflake.random() }
var body: some View {
ZStack {
// 雪
ForEach(snowflakes.indices, id: \.self) { index in
Circle()
.fill(Color.white)
.frame(width: snowflakes[index].size, height: snowflakes[index].size)
.position(snowflakes[index].position)
.task {
// ランダムな遅延を追加
try? await Task.sleep(nanoseconds: UInt64.random(in: 0...5) * 1_000_000_000)
// アニメーションで雪を落とす
withAnimation(
Animation.linear(duration: snowflakes[index].duration)
.repeatForever(autoreverses: false)
) {
snowflakes[index].position.y += 1000 // 下に降らせる
}
}
}
}
}
}
完成
こうして雪が降るクリスマス仕様のレコチョクマが完成しました!
自然な雪の降り方で画面に立体感が加わり、一気にクリスマスらしい雰囲気になりました!
GIFなので雪がカクついて見えますが、実際のXcode Previews上では滑らかに降ります!
最後に
本記事では、SwiftUIの宣言的な仕組みを利用して、クリスマス仕様のレコチョクマを完成させました。制作を通じて改めてSwiftUIの魅力を感じることができました。
宣言的な記述スタイルによってUIの動きやレイアウトを直感的に書ける点や、状態管理とUI更新がシームレスに連動する仕組みは、これまでのUIKitとは一味違う楽しさがありました。
さらに、Path
や task
、アニメーションといった機能を組み合わせることで、予想以上に複雑な図形の描画や自然な動きをシンプルに実装できました。
少しでもSwiftUIやiOSアプリ開発の楽しさが伝わっていれば嬉しいです。
企画段階では「鬼広報」による厳しいチェックを覚悟しつつ制作に挑みましたが、完成したデザインは広報チェックで『想像以上のクオリティ』との高評価をいただき、一発合格!
レコチョクマのほっぺたのふくらみやサンタ帽のデザインまでしっかり認められ、無事に公開することができました。
最後まで読んでいただき、ありがとうございました!
また、レコチョク Advent Calendar 2024をお楽しみいただき、感謝いたします。
株式会社レコチョクでは、新卒・中途採用を募集しています。興味のある方は、採用サイトをご覧ください。
それでは良いクリスマスと素敵な年末をお過ごしください!
この記事はレコチョクのエンジニアブログの記事を転載したものとなります。