こんにちは、23卒の ojun です!
2023年9月1日(金)から3日(日)までオンライン・オフラインのハイブリッド形式で開催された iOSDC Japan 2023 にて、DeNA はゴールドスポンサーとして協賛しました! また、DeNA は iOSDC Japan 2023 の「スポンサーブース」に出展し、iOS App エンジニアたちが考えた企画である「SwiftUI 脳内プレビュー大会」を実施しました!!
当日はたくさんの方々にご参加いただき、大盛況となりました。「SwiftUI 脳内プレビュー大会」で集めた解答用紙は、なんと243枚にものぼりました! このブログでは、「SwiftUI 脳内プレビュー大会」の問題とその答え、さらに解説を行います。
*本問題の動作環境はiOS 14.8以上となっています。
Question1の問題と解説
Question1の問題とPreview結果は以下のとおりです。
import SwiftUI
struct Question1View: View {
private let helloString = "Hello, iOSDC Japan 2023!"
var body: some View {
VStack {
HStack {
Text(helloString)
.layoutPriority(1)
Spacer()
Text(helloString)
}
Spacer()
Text(helloString)
Spacer()
HStack {
Text(helloString)
Spacer()
Text(helloString)
.layoutPriority(1)
}
}
.padding()
}
}
解説
この問題は、右上・右下・左上・左下・中央に配置されているHello, iOSDC Japan 2023!
をプレビューする問題です。
キーポイント
この問題を解くためのポイントは、
layoutPriority
にあります。
このメソッドを利用し値を設定することで、親レイアウトが子レイアウトにスペースを割り当てる優先順位を設定します。デフォルトは0
です。
問題では2つのText
に対して.layoutPriority(1)
が割り当てられており、これはデフォルト値である0
よりも大きいので、改行がされなくなります。
このメソッドを使って値を設定することで、親レイアウトが子レイアウトにスペースを割り当てる優先順位を変更できます。
デフォルトの優先順位は0
です。問題文では、2つのText
ビューに.layoutPriority(1)
が設定されており、その結果、自動での改行が避けられています。
反省点
テキストHello, iOSDC Japan 2023!
の長さが解答用紙のサイズを超えていたため、完全に書ききれない問題が生じていました。
また、立ちながらの書き込みが難しく、文字数を少なくするべきでした。
Question2の問題と解説
Question2の問題とPreview結果は以下のとおりです。
import SwiftUI
struct Question2View: View {
@State private var sliderValue: Double = 50.0
@State private var isOn = true
@State private var selectedDate = Date()
var body: some View {
Form {
Section(header: Text("iOSDC Japan 2023")) {
Toggle(isOn: $isOn) {
Text("Toggle")
}
Slider(value: $sliderValue, in: 0...100) {
Text("Slider")
}
DatePicker("Picker", selection: $selectedDate, displayedComponents: .date)
}
}
.navigationBarTitle("DeNA Booth")
}
}
解説
この問題では、Form
とSection
を中心に構築された標準的なUIコンポーネントのプレビューがテーマとなっています。
多くの設定画面などで見かける典型的なUIです。
Slider
のイニシャライザにおけるlabel
の引数にText("Slider")
という値が与えられていますが、これが画面上には表示されない点がトリッキーな部分です。
キーポイント
Slider
のイニシャライザ、
init(value:in:onEditingChanged:label:)
におけるlabel
は、そのインスタンスの機能や目的を説明するためのビューとして提供されます。
このlabelが必ずしも画面上に表示されるわけではない点を理解することが問題を解く鍵になります。実際、指定したlabel
は表示されず、VoiceOverのようなアクセシビリティ機能の際に活用されます。
また、問題で利用した
init(value:in:onEditingChanged:label:)
は、Deprecated
となっています。
将来的には、
init(value:in:label:onEditingChanged:)
の形式を利用することを推奨します。
よくあった間違い
多くの人が、Text("Slider")
の部分が画面上に表示されると考え、Slider
の横にSlider
という文字が出ると予想する誤りが見られました。
Question3の問題と解説
Question3の問題とPreview結果は以下のとおりです。プレビュー結果に関してはAppleのロゴが写っているのでスキップさせて頂きます。
import SwiftUI
struct Question3View: View {
var body: some View {
Image(systemName: "apple.logo")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.mask(Text(String(repeating: "ILoveSwift", count: 30)))
}
}
解説
この問題はILoveSwift
という30個の文字がAppleのロゴの形にマスクされている問題です。
マスクされた結果、AppleロゴのImage
は「ILoveSwiftILoveSwift…」というテキストの形状に従いクリップされた上で表示されます。
この問題では、文字列ILoveSwift
を利用し、その文字がAppleのロゴ形状に沿った形でマスクされた上で描写されています。
具体的には、ILoveSwift
という文字が繰り返され、その合計30文字がAppleのロゴの形状にマスク(クリップ)されます。
その結果として、AppleロゴのImage
は「ILoveSwiftILoveSwift…」という文字列の形状を取り、このテキストのシルエットに沿って表示されます。
反省点
小さな文字を紙に書くのが大変そうでしたので、Text
ではなくImage
等を利用するべきだと思いました。
この問題は、小さな文字を利用してロゴの細部まで正確に再現するのが難しく、紙に解答するのが難しそうでした。
文字ではなくImage
などを用いることで、より解答しやすかったのかなと考えています。
Question4の問題と解説
Question4の問題は以下の通りです。
import SwiftUI
struct Question4View: View {
var body: some View {
Form {
Section(header: Text("iOSDC Japan 2023")) {
HStack {
Rectangle()
.fill(.gray)
.border(.black)
.frame(width: 100, height: 300)
.overlay(Text("DeNA").bold())
Spacer()
Circle()
.fill(.gray)
.border(.black)
.frame(width: 100, height: 100)
.overlay(Text("Booth").bold())
}
.padding(.horizontal, 50)
}
}
}
}
解説
この問題では、基本的な図形であるRectangle()
とCircle()
を用いたプレビューが課題となっています。
これらの図形間にSpacer
が挿入されており、その結果、図形が均等な間隔で配置されるような表示になっています。
キーポイント
注目すべきポイントとして、Circle()
に.border(.black)
を適用する場合の動作があります。
.border(.black)
を用いると、円でなくframe
の周りに枠線を引くためこのような表示になります。
よくあった間違い
多くの人々が、Circle()
に.border(.black)
を適用した際に、円の外周を丸く囲むように書いてしまう間違いが多かったように感じました。
反省点
実際の問題の目的が図形のプレビューだけであるのなら、Form
やSection
のような追加のコンポーネントを利用する必要はなかったのではないかと感じています。
Question5の問題と解説
Question5の問題とPreview結果は以下のとおりです。
import SwiftUI
struct Question5View: View {
var body: some View {
VStack(spacing: -20) {
Text("SwiftUI")
.font(.largeTitle)
.padding()
.border(.black)
.offset(x: 10, y: 10)
HStack {
Rectangle()
.fill(.clear)
.border(.black)
.frame(width: 100, height: 100)
Circle()
.frame(width: 90, height: 90)
.mask(
Rectangle()
.fill(.clear)
.border(.black)
.rotationEffect(.degrees(45))
.padding(15)
)
}
}
}
}
解説
この問題は、特定の図形を描画する問題です。
前述のQuestion4
との主要な違いは、図形の描画プロセスにおいて、使用される各種属性やモディファイアの影響を理解し、その情報を基に描画される図形の位置や大きさを正確に推定する必要があることです。
特に、frame
、padding
、そしてrotationEffect
といったモディファイアがこの課題での鍵となっています。
加えて、VStack
自体にspacing
として-20
が設定されており、これがもたらすエフェクトも考慮に入れる必要があります。
このspacing
の設定により、図形同士の配置や間隔が一般的なデフォルトの状態とは異なる複雑な表示を持つこととなります。
Question6の問題と解説
Question6の問題とPreview結果は以下のとおりです。
import SwiftUI
struct Question6View: View {
private let symbols = [
"chevron.forward", "square.and.arrow.up", "ellipsis",
"bell", "paperplane", "trash",
"camera", "photo", "map",
]
private let columns: [GridItem] = [
.init(.flexible(), spacing: 16),
.init(.flexible(), spacing: 16),
.init(.flexible()),
]
var body: some View {
VStack {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(symbols, id: \.self) { symbol in
Image(systemName: symbol)
.font(.system(size: 48))
.frame(minWidth: 0, maxWidth: .infinity)
}
}
}
.padding()
}
}
解説
この問題では、Appleが提供する9種類のSF Symbols
を正確にプレビューする能力が求められます。これにより、ユーザーのApple製品やSF Symbols
への知識と愛情が試されます。
よくあった間違い
特にellipsis
やchevron.forward
のシンボルを正確にプレビューできなかった方が多く見受けられました。
一方で、map
のシンボルを正確に描いている方が多く、びっくりしました。
Question7の問題と解説
Question7の問題とPreview結果は以下のとおりです。
import SwiftUI
struct Question7View: View {
private let symbols = [
"iphone.gen1", "iphone.gen2", "iphone.gen3",
]
private let columns: [GridItem] = [
.init(.flexible(), spacing: 16),
.init(.flexible(), spacing: 16),
.init(.flexible()),
]
var body: some View {
VStack {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(symbols, id: \.self) { symbol in
Image(systemName: symbol)
.font(.system(size: 120))
.frame(minWidth: 0, maxWidth: .infinity)
}
}
}
.padding()
}
}
解説
この問題はQuestion6
と同様にSF Symbols
をプレビューする問題です。
この問題で登場するSF Symbols
はiOS 16.1+で利用できます。
キーポイント
iphone.gen3
のアイコンでは、dynamic island
が表現されています。この特定のデザイン要素は、このモデルのiPhoneの特徴を強調している部分であり、シンボルの中で特に注目すべき点です。
iphone.gen3
がdynamic island
が表現されているアイコンであることに気づくと、iphone.gen1
とiphone.gen2
も推測することができます。
少しあった間違い
アイコンを紙に描く際に、横一列の配置ではなく、縦一列に並べて描く間違いがありました。
Question8の問題と解説
Question8の問題とPreview結果は以下のとおりです。
import SwiftUI
struct Question8View: View {
private struct RadialItem: View {
let text: String
let angle: Double
private let offsetValue: CGFloat = 100
var body: some View {
Text(text)
.font(.title)
.padding()
.border(.black)
.offset(x: cos(CGFloat(angle)) * offsetValue, y: sin(CGFloat(angle)) * offsetValue)
}
}
private let items = [Int](0..<6)
var body: some View {
ZStack {
ForEach(0..<items.count, id: \.self) { index in
RadialItem(
text: index.description,
angle: 2 * .pi / Double(items.count) * Double(index)
)
}
}
}
}
解説
この問題は、円環上に配置された数字を正確にプレビューする問題です。 円周上の数字の配置や回転に関する高校数学の知識が試されています。
キーポイント
注意するべきは、最初に表示される0
が画面の右側から始まることです。
これは、数学的にx軸の正の方向と一致しており、ここから時計回りに数字が表示されるという挙動が重要です。
よくあった間違い
Text
の周囲に.border(.black)
モディファイアが適用されていることの見落としが多かったです。
このモディファイアにより、各数字が黒い枠で囲まれることになります。
さらに、数字を時計回りではなく反時計回りに配置する間違いもありました。
Question9の問題と解説
Question9の問題とPreview結果は以下のとおりです。
import SwiftUI
struct Question9View: View {
private struct CustomAlignedText: View {
let text: String
let offset: CGFloat
var body: some View {
Text(text)
.font(.system(size: 48))
.alignmentGuide(.customLeading) { dimensions in
dimensions[.leading] + offset
}
}
}
var body: some View {
VStack(alignment: .customLeading) {
CustomAlignedText(text: "SwiftUI", offset: 0)
CustomAlignedText(text: "is", offset: 30)
CustomAlignedText(text: "Amazing!", offset: 60)
}
.border(.black)
}
}
extension HorizontalAlignment {
static let customLeading = HorizontalAlignment(CustomLeading.self)
private enum CustomLeading: AlignmentID {
static func defaultValue(in dimensions: ViewDimensions) -> CGFloat {
dimensions[.leading]
}
}
}
解説
この問題では、CustomAlignedText
という独自のText
が内部定義されており、表示するテキストとそのオフセットを持っています。
このカスタムテキストビューは、与えられたoffsete
をもとに、Text
が左方向にどれだけずらされるかを管理します。このoffset
は、.alignmentGuide
モディファイアを使って実現されるのが特徴です。
キーポイント
Alignment GuideはSwiftUI
のS
の真左側に配置されており、これを基点に残りの2つのText
が段階的に左に配置されることがポイントです。
このAlignment Guideの詳しい動きや役割については、
iOSDC Japan 2022: 入門 SwiftUI Alignment Guide / monoqlo
にて詳しく解説されているので、興味のある方はこちらを参照すると良いでしょう。
よくあった間違い
3つのText
が段階的に左ではなく、右側に配置されると解釈された方が多く見られました。
反省点
offset
という変数名が、問題の解答者に誤解を与えた可能性がありました。
オフセットの実際の意味や動作と異なる動きを示唆する名前であったため、let offset: CGFloat
の部分をlet spacing: CGFloat
のようにもっと直感的な命名にすべきでした。
Question10の問題と解説
Question10の問題とPreview結果は以下のとおりです。
import SwiftUI
struct Question10View: View {
var body: some View {
VStack {
Button {
print("Hello iOS engineer")
} label: {
Capsule()
.fill(.orange)
.frame(width: 300, height: 48)
.overlay(
Label {
Text("DeNA Booth")
} icon: {
Image(systemName: "heart.fill")
}
.font(.headline.bold())
.foregroundColor(.white)
)
}
}
}
}
解説
この問題は、特定の形状(楕円)を持つボタンの描画に関するものです。
SwiftUIのButtonコンポーネントを使用しており、具体的にはButtonの
init(action:label:)
を利用しています。
このボタンの見た目は、
Capsule()
を用いることで楕円形状となっており、ボタンの上には.overlay
修飾子を使ってテキストとアイコン(ハート形状)が配置されています。
キーポイント
Capsule()
を使用することで、簡単に楕円のボタンやビューを作成できることがこのコードの要点です。
この形状は、モダンなUIデザインでよく見られるものです。
また、iOS15から導入された
background(_:in:fillStyle:)
修飾子を利用すれば、Capsule()
を背景スタイルとして直接指定することも可能で、これによって同じ結果を得られます。
反省点
本題の主要な内容としては、楕円のボタンを作成することが中心であるため、VStack
を使用する必要はありませんでした。
「SwiftUI 脳内プレビュー大会」に関する詳細を共有することができて、非常に嬉しく思います! 多くの方々の参加と体験を提供できたブースイベントは、今後も続けていきたいと思っています。 今回の記事が皆様のSwiftUI学習の一助となれば幸いです。コメントやフィードバックもお待ちしております。最後まで読んでいただき、本当にありがとうございました!!
エンジニア仲間を募集しています!
DeNAでは一緒にモノづくりをするiOSアプリ開発エンジニアの仲間を募集してます!興味のあるポジションを確認してぜひエントリーしてください!!
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。