NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

アドベントカレンダー風UIを作ってわかる!SwiftUI

本記事は NRIネットコム Advent Calendar 2022 1日目の記事です。
🎁 本記事 ▶▶ 2日目 🎄

概要

SwiftUIはiOSにおける宣言的なUIフレームワークで、2019年のWWDCにて発表され今年で4年目となります。
今年も様々な変更点に加え、より使いやすくアップグレードされ、いよいよ本番環境でも「そろそろやっていこうか」と思われた方が増えたのでは?といった印象です。
そこで今回の記事はSwiftUIでアドベントカレンダーをモチーフとしたアプリの開発を想定し、UIの部分を解説しながら作る事で少しでも"SwiftUIってこんな簡単にUI作れるんだ!"と感じていただけたらと思い書きました!
是非これからやっていこうかと思っている方など、参考にしていただければと思います。
また想定読者として以下の環境は構築済みとしていますので、Xcodeのダウンロードなど環境構築に関する内容はこの記事では触れませんのでご理解お願いします。

環境

この記事は以下のバージョン環境のもと作成されたものです。
【Xcode】14.1
【iOS】16.1
【macOS】Monterey

作成するUI

今回作成するアプリのUIは以下のようなアドベントカレンダーをモチーフとして、タイトルと日付を表示させたものになります。

アドベントカレンダーとは

アドベントカレンダーをモチーフと言ってもそもそもアドベントカレンダーとはなんぞやと言う方もいるかと思います。
アドベントカレンダーとは12/1〜12/24(もしくは12/25)までの期間、日数を数えるために使用されるカレンダーです。
カレンダーとは言え、紙などに日数が書かれているものもあれば、娯楽用として家やクリスマスツリーを模様した入れ物に24個〜25個の箱があり、そこにチョコレートなどを入れて、毎日一つずつ開けて楽しむと言うのもあるそうです。

画像元

アドベントカレンダーアプリ

今回はアドベントカレンダーをモチーフとしたアプリという事で

  • アドベントカレンダーっぽいUIを入れる
  • アプリタイトルを入れる
  • 今日の日付を表示する

の上記を満たしたUIを作っていきたいと思います!

アイコンの準備

アドベントカレンダーっぽいUIにするため、今回はicooon-monoにてクリスマスのアイコンをダウンロードしてきます。
icooon-monoはアイコン素材をフリーダウンロードできるサイトです。以下引用文です。

商用利用可能なアイコン素材をフリー(無料)ダウンロードできる素材配布サイトです。 WEBデザインやDTPのほか、ビジネスシーンで活用できるアイコン素材をストックしています。 TopeconHeroesが作成したこのサイトに掲載している素材は、どなたでも使用条件に違反しない限りクレジット表記や許可なしで、 自由にご利用いただけます。

クリスマスで絞り込むとこんな感じで絞れますので、この中からお好きなアイコンをいくつかダウンロードします。

UIを実装していく

ではUIの実装をしていきたいと思います!
まずXcodeを起動して赤枠のCreate a new Xcode projectを選択します。

続いてAppを選択し、Product Nameなどを記入します。Product Nameは正直なんでもいいですが、Interfaceの部分は必ずSwiftUIを選択してください。

これでエディター画面が起動して以下のような初期画面が表示されます。

SwiftUIでは右側にプレビューが表示されます。このプレビューはコードを変換すると即時反映されますが、時々反映されないこともありますので、その場合はリロードを行うか(リロードができる状態ではプレビュー上部にボタンが表示されます)プレビューの左下にある、LiveのアイコンもしくはSelectableのアイコンを選択して切り替えていただければ再描画されるので定時対応していただけばと思います。

一番左にあるのがLiveアイコンでその右側にあるのがSelectableアイコン

とりあえず試しにText("Hello, world!")にあるHello, world!を"こんにちは、世界"と書き換えてみるとプレビューにて変更される様子が確認できます。
確認したら、先ほどダウンロードしたアイコンをAssets.xcassetsの中に入れていきます。入れ方はドラッグ&ドロップでできます。

