blog

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

2024.09.05 イベントレポート

SwiftUI Crossword Quiz の問題紹介&解答解説

by Nana Ota

#iosdc #mobile #ios #swift #swiftui #swiftwednesday #crossword

こんにちは、エンジニアリング室の太田です!

2024年8月22日(木)から24日(土)までオンライン・オフラインのハイブリッド形式で開催された iOSDC Japan 2024 にて、DeNA は今年もゴールドスポンサーとして協賛しました! また、DeNA は iOSDC Japan 2024 の「スポンサーブース」に出展し、iOS App エンジニアたちが考えた企画、「SwiftUI Crossword Quiz」を実施しました!!

VenueSituation

SwiftUI Crossword Quizの様子

当日はたくさんの方々にご参加いただき、大盛況となりました。「SwiftUI Crossword Quiz」で集めた解答用紙は、254枚にものぼりました! このブログでは、「SwiftUI Crossword Quiz」の問題とその答え、さらに解説を行います。

*本問題の動作環境は iOS 17.5 以上となっています。

Question1の問題と解説

Question 1の問題と実行結果は以下の通りです。出題者は @ojun_9

import SwiftUI

struct Question01: View {
    var body: some View {
        ScrollView(.□□□□□□□□□□□) { // A-3
            □□□□□□(spacing: 40) { // A-4
                Text("iOSDC Japan 2024")
                    .bold()
                    .font(.title)
                Text("DeNA Booth")
                    .font(.title)
            }
            .padding(.□□□□□□□, 64) // A-1
        }
    }
}

# □□□□□□□ { // A-2
    Question01()
}

Question1_A

Question1のAnswer

Question1_P

Question1のPreview結果

解説

この問題はSwiftUIでよく利用するワードが問題になっています。 SwiftUIを利用してアプリを作ったことがある方には馴染みのあるワードだったので非常に答えやすかったのではないかと思います。

Question2の問題と解説

Question 2の問題と実行結果は以下の通りです。出題者は @yaso_san

import SwiftUI

struct Question02: View {
    static let colors: [Color] = [
        .green,
        .yellow,
        .orange,
        .red,
        .purple,
        .blue
    ]

    let stops = {
        let diff = 1.0 / Double(colors.count)
        var location: CGFloat = 0.0
        var stops: [Gradient.Stop] = []
        for color in colors {
            stops.append(.init(color: color, location: location))
            location += diff
            stops.append(.init(color: color, location: location))
        }
        return stops
    }()

    var body: some □□□□ { // A-5
        VStack(spacing: 48) {
            Image("MysteriousCharacter")
                .resizable()
                .renderingMode(.template)
                .frame(width: 320, □□□□□□: 320) // A-2
                .□□□□□□□□□□Style(LinearGradient(stops: stops, // A-1
                                                startPoint: .top,
                                                endPoint: .□□□□□□)) // A-4
            Text("What's his name ?")
                .font(.system(size: 40, weight: .□□□□)) // A-3
                .foregroundColor(.secondary)
        }
    }
}

Question2_A

Question2のAnswer

Question2_P

Question2のPreview結果

解説

日本のiOSエンジニアの間では有名なあのキャラクターを虹色に彩って表示をします。 虹色といっても7色ではなく6色となっており、敬虔なAppleファンの方ならお気付きかもしれませんが、Appleロゴと同じ配色です。 本当はSF SymbolsのAppleロゴで問題にしたかったのですが、その説明にも書かれている通り使用制限がありますので、今回はuhooi氏に代役を務めてもらいました。

キーポイント

虹色を表現するのにLinearGradientを使っています。LinearGradientは普通に使うと滑らかなグラデーションを描画できますが、Gradient.Stopのyの位置を2つ連続で同じ座標にすることで縞模様を実現しています。

反省点

難易度Easyにしてはforegroundは文字数が多く、ちょっと難しくなってしまったかもしれません。backgroundと同じ文字数で迷ったというご意見もいただいたので、例えばforeは見せてgroundだけ答えてもらうくらいでも良かった気がします。

