Swift Playgrounds「プロフィール」を解説する

# この投稿はなに？
iPadとMac向けアプリの「Swift Playgrounds4」に用意されている「プロフィール」Appプロジェクトを学ぶための解説です。

新しくなったSwift Playgrounds4については、[以前の投稿](https://qiita.com/imchino/items/01d46c27c6a400aad1de)で解説しています。

# 画面の構成
下の画像は、Appプロジェクト「プロフィール」を開いた直後の画面です。
![IMG_0498.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/2d720ba5-d805-a972-78eb-deee60eb482b.jpeg)

左側のエディタエリアにはコードとガイド、右側はライブプレビューが表示されています。
ライブプレビューは「実際のアプリがどのように見えるか」を示しています。コードを変更するたびに、その影響が自動的に反映されます。

## ナビゲーターエリア
Swift Playgroundsのナビゲーターエリアでは、プロジェクトに含まれるファイルやリソースを確認できます。
![IMG_0499.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/a5e195a9-9a8d-95d2-1ef5-6d548c8af35b.jpeg)

上の画像からは、7つのswiftファイルと1つの画像ファイルがあることを確認できます。

## ガイド全般
画面右側ではライブプレビューの代わりに、ガイド全般を確認することもできます。
![IMG_0502.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/e1304e24-00ac-af18-9762-f3e1ae93d3c7.jpeg)

エディタエリアのガイドを閉じてしまった場合には、このガイド全般から再開できます。学習を開始する前に、目を通しておくとよいでしょう。

# 「プロフィール」アプリのプロジェクトについて
実際のアプリ名はAboutMeであり、プロジェクト名もAboutMeです。「プロフィール」というのは、日本語ローカライズの都合でそのように表示されているわけです。

画面左上にある実行ボタンをクリックすれば、アプリをビルドして実行することが出来ます。さっそく実行して、どのようなアプリなのか試してください。このアプリが、4つのタブ（Home, Story, Favorites, Fun Facts）から構成される画面を持ち、それぞれが「ユーザー自身を紹介する内容」になっていることがわかるでしょう。

アプリの使い方がわかったら、アプリの実行を停止してプロジェクトに戻りましょう。ツールバーのswiftアイコンをクリックして、「プロジェクトを表示」を選択してください。

# リソースのファイル
以下、プロジェクトに含まれるリソースを１つずつ確認していきます。

## ContentView.swift
アプリを起動した直後に表示される画面を定義しているファイルです。
実際のSwiftUIアプリ開発においては、デフォルトで作成されます。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {

    var body: some View {
        TabView {
            HomeView()
                .tabItem {
                    Label("Home", systemImage: "person")
                }

            StoryView()
                .tabItem {
                    Label("Story", systemImage: "book")
                }
            
            FavoritesView()
                .tabItem {
                    Label("Favorites", systemImage: "star")
                }
            
            FunFactsView()
                .tabItem {
                    Label("Fun Facts", systemImage: "hand.thumbsup")
                }
        }        
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

‘TabView’のブロック内では先頭に’HomeView’が記述されています。従って、アプリを起動した直後はHomeタブが表示されることになります。

## Data.swift
アプリ全体にわたってアクセスされるデータを定義したファイルです。

```swift:Data.swift
import Foundation
import SwiftUI

struct Info {
    let image: String
    let name: String
    let story: String
    let hobbies: [String]
    let foods: [String]
    let colors: [Color]
    let funFacts: [String]
}
let information = Info(
    image: "Placeholder",
    name: "My Name",
    story: "A story can be about anything you can dream up. There are no right answers, there is no one else.\n\nNeed some inspiration?\n• 🐶🐱🛶️🎭🎤🎧🎸\n• 🏄‍♀️🚵‍♀️🚴‍♀️⛵️🥾🏂⛷📚\n• ✍️🥖☕️🏋️‍♂️🚲🧗‍♀️ ",
    hobbies: ["bicycle", "ticket.fill", "book.fill"],
    foods: ["🥐", "🌮", "🍣"],
    colors: [Color.blue, Color.purple, Color.pink],
    funFacts: [
        "The femur is the longest and largest bone in the human body.",
        "The moon is 238,900 miles away.",
        "Prince’s last name was Nelson.",
        "503 new species were discovered in 2020.",
        "Ice is 9 percent less dense than liquid water.",
        "You can spell every number up to 1,000 without using the letter A.\n\n...one, two, three, four...ninety-nine...nine hundred ninety-nine 🧐",
        "A collection of hippos is called a bloat.",
        "White sand beaches are made of parrotfish poop.",
    ]
)
```
`Info`構造体は、自己紹介したいユーザーの情報を示すデータ型です。プロフィール写真となる画像のファイル名、自身の名前、趣味や好きな食べ物などの情報を保持します。

定義した`Info`構造体をコード後半で、定数`information`としてインスタンス化している点に注目してください。

## HomeView.swift
自己紹介のために、自身の写真と名前を表示する画面を定義したファイルです。

```swift:HomeView.swift
import SwiftUI

struct HomeView: View {

    var body: some View {
        VStack {
            Text("All About")
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding()

            Image(information.image)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .cornerRadius(10)
                .padding(40)

            Text(information.name)
                .font(.title)
        }
    }
    
}

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        HomeView()
    }
}
```

以下のような画面で、プロフィール写真と名前を表示します。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/5412647d-b05f-f760-16e5-d2750c197890.jpeg">

## StoryView.swift
アプリのStoryタブをタップしたときに表示される画面を定義したファイルです。

```swift:StoryView.swift
import SwiftUI

struct StoryView: View {

    var body: some View {
        VStack {
            Text("My Story")
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding()
            
            ScrollView {
                Text(information.story)
                    .font(.body)
                    .padding()
            }
        }
        .padding([.top, .bottom], 50)
    }
}

struct StoryView_Previews: PreviewProvider {
    static var previews: some View {
        StoryView()
    }
}
```

以下のような画面で、将来の夢や目標、なんでも自由に語ることが出来ます。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/7451b290-2c27-0223-c639-215a5caa4925.jpeg">

## FavoritesView.swift
アプリのFavoritesタブをタップしたときに表示される画面を定義したファイルです。

```swift:FavoritesView.swift 
import SwiftUI

struct FavoritesView: View {

    var body: some View {
        VStack {
            Text("Favorites")
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding(.bottom, 40)
            
            Text("Hobbies")
                .font(.title2)
            
            HStack {
                ForEach(information.hobbies, id: \.self) { hobby in
                    Image(systemName: hobby)
                        .resizable()
                        .frame(maxWidth: 80, maxHeight: 60)
                }
                .padding()
            }
            .padding()

            Text("Foods")
                .font(.title2)
            
            HStack(spacing: 60) {
                ForEach(information.foods, id: \.self) { food in
                    Text(food)
                        .font(.system(size: 48))
                }
            }
            .padding()

            Text("Favorite Colors")
                .font(.title2)

            HStack(spacing: 30) {
                ForEach(information.colors, id: \.self) { color in
                    color
                        .frame(width: 70, height: 70)
                        .cornerRadius(10)
                }
            }
            .padding()
        }
    }
}

struct FavoritesView_Previews: PreviewProvider {
    static var previews: some View {
        FavoritesView()
    }
}
```

以下のような画面で、趣味や好きな食べ物、色を表示します。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/6b3fd4c8-31c7-681f-6116-7d096cc94daa.jpeg">

## FunFactsView.swift
アプリのFun Fuctsタブをタップしたときに表示される画面を定義したファイルです。

```swift:FunFactsView.swift
import SwiftUI

struct FunFactsView: View {
    @State private var funFact = ""

    var body: some View {
        VStack {
            Text("Fun Facts")
                .font(.largeTitle)
                .fontWeight(.bold)
                        
            Text(funFact)
                .padding()
                .font(.title)
                .frame(minHeight: 400)

            Button("Show Random Fact") {
                funFact = information.funFacts.randomElement()!
            }
        }
        .padding()
    }
}

struct FunFactsView_Previews: PreviewProvider {
    static var previews: some View {
        FunFactsView()
    }
}
```

以下のような画面で、ボタンをタップするたびに表示される「楽しいこと」が変化します。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/4388cd44-b9cb-9359-b48a-aafc10593ee2.jpeg">

## AboutMeApp.swift
アプリ自体を定義するファイルですが、このAppプロジェクトの学習目標ではないので、特に理解する必要はありません。
実際のSwiftUIアプリ開発においても、自動的に「プロジェクト名 + App」という形式のファイル名になります。

```swift:AboutMeApp.swift
import SwiftUI

@main
struct AboutMeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```