これでアイコンが使用できるようになります。
確認の為Image(systemName: "globe")Image("icon1")と変更すると以下のようにプレビューで表示されます。

Stackを使用してアイコンを任意の位置に配置する

ではいよいよ配置していきたいと思います。
まず押さえておきたいポイントとしては、SwiftUIでUIを構成していく際に、どこに何を書いていけばいいかということです。基本的にUIを構築するにはbody内に書いていきます。
デフォルトではVStackの中にImageとTextが配置されています。このVStackはViewのコンポーネントを垂直に並べる時に使用します。他にも水平に並べることや重ねて配置することができます。

  • VStack = 垂直に配置。コード上で上から書かれている順に、上から表示される。
  • HStack = 水平に配置。 コード上で上から書かれている順に、左から表示される。
  • ZStack = 重ねて配置 。コード上で上から書かれている順に、最背面から表示される。zIndexを使用する事で表示する層を指定する事ができる。

そして次にmodifierです。TextやImageなどに対して.frame()のような形で追記することで、様々な効果を追加する事ができます。
例えば .frame(width: 50, height: 50)とするとViewのサイズを指定する事ができます。.foregroundColor(.red)とすると赤色に変更することができます。
このように必要なmodifierを追記して、実装したいUIを作っていきます。

では先ほどAssetsに入れたアイコンと各Stackを使って以下のようなUIを実装していきたいと思います。

垂直方向に7段アイコンが積まれる形となっているので大枠にVStackを定義してその中にImageを配置していきたいと思います。

VStack {
    Image("icon1")
        .resizable()
        .frame(width: 50, height: 50)
    Text("1")
}

これでicon1のImageが任意のサイズで表示されていると思います。サイズに関してはframeで指定しています。ただImageに関してはresizableを付与しないとframeで指定したサイズが反映されないのでresizableも付与しています。
続いて2段目の作成をしていきたいと思います。2段目に関してはアイコンが水平方向に2つ並んでいるのでHStackを使用していきます。
HStackは{ }内の上から順に、左から水平に表示されます。例えば先にicon2を実装し、次にicon3を実装した場合左からicon2、icon3と表示されます。

VStack(spacing: 0) {
    VStack {
        Image("icon1")
            .resizable()
            .frame(width: 50, height: 50)
        Text("1")
    }
    HStack {
        VStack {
            Image("icon2")
                .resizable()
                .frame(width: 50, height: 50)
            Text("2")
        }
        VStack {
            Image("icon3")
                .resizable()
                .frame(width: 50, height: 50)
            Text("3")
        }
    }
}

この段階で以下のようになっている事がプレビューで確認できるかと思います。

後は3段目、4段目...とVStackの中にHStackを積んでいく事でツリー状のUIができてきますが、このままだと以下のコードを頻繁に繰り返し書くことになるので、ここで一旦使い回しができるようにコンポーネント化しておきたいと思います。

VStack {
    Image("icon1")
        .resizable()
        .frame(width: 50, height: 50)
    Text("1")
}