Question3の問題と解説

Question 3の問題と実行結果は以下の通りです。出題者は 奥瀬 雄哉!

import SwiftUI

struct Question03: View {
    let cardTitle = "iOSDC Japan 2024"
    let title = "Build with SwiftUI"
    let description = "SwiftUI is the best way to build apps across Apple platforms"

    var body: some View {
        VStack(spacing: 32) {
            Rectangle()
                .fill(.pink)
                .aspectRatio(2, contentMode: .fit)
                .□□□□□□□(Text(cardTitle) // A-2
                    .font(.largeTitle)
                    .foregroundColor(.white))
                .cornerRadius(8)
                .□□□□□□(radius: 10) // A-3
                .padding(.horizontal)

            HStack {
                □□□□□(systemName: "swift") // A-1
                    .resizable()
                    .frame(width: 48, height: 48)
                    .□□□□□□□□□□□□□□□(.blue) // A-4
                    .padding()

                □□□□□□(alignment: .leading) { // A-5
                    Text(title)
                        .font(.title)
                    Text(description)
                        .font(.title3)
                }
            }
        }
    }
}

Question3_A

Question3のAnswer

Question3_P

Question3のPreview結果

解説

SwiftUIのHStackVStackは、UI構築で頻繁に使用される基本的なコンポーネントです。今回作成したクイズは、この2つと基本的なmodifierを使いながらUI構築を行っています。

反省点

DeprecatedになっているforegroundColorを解答に含めてしまった。

Question4の問題と解説

Question 4の問題と実行結果は以下の通りです。出題者は 山本 ののか!

import Charts
import SwiftUI

struct ChartEntry: Identifiable {
    var title: String
    var minY: Int
    var maxY: Int
    var averageY: Int {
        (minY + maxY) / 2
    }

    var id: String { title }
}

struct Question04: View {
    let data: [ChartEntry] = [
        .init(title: "4/1", minY: 15, maxY: 27),
        .init(title: "4/2", minY: 17, maxY: 28),
        .init(title: "4/3", minY: 18, maxY: 24),
        .init(title: "4/4", minY: 16, maxY: 25),
        .init(title: "4/5", minY: 14, maxY: 26),
        .init(title: "4/6", minY: 17, maxY: 20),
        .init(title: "4/7", minY: 17, maxY: 23)
    ]

    var body: some View {
        VStack {
            Text("Temperature")
                .font(.system(size: 20, weight: .bold))
                .□□□□□□□□□□Color(.secondary) // A-4
            □□□□ { // A-5
                Chart {
                    ForEach(data) { dataPoint in
                        AreaMark(x: .value("Date", dataPoint.title),
                                 yStart: .value("Lowest temperature", dataPoint.minY),
                                 yEnd: .value("Highest temperature", dataPoint.maxY))
                            .foregroundStyle(Color.yellow)
                            .□□□□□□□(0.3) // A-2
                        □□□□□□□□(x: .value("Date", dataPoint.title), // A-1
                                 y: .value("Average", dataPoint.averageY))
                    }
                }
                .chartYAxis {
                    AxisMarks(position: .leading) { value in
                        AxisGridLine()
                        □□□□Tick() // A-6
                        Axis□□□□□□□□□□ { // A-3
                            if let intValue = value.as(Int.self) {
                                Text("\(intValue)°C")
                            }
                        }
                    }
                }
                .frame(height: 200)
                .chartForegroundStyleScale([
                    "temperature range": .yellow, "average temperature": .blue
                ])
            }
        }
    }
}

Question4_A

Question4のAnswer

Question4_P

Question4のPreview結果

解説

Chartsを使用して、気温に関するグラフを作成しました。 気温の変動幅を表すAreaMarkと平均温度を表すLineMarkを使用しています。

キーポイント

