【Xcode12.3】Appleプラットフォームのアプリ開発入門〜オブジェクト編〜【SwiftUI】

# SwiftUI とは
`SwiftUI`とは、AppleプラットフォームのアプリケーションUIを、開発言語を`Swift`とした`宣言型シンタックス(Declarative Syntax)`で構築する`UIフレームワーク`です。

`SwiftUI`同様、Apple製品のアプリケーションUIを構築するフレームワークとしては`Flutter`が有名ですが、その違いを以下の表にまとめました。

|   | SwiftUI | Flutter |
|:-:|:-:|:-:|
| 対応プラットフォーム | Appleのみ | iOS, Android, Web |
| IDE | Xcode | Android Studio, VS code |
| 記述形式 | 宣言的シンタックス | 宣言的シンタックス |
| 開発言語 | Swift | Dart, Widget |
| コード量 | 少ない | 多い |
| 拡張性 | Flutterに劣る | 高い |

Appleプラットフォームのアプリ開発にあたって、Apple公式のフレームワーク`SwiftUI`に注目していきます。

# SwiftUI
## テキスト
### テキスト(Text)
![スクリーンショット 2021-01-13 15.12.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/8c745849-9103-f123-4430-5a414ac1ec0a.png)

```swift:テキスト
Text("<文字列>")
```

### リンク(Link)
![スクリーンショット 2021-01-09 13.25.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/72bc7bf4-baa3-0b58-3fdd-0593d92f9215.png)

```swift:リンク
Link(destination: <URL>) { Text("<ラベル>") }
```

### テキストフィールド(Text Field)&セキュアテキストフィールド(Secure Field)

![スクリーンショット 2021-01-13 12.28.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/1a394ae6-e1fb-c59a-27ca-13a8176cc145.png)
↑未入力の状態(プレースホルダテキストが表示)

![スクリーンショット 2021-01-13 15.39.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/706a7b89-d9ae-d4aa-3cca-681914a87d64.png)

↑テキストフィールド。入力中の状態

![スクリーンショット 2021-01-13 12.14.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/62ff0840-cb83-935e-0abf-5e81e7ddf330.png)
↑セキュアテキストフィールド。入力中の状態

```swift:テキストフィールドとセキュアテキストフィールド
@State private var value: String = ""

// TextFieldとSecureFieldの値を同一プロパティにすることで連動が可能
TextField("<プレースホルダテキスト>", text: $value)
SecureField("<プレースホルダテキスト>", text: $value)
```

### テキストエディタ
![スクリーンショット 2021-01-13 15.33.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/9910479a-9ce7-27c0-f8c3-6e2bd47e74e0.png)

```swift:テキストエディタ
@State private var value: String = ""

TextEditor(text: $value)  // テキストエディタはプレースホルダテキストを標準で実装していない
```

## ボタン
### システムボタン(Button)
![スクリーンショット 2021-01-08 19.17.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/0d44224c-b9e5-20ab-ffce-6174a2eb8c98.png)

```swift:ボタン
Button(action: <実行メソッド>) { <ラベル> }
```

### エディタボタン
![スクリーンショット 2021-01-08 19.51.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/be779639-c98f-525f-c6f9-eeb06570f0cc.png)
↑エディタが非アクティブな状態

![スクリーンショット 2021-01-08 19.53.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/2bb108aa-ee88-7d98-3434-b1135a625add.png)
↑エディタがアクティブな状態

```swift:エディタボタン
EditButton()
```

### スイッチ(Toggle Button)
![スクリーンショット 2021-01-13 15.42.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/75bde22c-c897-5ae3-50c5-31dd16d4f257.png)

```swift:スイッチ
@State private var flg: Bool = True

Toggle(isOn: $flg) {
    <ラベル>
}
```

### サインインボタン(Sign In With Apple Button)
![スクリーンショット 2021-01-13 14.22.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/f604d68b-6a43-5b51-b1e7-fd0963b095d9.png)