簡単に既存のコードを別のstructにしてコンポーネント化する方法があります。コンポーネントとして切り出したいVStackなどにカーソルを当てて⌘+クリックします。その後検索にて"sub"と入力し、Extract Subviewを選択します。
選択後は自動的にExtractedView()が生成され、最下部にもstruct ExtractedView: View {...の形でExtractedViewが生成されています。これで自動的にstructでViewのコンポーネントが生成されます。

わかりやすいようにするため命名をIconViewに変更しておきます。またVStackはデフォルトでspacingに8が入ってしまうため、0と明記してImageとTextにあったスペースを無くします。
iconNameやiconNumには宣言時に定義したいため、値を保持しない形でプロパティを作成します。
またiconNumの色に関しては三項演算子を使用して、偶数の時は赤、奇数の時は緑で表示されるようにforegroundColorで定義します。
修正後は以下のような形になります。

struct IconView: View {
    let iconName: String
    let iconNum: Int
    var body: some View {
        VStack(spacing: 0) {
            VStack {
                Image(iconName)
                    .resizable()
                    .frame(width: 50, height: 50)
                Text(String(iconNum))
                    .foregroundColor(iconNum % 2 == 0 ? .green : .red)
            }
        }
    }
}

このコンポーネントはIconView(iconName: "icon1", iconNum: 1)とする事で使用できます。
ここまで作成したコードをIconViewを使ったコードへリファクタリングします。

VStack(spacing: 0) {
    IconView(iconName: "icon1", iconNum: 1)
    HStack {
        IconView(iconName: "icon2", iconNum: 2)
        IconView(iconName: "icon3", iconNum: 3)
    }
}

非常にすっきりしたコードになりました。
では残り段を実装してツリー状のUIを完成させます。

VStack(spacing: 0) {
    IconView(iconName: "icon1", iconNum: 1)
    HStack {
        IconView(iconName: "icon2", iconNum: 2)
        IconView(iconName: "icon2", iconNum: 3)
    }
    HStack {
        IconView(iconName: "icon2", iconNum: 4)
        IconView(iconName: "icon3", iconNum: 5)
        IconView(iconName: "icon2", iconNum: 6)
    }
    HStack {
        IconView(iconName: "icon4", iconNum: 7)
        IconView(iconName: "icon2", iconNum: 8)
        IconView(iconName: "icon2", iconNum: 9)
        IconView(iconName: "icon2", iconNum: 10)
    }
    HStack {
        IconView(iconName: "icon2", iconNum: 11)
        IconView(iconName: "icon3", iconNum: 12)
        IconView(iconName: "icon2", iconNum: 13)
        IconView(iconName: "icon2", iconNum: 14)
        IconView(iconName: "icon5", iconNum: 15)
    }
    HStack {
        IconView(iconName: "icon2", iconNum: 16)
        IconView(iconName: "icon6", iconNum: 17)
        IconView(iconName: "icon2", iconNum: 18)
        IconView(iconName: "icon2", iconNum: 19)
        IconView(iconName: "icon3", iconNum: 20)
        IconView(iconName: "icon2", iconNum: 21)
    }
    HStack {
        IconView(iconName: "icon2", iconNum: 22)
        IconView(iconName: "icon2", iconNum: 23)
        IconView(iconName: "icon7", iconNum: 24)
    }
}

余談ですがもし繰り返し処理でViewを生成したいのであれば是非以下の記事を読んでチャレンジしてみてください!

tech.nri-net.com

overlayで枠線を実装する

続いて上記で作成したツリー状の周りに枠線を実装していきたいと思います。
今回はoverlayを使用して実装します。このmodifierを使用する事でViewに重ねてViewを表示する事ができます。
使用方法は以下の通りで、重ねたいViewに対して.overlayを追加し、{ }内で重ねたいViewを実装します。

.overlay {
    RoundedRectangle(cornerRadius: 16)
        .stroke()
}

RoundedRectangleは角丸の四角で、cornerRadiusでRの値をしているする事ができます。また.stroke()で輪郭の線のみを表示しています。

この状態だとアイコンと外枠の線が近いのでアイコンとoverlayの間にpadding()を追加します。paddingを追加する事で余白を入れる事ができます。

VStack(spacing: 0) {
    IconView(iconName: "icon1", iconNum: 1)
    ...
}
.padding()
.overlay {
    RoundedRectangle(cornerRadius: 16)
        .stroke()
}

これで余白の外側に外枠の線を実装する事ができます。

続いてタイトルの実装をしていきたいと思います。
VStack内でアイコンの上部にTextを追加してでもいいのですが、Appleが標準フレームワークでNavigationStack内で表示できるnavigationTitleというmodifierを用意してくれているのでこちらを使用したいと思います。
NavigationStackについては

ルート ビューを表示し、ルート ビューに追加のビューを表示できるようにするビュー。 とあり、つまり画面上部に表示されるBackボタンや設定ボタン、またはタイトルなどを表示するViewで、画面遷移を行うNavigationLinkも、このNavigationStack内で実装することになります。
Apple Developer Documentation ※iOS16未満の場合はNavigatioViewを使用してください。
Apple Developer Documentation

NavigationStackは今回Viewの一番外側で実装したいため以下のように追加して実装します。

NavigationStack {
    VStack(spacing: 0) {
        IconView(iconName: "icon1", iconNum: 1)
        ...
    }
    .padding()
    .overlay {
        RoundedRectangle(cornerRadius: 16)
            .stroke()
    }
    .navigationTitle("Advent Calendar")
}

まず一番外側に実装しているVStackに対してNavigationStackを追加します。そのNavigationStack内にてnavigationTitleを追加して、表示させたいテキストを渡します。
これで画面上部にタイトルが表示されます。

DateFormatterを使用して整形した日付を表示

最後に最下部に本日の日付を表示したいと思います。
ここまで実装したならどこに実装すればいいのかはわかると思います。アイコンで作成したツリー状のViewの下に表示させたいのでツリーを表示しているVStackを更にVStackで囲い、その最下部にTextを追加します。

NavigationStack {
    Vstack {
        VStack(spacing: 0) {
            IconView(iconName: "icon1", iconNum: 1)
            ...
        }
        .padding()
        .overlay {
            RoundedRectangle(cornerRadius: 16)
                .stroke()
        }
        Text("ここに日付を書く")
    }
    .navigationTitle("Advent Calendar")
}

Text("Today: " + "2022/12/01")とし、font modifierを追加してfontの大きさを.titleとすれば日付のようなテキストを拡大して表示する事ができます。
ただ今回は本日の日付を表示したいのでDateFormatterを使用して実装していきたいと思います。
こちらは名前のままで、日付を任意のテキスト表示に変換してくれるFormatterです。
Apple Developer Documentation

日付を表示するための変数を用意します。変数はbodyの外側に定義する必要があるのでbodyの外側に@State var dateLabel = ""を追加します。
普通変数はvar...と定義しますがvarの前に@Stateが付いています。これはSwiftUIの特性で@Stateを付与した変数はメモリ管理がSwiftUIフレームワークに委譲され、値の変更が監視されるようになり、値が変更されると再描画されるようになります。
またSwiftUIにはonAppearというものがあり、onAppear内での処理はViewが表示される前に実行されます。
このonAppearを使用して、Viewが表示される時に、onAppear内で今日の日付を取得して、dateLabelに渡したいと思います。

.onAppear() {
    let date = Date()
    let dateFormatter = DateFormatter()
    dateFormatter.calendar = Calendar(identifier: .gregorian)
    dateFormatter.locale = Locale(identifier: "ja_JP")
    dateFormatter.timeZone = TimeZone(identifier:  "Asia/Tokyo")
    dateFormatter.dateStyle = .medium
    dateFormatter.dateFormat = "yyyy/MM/dd"
    dateLabel = dateFormatter.string(from: date)
}

これで表示する前にdateLabelに本日の日付を渡す事ができます。

これで完成です!

以下コードの全文です。

まとめ

今回はUIの部分のみにフォーカスして解説してみました!SwiftUIの魅力はまだまだ他の部分にもありますが、今回はの記事で少しでも”おっ!こんなに簡単にUI作れるのか”と思っていただければ自分としては大成功です!
まだまだ今後も色んな角度でSwiftUIに関する発信を続けていきたいと思います!
NRIネットコムのアドベントカレンダー1日目でしたー☆是非明日の記事も見てください☆

執筆者岡優志

iOSエンジニア
iOSを専門とし、モバイルアプリの開発を行なっています。

Twitter