A-3は、これ自体を知らなくても、これ自体の役割から連想できるかがポイントです。値のlabelという役割を担っていることから”ValueLabel”と入れてしまえば答えになります。 A-6は、気づきにくいですが、グラフの各数値の目盛りを表しています。これ自体を知らなくても、他のコードにも多用されている”Axis”というワードが使われるのではないか、という発想ができるかどうかがポイントです。

反省点

Chartsを使用している方が少なかったので、そもそもパラメータやコンポーネント名を知らなくて答えられないという事象が多く発生してしまいました。 他のコードから連想してみると意外と答えに辿りつけるようにしていたつもりでしたが、難易度が少し高かったかな、と反省しています。

Question5の問題と解説

Question5の問題とPreview結果は以下のとおりです。出題者は @akkiee76

import MapKit
import SwiftUI

struct Question05: View {
    @State private var isPresented: Bool = true

    var body: some View {
        VStack {
            MapView()
                .□□□□□□Ratio(1, contentMode: .fit) // A-2

            Spacer()
        }
        .sheet(isPresented: $isPresented) {
            VStack(alignment: .leading) {
                Text("Golden Gate Bridge")
                    .font(.title)

                Text("San Francisco, California")
                    .font(.subheadline)
                    .foreground□□□□□(.secondary) // A-5

                □□□□□□□() // A-6

                Text("□□□□□ Golden Gate Bridge") // A-1
                    .font(.title2)
                Text("A suspension bridge spanning the Golden Gate Strait connecting San Francisco Bay on the west coast of the United States with the Pacific Ocean.")

                HStack {
                    Spacer()
                    Image("GoldenGateBridge")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .cornerRadius(8)
                    Spacer()
                }

                Spacer()

                Text("photo by @mvdheuvel on Unsplash")
                    .font(.subheadline)
            }
            .□□□□□□□(.all, 8) // A-3
            .presentationDetents([.medium])
        }
    }
}

struct MapView: View {
    // A-4
    private let coordinate = CLLocation□□□□□□□□□□2D(latitude: 37.8199, longitude: -122.4783)

    private var region: MKCoordinateRegion {
        MKCoordinateRegion(center: coordinate,
                           span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
    }

    var body: some View {
        □□□(initialPosition: .region(region)) { // A-7
            Marker("Golden Gate Bridge", coordinate: coordinate)
        }
    }
}

Question5_A

Question5のAnswer

Question5_P

Question5のPreview結果

解説

MapKitを利用してよくある地図アプリ風の問題にしました。回答部分は基本的なModifierを出題しています。MapKitのキーワードはMapKitを使ったことがなくても回答しやすい問題となっているので、前後のコードから推測してみてください!

Question6の問題と解説

Question 6の問題と実行結果は以下の通りです。出題者は @66nylon_y

import SwiftUI

struct Question06: View {
    var body: some View {
        ZStack {
            Rectangle()
                .fill(.radialGradient(colors: [lightYellow, orange, purple, lightBlue, blue],
                                      center: .top,
                                      startRadius: 0,
                                      endRadius: 190))
            YellowToOrangeShape()
                .fill(.radialGradient(colors: [lightYellow, orange, paleOrange],
                                      center: .top,
                                      startRadius: 100,
                                      endRadius: 200))
            CurvedCenterLineShape()
                .fill(.radialGradient(colors: [lightYellow, orange, purple, lightBlue],
                                      center: .top,
                                      startRadius: 30,
                                      endRadius: 200))
            Text("□□□□□") // A-1
                .font(.largeTitle.bold())
                .□□□□□□□□□□□□□□□(.white) // A-4
        }
        .frame(height: 200)
        .mask(Rectangle().frame(height: 200))
    }
}

struct ShapeSource {
    let center: □□□□□□□ // A-5
    let radius: CGFloat

    init(rect: CGRect) {
        center = CGPoint(x: rect.midX, y: rect.minY - 15)
        radius = rect.width / 2 + 120
    }
}

struct YellowToOrangeShape: □□□□□ { // A-2
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let source = ShapeSource(rect: rect)

