はじめに
この記事は[SwiftUI入門] 3.タスクキルしても情報を保存するようにしよう!の続きになります。こちらを読んでない人は先にこちらを読むことをおすすめします。
それでは、今回もやっていきましょー
つくるもの
今回つくるものは前回までつくってきたContentViewは使わず新しいContentViewを使います。
UIの配置はこんな感じです!
そして、「りんご」か「Apple」を押したときは
りんごの画像が出てきて、
「ぶどう」か「Grape」を押したときは
ぶどうの画像が出てきます。
つまり、今回は画面遷移をやっていきましょう!
これですごくスマホのアプリっぽくなってきます!
TODO
- ContentViewを新規作成&設定の変更
- UIの設置
- 素材の用意
- 新しいViewの作成
- NavigationStackとNavigationLinkの適応
- データを構造体で管理する
-
.navigationDestination
を使う
開発
それでは今回もTODOに沿って開発をしていきましょう!
ContentViewを新規作成&設定の変更
今まで使っていたContentViewは置いておいて、新しいContentViewを作成しましょう。
ContentView.swift
と同じ階層でContentView2.swift
を作成してください。そのときテンプレートは「SwiftUI View」を指定してください。
これでTextで「Hello, world!」と書かれたViewが生成されていると思います。
そして、「プロジェクト名App.swift」というファイルがあると思うのですが、その中を見てみましょう。
import SwiftUI
@main
struct rinngo_practiceApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
このようになっていると思いますので、このContentView
を先ほど作成したContentView2
に変更してください。
import SwiftUI
@main
struct rinngo_practiceApp: App {
var body: some Scene {
WindowGroup {
ContentView2()
}
}
}
この状態で実行してみてください。すると、新しいContentView2の方が表示されると思います。
UIの設置
上の画像のようにUIを設置していきます。
これはList
とSection
を使って作成します。
ListとSectionは下のように使うことができます。
List {
Section(header: Text("タイトル1")) {
Text("Text1")
Text("Text2")
}
Section(header: Text("タイトル2")) {
Text("Text1")
Text("Text2")
}
}
こんな感じで、List
の中にSection
を入れ、そのSection
の中にTextを入れることといいです。
それでは、ソースコードはこうなりますのでこれを参考にしてみてください。
import SwiftUI
struct ContentView2: View {
var body: some View {
List {
Section(header: Text("日本語")) {
Text("りんご")
Text("ぶどう")
}
Section(header: Text("English")) {
Text("Apple")
Text("Grape")
}
}
}
}
#Preview {
ContentView2()
}
素材の用意
次に、「Preview Content」の中の「Preview Assets.xcassets」を開いてください。
このような画面になると思います。
それでは適当にりんごの画像でも持ってきましょう。いらすとやなんかでもいいと思います。
それではダウンロードしてきて、
上の画像の矢印の部分にドラッグ&ドロップしてください。そして、名前を「Apple」にしておきましょうか。
次に、ぶどうの画像を持ってきて、同じようにGrapeにしましょう。
こうなると思います。
次は、先ほどと同じように「New Image Set」で「Grape」を作成して、ぶどうの画像を持ってきて、同じように貼り付けてください。
これで素材の用意ができました。
新しいViewの作成
まずは新しいフォルダ「ImageView」を作成して、その中に新しいファイルを「SwiftUI View」のテンプレートを使用してつくりましょう。
名前は「AppleImageView」と、「GrapeImageView」にしてください。
まずは、AppleImageViewを開いてください。
ここには画像だけを表示します。
画像の表示には、Image
を使います。
Image("Image")
このように使います。
このImageの引数の中には「Preview Assets.xcassets」で先ほど生成した画像の名前を指定するとよいです。
よって
Image("Apple")
にすればよいので
import SwiftUI
struct AppleImageView: View {
var body: some View {
Image("Apple")
}
}
#Preview {
AppleImageView()
}
こんな感じになります。ちょっと見切れてますね。まあいっか...
同じように、GrapeImageViewにも同じようにしましょう。
import SwiftUI
struct GrapeImageView: View {
var body: some View {
Image("Grape")
}
}
#Preview {
GrapeImageView()
}
こんな感じですね。ぎりぎり見切れませんでしたね。
これで遷移先のViewを作成することができました!
NavigationStackとNavigationLinkの適応
次は画面遷移をつくっていきましょう!
画面遷移には、NavigationStack
とNavigationLink
を使います。
NavigationStack | NavigationLink |
---|---|
画面遷移の土台 | 画面遷移のボタン |
本当にざっくりとした説明ですが、この2つはこのような働きがあります。
使い方は
NavigationStack {
NavigationLink(destination: ContentView()) {
Text("Link")
}
}
このように、NavigationLink
をNaviationStack
で囲ってやることで使うことができます。
NavigationStackが画面遷移をする土台のようなもので、このViewの中で画面遷移します。
画面遷移する先のViewはNavigationLink
のdestination
で指定することができます。
試しに、下のようなソースコードを書いてみましょう。
import SwiftUI
struct ContentView2: View {
var body: some View {
List {
Section(header: Text("日本語")) {
Text("りんご")
Text("ぶどう")
}
Section(header: Text("English")) {
Text("Apple")
Text("Grape")
}
}
NavigationStack {
NavigationLink(destination: ContentView()) {
Text("Link")
}
}
}
}
#Preview {
ContentView2()
}
そうすると、画面の下の方にLinkというボタンが生成され、それをタップするとContentViewでつくったUIが表示されます。
タップすると
また、このNavigationStack
には別の使い方ができるのですが、今はまだできないので後でその機能を試してみましょう。
それでは、NavigationStack
とNavigationLink
を設置していきます。
それでは先ほど書いたNavigationStack
とNavigationLink
を削除してください。
import SwiftUI
struct ContentView2: View {
var body: some View {
List {
Section(header: Text("日本語")) {
Text("りんご")
Text("ぶどう")
}
Section(header: Text("English")) {
Text("Apple")
Text("Grape")
}
}
}
}
#Preview {
ContentView2()
}
こうですね。
では、ここにNavigationStack
とNavigationLink
を設定していくのですが、基本的にNavigationStack
はList
を囲うように設置し、NavigationList
はボタンの役割をするのでText
を囲うようにしてください。
NavigationStack
はこんな感じで大丈夫です。
import SwiftUI
struct ContentView2: View {
var body: some View {
NavigationStack {
List {
Section(header: Text("日本語")) {
Text("りんご")
Text("ぶどう")
}
Section(header: Text("English")) {
Text("Apple")
Text("Grape")
}
}
}
}
}
#Preview {
ContentView2()
}
次にNavigationLink
です。
import SwiftUI
struct ContentView2: View {
var body: some View {
NavigationStack {
List {
Section(header: Text("日本語")) {
NavigationLink(destination: AppleImageView()) {
Text("りんご")
}
NavigationLink(destination: GrapeImageView()) {
Text("ぶどう")
}
}
Section(header: Text("English")) {
NavigationLink(destination: AppleImageView()) {
Text("Apple")
}
NavigationLink(destination: GrapeImageView()) {
Text("Grape")
}
}
}
}
}
}
#Preview {
ContentView2()
}
こんな感じで、ボタンにしたいところに全部つけていきます。
というわけで、これで一旦は実装し終えましたね!
ですが、ちょっとこのソースコードって...無駄が多い気がしませんか?
Session
やText
、NavigationLink
など何回も書かなければいけなく、効率が悪そうです。
実際、ここに新たにデータをつけようとするとどんどんソースコードが肥大化していきます。
ということで、次はその解決策について考えていきましょう。
データを構造体で管理する
データが増えるとその分コードが肥大化し、さらに作業量も増える...という問題点を解決するにはどうすればいいでしょうか?
それは、データを管理する構造体をつくり、それを一気に読み込んでUIとして反映させるというものです!
ではつくっていきましょうか。
まずは、データの構造体をつくっていきましょう。
先に、NavigationLink
ごとの構造体をつくりましょう。
格納するデータは
Item
- Textの文字
- なんの果物か
になります。
この構造体をつくっていきましょう。
struct Item {
var text: String
var fruit: String
}
では、次にSection
ごとの構造体をつくりましょう。
Section
が持つデータは
ItemSection
- 言語
- Item
これを構造体にしてみましょうか。
struct SectionItem {
var lang: String
var items: [Item]
}
こんなもんですかね。
では、次にこの構造体のデータをつくりましょう。
var sectionItems = [
SectionItem(lang: "日本語", items: [
Item(text: "りんご", fruit: "Apple"),
Item(text: "ぶどう", fruit: "Grape")
]),
SectionItem(lang: "English", items: [
Item(text: "Apple", fruit: "Apple"),
Item(text: "Grape", fruit: "Grape")
])
]
こんなもんでいいですかね。
これで表示するデータを全て格納することができました。
とりあえずこれを書き込んでいきましょう。
import SwiftUI
struct Item {
var text: String
var fruit: String
}
struct SectionItem {
var lang: String
var items: [Item]
}
struct ContentView2: View {
var sectionItems = [
SectionItem(lang: "日本語", items: [
Item(text: "りんご", fruit: "Apple"),
Item(text: "ぶどう", fruit: "Grape")
]),
SectionItem(lang: "English", items: [
Item(text: "Apple", fruit: "Apple"),
Item(text: "Grape", fruit: "Grape")
])
]
var body: some View {
NavigationStack {
List {
Section(header: Text("日本語")) {
NavigationLink(destination: AppleImageView()) {
Text("りんご")
}
NavigationLink(destination: GrapeImageView()) {
Text("ぶどう")
}
}
Section(header: Text("English")) {
NavigationLink(destination: AppleImageView()) {
Text("Apple")
}
NavigationLink(destination: GrapeImageView()) {
Text("Grape")
}
}
}
}
}
}
#Preview {
ContentView2()
}
それでは、本格的にデータを読み込むロジックを組みましょうか。
Item
もSectionItem
も構造体の配列なので、ForEach
を使うとよさそうですね。
まず1つずつ書いていきましょう。Section
ごとのForEach
を書いていきます。
NavigationStack {
List {
ForEach(sectionItems) { sectionItem in
Section(header: Text(sectionItem.lang)) {
}
}
}
}
これでSection
ごとのForEach
が書けたので、次はSection
の中を書きましょう。
これも構造体の配列なので、ForEach
で大丈夫ですね。
NavigationStack {
List {
ForEach(sectionItems) { sectionItem in
Section(header: Text(sectionItem.lang)) {
ForEach(sectionItem.items) { item in
if item.fruit == "Apple" {
NavigationLink(destination: AppleImageView()) {
Text(item.text)
}
} else {
NavigationLink(destination: GrapeImageView()) {
Text(item.text)
}
}
}
}
}
}
}
こんな感じですね。
item.fruit == "Apple" {
NavigationLink(destination: AppleImageView()) {
Text(item.text)
}
} else {
NavigationLink(destination: GrapeImageView()) {
Text(item.text)
}
}
ここ少し気になりますよね。NavigationLink
を2回も書いているので、ここも簡単にできそうですよね。
そこで、
let view: View = (item.fruit == "Apple") ? AppleImageView() : GrapeImageView()
NavigationLink(destination: view) {
Text(item.text)
}
にしたら簡単に書けそうですが、
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
このようなエラーが出ます。
これは、型推論に時間がかかりすぎて起こるエラーです。
このエラーの原因は
let view: View = (item.fruit == "Apple") ? AppleImageView() : GrapeImageView()
でViewを生成するのですが、ここでview
にはAppleImageView
型が入るのかGrapeImageView
型が入るのかの認識が複雑になってしまうかららしいです。
たしかにview
にはView
プロトコルに準拠した型が入るということは分かりますが、それがAppleImageView
なのかGrapeImageView
なのかはわからないですよね。(たぶんそういうこと)
この解決策として、変数view
の型推論がうまくいかないのだから、view
などの変数を使わないようにすればいいので、上のようなコードにすればうまくいきます。
現状で完成しているソースコードも書いておきますね。
import SwiftUI
struct Item: Identifiable {
var id = UUID()
var text: String
var fruit: String
}
struct SectionItem: Identifiable {
var id = UUID()
var lang: String
var items: [Item]
}
struct ContentView2: View {
var sectionItems = [
SectionItem(lang: "日本語", items: [
Item(text: "りんご", fruit: "Apple"),
Item(text: "ぶどう", fruit: "Grape")
]),
SectionItem(lang: "English", items: [
Item(text: "Apple", fruit: "Apple"),
Item(text: "Grape", fruit: "Grape")
])
]
var body: some View {
NavigationStack {
List {
ForEach(sectionItems) { sectionItem in
Section(header: Text(sectionItem.lang)) {
ForEach(sectionItem.items) { item in
if item.fruit == "Apple" {
NavigationLink(destination: AppleImageView()) {
Text(item.text)
}
} else {
NavigationLink(destination: GrapeImageView()) {
Text(item.text)
}
}
}
}
}
}
}
}
}
#Preview {
ContentView2()
}
.navigationDestination
を使う
あとちょっとです!!がんばりましょう!!
さっきのコードでもいいのですが、NavigationLink
が2回も出ていて無駄な感じがしますよね。
また、もしこのまま果物の種類が増えるとif文の分岐も増えていきます。
そこで、.navigationDestination
というものを使いましょう。
まず、なぜ先ほどのコードではNavigationLink
が2回も出ていたのかというと
「定義していたViewが2つもあり、条件によってその2つのViewを使い分けなければいけなかったから」
ですよね。
つまり、あらかじめView
を定義するのではなく、データに合わせてView
を定義できるようにすればいいのではないでしょうか。
これを実現するのが、.navigationDestination
になるます。
ではこれの使い方を解説していきます。
まず、NavigationLink
は
NavigationLink(destination: AppleImageView())
のように使っていましたが、これを
NavigationLink(value: item.fruit)
にしましょう。
この機能は先ほども説明したとおりデータに合わせてView
を定義するものですので、どのようなView
にするのかに使うパラメータを渡しましょう。
今回は、果物の種類を表すitem.fruit
でいいですね。
次に、.navigationDestination
の使い方です。
これは、遷移した後の画面のView
を定義することができるトレイリングクロージャです。その際、引数として先ほどのパラメータも使うことができます。
また、引数として、そのパラメータの型
を渡す必要があります。この場合、item.fruit
はString
型なので、String.self
にしてください。これは、String
型を値として使うものです。String
だけではだめです。
.navigationDestination(for: String.self) { image in
Image(image)
}
これで遷移先の画面のView
をつくることができました。
それではこれを実装しましょう。
NavigationStack
に対応させます。
import SwiftUI
struct Item: Identifiable {
var id = UUID()
var text: String
var fruit: String
}
struct SectionItem: Identifiable {
var id = UUID()
var lang: String
var items: [Item]
}
struct ContentView2: View {
var sectionItems = [
SectionItem(lang: "日本語", items: [
Item(text: "りんご", fruit: "Apple"),
Item(text: "ぶどう", fruit: "Grape")
]),
SectionItem(lang: "English", items: [
Item(text: "Apple", fruit: "Apple"),
Item(text: "Grape", fruit: "Grape")
])
]
var body: some View {
NavigationStack {
List {
ForEach(sectionItems) { sectionItem in
Section(header: Text(sectionItem.lang)) {
ForEach(sectionItem.items) { item in
NavigationLink(value: item.fruit) {
Text(item.text)
}
}
}
}
}
.navigationDestination(for: String.self) {image in
Image(image)
}
}
}
}
#Preview {
ContentView2()
}
こんな実装で大丈夫です!
これで、ようやく完成です!
さいごに
今回もお疲れ様でしたー!
本当に長くて大変でしたね...
スマホ特有の機能を扱う章で、かつ私自身が少し詰まってしまったところだったので結構詳しく書いてみました。
(ですけど...最後のほうはちょっと適当になっちゃったかも...)
もしかしたら分かりづらいところもあるかもしれないので、そのときはもっと勉強してみてください!
それでは次もがんばりましょー!