blog

DeNAのエンジニアが考えていることや、担当しているサービスについて情報発信しています

2023.09.07 イベントレポート

SwiftUI 脳内プレビュー大会の問題紹介&解答解説

by ojun

#iosdc #mobile #ios #swift #swiftui #swiftwednesday #brain-previews

こんにちは、23卒の ojun です!

2023年9月1日(金)から3日(日)までオンライン・オフラインのハイブリッド形式で開催された iOSDC Japan 2023 にて、DeNA はゴールドスポンサーとして協賛しました! また、DeNA は iOSDC Japan 2023 の「スポンサーブース」に出展し、iOS App エンジニアたちが考えた企画である「SwiftUI 脳内プレビュー大会」を実施しました!!

VenueSituation

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()
    }
}

Question1

Question1のPreview結果

解説

この問題は、右上・右下・左上・左下・中央に配置されている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")
    }
}

Question2

Question2のPreview結果

解説

この問題では、FormSectionを中心に構築された標準的な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)
            }
        }
    }
}

Question4

Question4のPreview結果

解説

この問題では、基本的な図形であるRectangle()Circle()を用いたプレビューが課題となっています。 これらの図形間にSpacerが挿入されており、その結果、図形が均等な間隔で配置されるような表示になっています。

キーポイント

注目すべきポイントとして、Circle().border(.black)を適用する場合の動作があります。 .border(.black)を用いると、円でなくframeの周りに枠線を引くためこのような表示になります。

よくあった間違い

多くの人々が、Circle().border(.black)を適用した際に、円の外周を丸く囲むように書いてしまう間違いが多かったように感じました。

反省点

実際の問題の目的が図形のプレビューだけであるのなら、FormSectionのような追加のコンポーネントを利用する必要はなかったのではないかと感じています。

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)
                    )
            }
        }
    }
}

Question5

Question5のPreview結果

解説

この問題は、特定の図形を描画する問題です。 前述のQuestion4との主要な違いは、図形の描画プロセスにおいて、使用される各種属性やモディファイアの影響を理解し、その情報を基に描画される図形の位置や大きさを正確に推定する必要があることです。 特に、framepadding、そして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()   
    }
}

Question6

Question6のPreview結果

解説

この問題では、Appleが提供する9種類のSF Symbolsを正確にプレビューする能力が求められます。これにより、ユーザーのApple製品やSF Symbolsへの知識と愛情が試されます。

よくあった間違い

特にellipsischevron.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()   
    }
}

Question7

Question7のPreview結果

解説

この問題はQuestion6と同様にSF Symbolsをプレビューする問題です。 この問題で登場するSF SymbolsはiOS 16.1+で利用できます。

キーポイント

iphone.gen3のアイコンでは、dynamic islandが表現されています。この特定のデザイン要素は、このモデルのiPhoneの特徴を強調している部分であり、シンボルの中で特に注目すべき点です。 iphone.gen3dynamic islandが表現されているアイコンであることに気づくと、iphone.gen1iphone.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)
                )
            }
        }
    }
}

Question8

Question8のPreview結果

解説

この問題は、円環上に配置された数字を正確にプレビューする問題です。 円周上の数字の配置や回転に関する高校数学の知識が試されています。

キーポイント

注意するべきは、最初に表示される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]
        }
    }
}

Question9

Question9のPreview結果

解説

この問題では、CustomAlignedTextという独自のTextが内部定義されており、表示するテキストとそのオフセットを持っています。 このカスタムテキストビューは、与えられたoffseteをもとに、Textが左方向にどれだけずらされるかを管理します。このoffsetは、.alignmentGuideモディファイアを使って実現されるのが特徴です。

キーポイント

Alignment GuideはSwiftUISの真左側に配置されており、これを基点に残りの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)
                    )
            }
        }
    }
}

Question10

Question10のPreview結果

解説

この問題は、特定の形状(楕円)を持つボタンの描画に関するものです。 SwiftUIのButtonコンポーネントを使用しており、具体的にはButtonの init(action:label:) を利用しています。 このボタンの見た目は、 Capsule() を用いることで楕円形状となっており、ボタンの上には.overlay修飾子を使ってテキストとアイコン(ハート形状)が配置されています。

キーポイント

Capsule()を使用することで、簡単に楕円のボタンやビューを作成できることがこのコードの要点です。 この形状は、モダンなUIデザインでよく見られるものです。 また、iOS15から導入された background(_:in:fillStyle:) 修飾子を利用すれば、Capsule()を背景スタイルとして直接指定することも可能で、これによって同じ結果を得られます。

反省点

本題の主要な内容としては、楕円のボタンを作成することが中心であるため、VStackを使用する必要はありませんでした。


「SwiftUI 脳内プレビュー大会」に関する詳細を共有することができて、非常に嬉しく思います! 多くの方々の参加と体験を提供できたブースイベントは、今後も続けていきたいと思っています。 今回の記事が皆様のSwiftUI学習の一助となれば幸いです。コメントやフィードバックもお待ちしております。最後まで読んでいただき、本当にありがとうございました!!

エンジニア仲間を募集しています!

DeNAでは一緒にモノづくりをするiOSアプリ開発エンジニアの仲間を募集してます!興味のあるポジションを確認してぜひエントリーしてください!!

最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。

recruit

DeNAでは、失敗を恐れず常に挑戦し続けるエンジニアを募集しています。