        let degrees: [(Double, Double)] = [(10, 20), (45, 85), (155, 165)]
        for degree □□ degrees { // A-6
            path.move(to: source.center)
            path.□□□□□□(center: source.center, // A-7
                        radius: source.radius,
                        startAngle: Angle(degrees: degree.0),
                        endAngle: Angle(degrees: degree.1),
                        clockwise: false)
        }
        return path
    }
}

struct CurvedCenterLineShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let source = ShapeSource(rect: rect)

        path.move(to: source.center)
        path.add□□□□Curve(to: CGPoint(x: source.center.x - 30, y: rect.maxY), // A-3
                          control: CGPoint(x: source.center.x - 33, y: rect.maxY * 0.8))
        path.addLine(to: CGPoint(x: source.center.x + 30, y: rect.maxY))
        path.add□□□□Curve(to: CGPoint(x: source.center.x + 5, y: rect.minY), // A-3
                          control: CGPoint(x: source.center.x + 33, y: rect.maxY * 0.75))
        return path
    }
}

Question6_A

Question6のAnswer

Question6_P

Question6のPreview結果

解説

RectangleとPathで書いたViewをZStackで重ねて、macOS Sequoiaを描きました。

キーポイント

黄色からオレンジの層はaddArcを使って円の中の指定された角度内のみ描画して塗りつぶし、中央の黄色からライトブルーのラインはaddQuadCurveを使ってベジェ曲線を追加することで少し丸みを持たせています。

よくあった間違い

addArc、addQuadCurveを知っていないと埋めるのがなかなか難しい。

反省点

最初にキーワードSequoiaにして、Sequoiaを描こう…!というアイデアからそのまま走り出してしまったので、「q」が含まれるメソッドなどを見つけるのにも難航し、「知っているか知っていないか…」という問題になってしまいました、、

Question7の問題と解説

Question 7の問題と実行結果は以下の通りです。出題者は @nekodoko315

import MapKit
import SwiftUI

struct Question07: View {
    static let tokyoTowerForCameraCoordinate: CLLocationCoordinate2D = .init(latitude: 35.660, longitude: 139.7454)
    static let tokyoTowerCoordinate: CLLocationCoordinate2D = .init(latitude: 35.658581, longitude: 139.745438)

    @State private var cameraPosition: MapCameraPosition =
        .camera(.init(.init(lookingAtCenter: Question07.tokyoTowerForCameraCoordinate,
                            fromDistance: 1000, pitch: 45,
                            heading: 0)))
    @Namespace var mapScope

    var body: some View {
        □□□(position: $cameraPosition, scope: mapScope) { // A-7
            □□□□□□□□□□(coordinate: Question07.tokyoTowerCoordinate) { // A-6
                Image(systemName: "mappin.and.ellipse") // A-2: Image
                    .foregroundStyle(.white) // A:8: foregroundStyle
                    .padding(8) // A-4: padding
                    .background(□□□□□□□□□□□□□□□□(cornerRadius: 8) // A-5
                        .fill(.blue))
            } label: {
                Text("東京タワー")
                    .bold()
                    .padding([.top], 5)
            }
        }
        .□□□□□□□(alignment: .bottomTrailing) { // A-1
            VStack { // A-3: VStack
                □□□□□□□□□□(scope: mapScope) // A-9
                    .mapControlVisibility(.visible)
            }
            .padding(.trailing, 10)
            .buttonBorderShape(.circle)
        }
        .mapScope(mapScope)
        .□□□□□□□□(.standard(elevation: .realistic,
                            pointsOfInterest: .excludingAll)) // A-3
    }
}

Question7_A

Question7のAnswer

Question7_P

Question7のPreview結果

解説