```swift:サインインボタン
import Authentication Services  // ASAuthorizationAppleIDButtonの実装に必要なパッケージ

struct SignInWithAppleButton: UIViewRepresentable {
    func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
    // ASAuthorizationAppleIDButtonはApple標準のボタンコンポーネントだが、
    // SwiftUIの標準コンポーネントとして用意されていないため、SwiftUIのViewコンポーネントとして実装する必要がある
        let button = ASAuthorizationAppleIDButton()
        return button
    }

    func updateUIView(_: ASAuthorizationAppleIDButton, context _: Context) [}
}

SignInWithAppleButton()
```

`認証機能`を実装したい場合は、[こちら]
(https://inon29.hateblo.jp/entry/2020/03/07/171915)を参照。

## ピッカー
### ピッカ-(Picker)
![スクリーンショット 2021-01-13 11.30.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/21276f8a-a160-7c9a-c402-872c586a1125.png)

```swift:ピッカ-
Picker(selection: .<選択値プロパティ名>, label: Text(<ラベル>)) {
    Text(<ラベル>).tag(<選択値>)
}
```

### カラーピッカー(Color Picker)
![スクリーンショット 2021-01-08 19.32.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/22f102c3-1447-3572-197c-28f6ecf0455a.png)

```swift:カラーピッカー
ColorPicker("<ラベル>", selection: .constant(.<既定色>))
```

### 日付ピッカー(Date Picker)
![スクリーンショット 2021-01-08 19.37.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/0b5a45e5-c8dd-51e8-1767-ce30ca3bddde.png)

```swift:日付ピッカー
DatePicker(selection: .constant(Date()), label: { Text("<ラベル>") })
```

## リスト

### リスト(List)
![スクリーンショット 2021-01-09 13.34.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/7f0edf49-8d1d-f6d4-6792-5893a8eb5d40.png)

```swift:リスト
List { <コンテンツ> }
```

### ラベル(Label)
![スクリーンショット 2021-01-09 13.22.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/7cbf9f03-fd01-1531-6b8d-878ad444efa6.png)

```swift:ラベル
Label("<ラベル>", systemImage: "<記号>")
```

### セクション(Section)
![スクリーンショット 2021-01-13 12.11.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/94724fb1-2c1f-a06f-1f97-3dcea6cb7e9a.png)

```swift:セクション
Section(header: Text(<ラベル>)) {
    <コンテンツ>
}
```

### グループ(Disclosure Group)
![スクリーンショット 2021-01-08 19.43.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/516fae5c-8541-773e-f4d6-acbcf15fd835.png)
↑閉じた状態

![スクリーンショット 2021-01-08 19.44.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/b6ef7786-b497-120c-d492-a56058a5031f.png)
↑開いた状態

### アウトライン(Outline Group)
![スクリーンショット 2021-01-09 17.01.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/95464a76-6ea2-4517-305d-1a70dd2a70e8.png)
↑ListなしVer.

![スクリーンショット 2021-01-09 17.02.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/f0b998ae-bb22-c630-01ba-cfe84a595ac2.png)
↑ListありVer.

```swift:アウトライン(実装が難解だったため、ソースコードを記述)
struct CityData: Identifiable {  // OutlineGroupで使用するデータはIdentifiableプロトコルに適合していなければならない
    //構造体"CityData"のプロパティはid, name, sitesの3つ
    let id = UUID()  //UUID(ユニークID)…128bitの16進数
    let name: String
    var sites: [CityData]?  // タイプメソッドresult()によって値が後から代入される

    static func result() -> [CityData] {  // タイプメソッド(静的メソッド)の定義はstaticキーワードを付加
        let city1 = [CityData(name: "Site A"), CityData(name: "Site B")]
        let city2 = [CityData(name: "Site C"), CityData(name: "Site D")]

        return [CityData(name: "city1", sites: city1),
                CityData(name: "city1", sites: city1)]
    }  // タイプメソッドによってsitesの値が以下のようになる
       // [name: city1, sites: Optional([name: site A, sites: nil], [name: Site B, sites: nil]) 
       //  name: city2, sites: Optional([name: site C, sites: nil], [name: Site D, sites: nil])]
}

struct ContentView: View {
    var body: some View {
        List {
            ForEach(CityData.result()) { city in  // CityData.sitesのname: city1, city2のそれぞれに対して以下の処理を実行
                OutlineGroup(city, children: \.sites) { site in  // CityData.sites.sitesのname: site A, site Bのそれぞれに対して以下の処理を実行
                    Text(site.name)
                }
            }
        }
    }
}
```

```swift:Identifiableプロトコル
public protocol Identifiable {
    associatedtype ID: Hashable  //型パラメータ(=プレースホルダ)"ID"は"Hashable"プロトコルに適合
    var id: Self.ID { get }  //プロパティ"id"は読み取り専用であり、インスタンス自身の型パラメータ"ID"に適合
}
```

```swift:Hashableプロトコル
public protocol Hashable{
    func hash(into hasher: inout Hasher)
    var hashValue: Int { get }  
```

### グループボックス(Group Box)
![スクリーンショット 2021-01-09 13.19.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/36cad73f-79db-e22a-1547-bd63214d91e4.png)

```swift:グループボックス
GroupBox(label: <ラベル>) { <コンテンツ> }
```

```swift:グループ
DisclosureGroup("<ラベル>") { <コンテンツ> }
```

### フォーム(Form)
![スクリーンショット 2021-01-09 13.14.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/bdf0374c-5e9f-71ee-3091-b9b3c2f2f2f5.png)

```swift:フォーム
Form { <コンテンツ> }
```

##ビュー
### ナビゲーションビュー(Navigation View), ナビゲーションリンク(Navigation Link)
![Simulator Screen Shot - iPhone 12 - 2021-01-09 at 13.57.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/59bb01a0-e631-8e5b-67c9-53c77e18546c.png)
↑画面遷移前

![Simulator Screen Shot - iPhone 12 - 2021-01-09 at 13.57.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/4447050e-8e33-2444-a57c-cd4003369a7d.png)
↑画面遷移後

```swift:ナビゲーションリンク
NavigationView {  //NavigationLinkはNavigationViewのスコープ内で記述
    NavigationLink(destination: <Viewプロトコルに準拠したインスタンス>) { <ラベル> }
}
```

### 進捗ビュー(Progress View)
![スクリーンショット 2021-01-13 11.41.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/583a0a2b-620e-fd7e-65a1-9cb08ac4855f.png)

```swift:進捗ビュー
ProgressView(value: <0~1の進捗値>)
```

### スクロールビュー(Scroll View)
![スクリーンショット 2021-01-13 11.58.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/01e51083-68bc-30a2-3e05-c9e1dec07ff8.png)

```swift:スクロールビュー
ScrollView(.<方向>) {
    VStack {   // 縦方向の場合はVStackでまとめる
        <コンテンツ>
    }
    .frame(maxWidth: .infinity)  // 対象範囲の最大化
}
```

### スライダ(Slider)&ステッパ(Stepper)
![スクリーンショット 2021-01-13 14.36.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/c69c9f3d-f199-dd55-b00a-5c6c4ce7ff6c.png)
↑スライダ

![スクリーンショット 2021-01-13 14.38.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/b28ff796-4c89-8e49-6b13-ea8840845ae2.png)
↑ステッパ

```swift:スライダとステッパ
@State private var value: Double = 0

// SliderとStepperの値を同一プロパティにすることで連動が可能
Slider(value: $value, in: 0...100)
Stepper(value: $value, in: 0...100) {
    <ラベル>
}
```

### タブバー(Tab View)
![スクリーンショット 2021-01-13 14.46.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/673880/b988041e-4423-f935-72c9-afebb9804fdd.png)

```swift:タブバー
@State private var selection = 0 // 選択タブを保持するプロパティ

TabView(selection: $selection) {
    Text("<コンテンツ>").tabItem { Text(<ラベル>) }.tag(0)
    Text("<コンテンツ>").tabItem { Text(<ラベル>) }.tag(1)
}
```