Appleのマップ上で東京タワー位置を表示し、東京タワーの経度、緯度にビューを表示してこの位置が東京タワーのことを明示しています。 マップビューの上にいくつかマップに関する機能ボタンを表示していますが、こちらについても全部Apple公式のAPIを利用しています。

キーポイント

Apple公式のMapKitを利用してマップを表示し、ボタンなどについても公式のものだけを使って表示しています。

よくあった間違い

A-6はUIKitのマップでも使われる用語でGoogleMapsではGMSMarkerと呼ばれるものです。

反省点

そもそもMapKitに触れたことがない方が多かったので、次はもっと一般的に使えるものを提案します..

Question8の問題と解説

Question 8の問題と実行結果は以下の通りです。出題者は @the_uhooi

import SwiftUI

struct Question08: View {
    @Environment(\.dismiss) private var dismiss

    private let date: Date = .□□□ // A-2

    var body: some View {
        Form {
            LabeledContent("現在日時") {
                Text("\(date, format: .dateTime)")
            }

            LabeledContent("アイコン") {
                Image(systemName: "tennis.racket")
                    .resizable()
                    .□□□□□□□□□□□() // A-4
                    .foreground□□□□□(.green) // A-1
            }

            LabeledContent("角丸四角形") {
                Color.blue
                    .□□□□□□□□□(□□□□□□□□□□□□□□□□(cornerRadius: 8)) // A-3, A-5, 6
                    .padding()
            }

            LabeledContent("ヒント") {
                Text("SwiftLintのオレオレカスタムルール一覧 by uhooi | トーク | iOSDC Japan 2024 #iosdc - fortee.jp")
            }
        }
        .toolbar {
            ToolbarItem(placement: .□□□□□□□□□□□□□□) { // A-7
                Button("閉じる", systemImage: "xmark") {
                    dismiss()
                }
            }
        }
    }
}

Question8_A

Question8のAnswer

Question8_P

Question8のPreview結果

解説

ウホーイが iOSDC Japan 2024 に寄稿したパンフレット記事を読めばわかる問題になっています。 非推奨になった API を使わず、新しい API を覚えてもらうために作成しました。

Qiita にもほぼ同様の記事を投稿しているので、よかったら読んでみてください。 https://qiita.com/uhooi/items/9deb56416aaa81709f69

よくあった間違い

  • A-1: 私がブースに立っているときはなかったが、 foregroundColor() のミスリードを狙った。 foregroundColor() は非推奨になり、iOS 15.0+ では foregroundStyle() を使う

反省点

  • A-4: ノーヒントだったので、 scaled□□□□□() など一部を露出すべきだった
  • A-7: .○○Action も考えられるので、一意に決まりやすいワードを選ぶべきだったかもしれない

Question9の問題と解説

Question 9の問題と実行結果は以下の通りです。出題者は @ojun_9

import Foundation
import UserNotifications

// Apple Swift version 6.0 (swiftlang-6.0.0.5.15 clang-1600.0.22.6)

// ちょっとStrict Concurrencyの対応が間に合わない😭
□□□□□□□□□□□□□□□ // A-4
func someFunction() {}

// 対応間に合った😭
func anotherFunction(
  completion: □□□□□□□□□ □□□□□□□□□ () -> Void  // A-5, A-6
) {
  // ...
}

protocol P: Sendable {
  func f()
}

// テストをするためにプロパティを持たせたい
// などの理由で どうしても class で実装したい
final class C1: P {
  □□□□□□□□□□□□□□□□□□□ var array = [1, 2, 3] // A-1

  func f() { /* ... */ }
}

// ビルドが通るように...
extension UNNotification: □□□□□□□□□□□□ □□□□□□□□□□ Sendable {} // A-7, A-2, A-3

Question9_A

Question9のAnswer

解説

この問題はStrict Concurrencyチェックの対応を行う際に、一時的な対応として実装する可能性のあるキーワードを当てる問題になっています。 iOSDC Japan 2024のセッション 座談会 「Strict ConcurrencyとSwift 6が開く新時代: 私たちはどう生きるか?」 を見た方やZennの記事である 事例別!Strict Concurrency対応方法 を読んだ方はスラスラと解けたのではないでしょうか。

キーポイント

キーポイントは、それぞれのキーワードを知っているか・それぞれのキーワードを実際に利用したことがあるかどうかになります。また、キーワードを実際に使ったことがあっても A-1 のような長いキーワードを覚えているかどうかも問題を解く鍵となります。

Question10の問題と解説

Question 10の問題と実行結果は以下の通りです。出題者は @akkiee76

import Charts
import SwiftUI

struct Question10: View {
    let columns = [GridItem(.flexible()), GridItem(.flexible())]

    var body: some View {
        Text("Donuts Menu")
            .font(.largeTitle)
            .padding()

        LazyVGrid(columns: columns, □□□□□□□: 10) { // A-3
            ForEach(Donuts.allCases, id: \.self) { donut in
                VStack {
                    □□□□□(donut.fravors, id: \.name) { fravor in // A-5
                        □□□□□□Mark(angle: .value("value", donut.fravorRatio), // A-4
                                   innerRadius: .ratio(0.4), // A-7: innerRadius
                                   angularInset: 2)
                            .□□□□□□Radius(12) // A-6
                            .foregroundStyle(by: .value("fravor", fravor.name))
                            .opacity(0.7)
                    }
                    .chartForegroundStyleScale(donut.scales)
                    .chartLegend(.□□□□□□) // A-8
                    .rotationEffect(□□□□□(degrees: 30)) // A-1
                    .frame(height: 100)
                    .padding(.top, 10) 

                    Text(donut.taste)
                        .multilineTextAlignment(.center)
                        .□□□□□□□(.vertical, 8) // A-2
                }
            }
        }
        .padding(.horizontal)
    }
}

// case planeAndChocolate以降は省略しています。 'case planeAndChocolate' and after are omitted.
enum Donuts: CaseIterable {
    case plane
    case planeAndChocolate
    
    var taste: String {
        switch self {
        case .plane: "Plane & Sugar"
        case .planeAndChocolate: "Plane & Chocolate"
        }
    }
    
    var fravors: [Fravor] {
        switch self {
        case .plane:
            [.init("sugar", .init(uiColor: .brown).opacity(0.7)), .init("plane", .init(uiColor: .brown))]
        case .planeAndChocolate:
            [.init("chocolate", .init(uiColor: .black)), .init("plane", .init(uiColor: .brown))]
        }
    }
    
    var scales: KeyValuePairs<String, Color> {
        switch self {
        case .matcha, .lemon:
            [fravors[0].name: fravors[0].color]
        default:
            [fravors[0].name: fravors[0].color, fravors[1].name: fravors[1].color]
        }
    }
    
    var fravorRatio: Int {
        100 / fravors.count
    }
}

Question10_A

Question10のAnswer

Question10_P

Question10のPreview結果

解説

Chartsを利用したデザインで、ドーナッツメニューをイメージして作成した問題です。

キーポイント

WWDC23で発表されたpie chartsの機能を使っています。ご興味があれば、ぜひ以下のセッションもチェックしてみてください。 https://developer.apple.com/videos/play/wwdc2023/10037/

反省点

最終日で最後の問題でLTの時間と被っていたので、チャレンジできた人が少なかったかも。Chartsの問題は難易度が高いので、Chartsを知らない人でも回答しやすい問題にした方が良かったかも。


「SwiftUI Crossword Quiz」について詳細をお伝えできることを、心から嬉しく思います!昨年の「SwiftUI Preview Challenge」に続き、たくさんの方に参加してもらえて、ユニークな体験を提供できるブースイベントをこれからも続けていきたいと思ってます。この記事がみなさまの SwiftUI 学習の助けになればうれしいです!コメントやフィードバックもお待ちしております!最後まで読んでいただき、本当にありがとうございました!!

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

recruit

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