SwiftUIチュートリアルを雑にやってみるその2

https://developer.apple.com/tutorials/swiftui/building-lists-and-navigation
コレやっていきましょう
まじで雑にやっていくので、コレは違うわいクソボケがというところあったらコメントなりなんなりください。

# Building Lists and Navigation編
Xcode-betaは事前にインスコしといてください。
preview機能はOSもcatalinaでないと使えないようです。

プロジェクトは[前回](https://qiita.com/mtzack/items/1b216646161afd11a6ec)のものを引き続き使います。
# 今回はListViewを作ります。
おなじみリストViewです。
まずはモデルから作っていくようです。
> In the Project navigator, choose Models > Landmark.swift.

プロジェクトナビゲーターからModelsを選んでLandmark.swift！
全く意味がわかりません。プロジェクトナビゲーターは左側の~~.swiftとかAssets.xcassetとかが並んでるところですね。
Modelsを選んでと言われてもそんなものありません。仕方ないのでゴリ押しで行きます。

今までファイルを作っていた階層に`new Group`を作ります。名前はModelsで。
おそらくmodelを作ってここで管理するのでしょう。
そのModels配下に`new File`でLandmark.swiftを作成しましょう。
たぶん`Swift File`で十分です。

中身はそのままコピペしていきましょう。

```swift
import SwiftUI
import CoreLocation

struct Landmark: Hashable, Codable {
    var id: Int
    var name: String
    fileprivate var imageName: String
    fileprivate var coordinates: Coordinates
    var state: String
    var park: String
    var category: Category

    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(
            latitude: coordinates.latitude,
            longitude: coordinates.longitude)
    }

    func image(forSize size: Int) -> Image {
        ImageStore.shared.image(name: imageName, size: size)
    }

    enum Category: String, CaseIterable, Codable, Hashable {
        case featured = "Featured"
        case lakes = "Lakes"
        case rivers = "Rivers"
    }
}

struct Coordinates: Hashable, Codable {
    var latitude: Double
    var longitude: Double
}
```

ImageStoreってなんやねんみたいな怒られも発生しつつ、とりあえず次に進みます。

> In the Project navigator, choose Resources > landmarkData.json.

データソースになるjsonをResoureces配下に作れとおっしゃっています。
これもゴリ押しでLandmark.swiftと同じように作ってみましょう。後は野となれ山となれです。
なぜかキャメルケースなんですが、swift界ではResources配下のファイルはキャメルケースというお約束があるんでしょうか？
Resourcesディレクトリですが、以下のように配置することでbundleに認識されるようです。

```
hoge
├── Resources
│   └── landmarkData.json
├── hoge
│   ├── AppDelegate.swift
│   ├── Base.lproj
│   │   └── LaunchScreen.storyboard
│   ├── CircleImage.swift
│   ├── Info.plist
│   ├── LandmarkDetail.swift
│   ├── LandmarkList.swift
│   ├── LandmarkRow.swift
│   ├── Map.swift
│   ├── Models
│   │   ├── Data.swift
│   │   └── Landmark.swift
│   └── SceneDelegate.swift
```

ファイル作成時に`Geojson File`の選択肢しかなかったので、無理やりlandmarkData.jsonにして保存しました。
中身はコピペしてちょっといじりましょう。

```json
[
    {
        "name": "Turtle Rock",
        "category": "Featured",
        "city": "Twentynine Palms",
        "state": "California",
        "id": 1001,
        "park": "Joshua Tree National Park",
        "coordinates": {
            "longitude": -116.166868,
            "latitude": 34.011286
        },
        "imageName": "turtlerock"
    },
    {
        "name": "Silver Salmon Creek",
        "category": "Lakes",
        "city": "Port Alsworth",
        "state": "Alaska",
        "id": 1002,
        "park": "Lake Clark National Park and Preserve",
        "coordinates": {
            "longitude": -152.665167,
            "latitude": 59.980167
        },
        "imageName": "silversalmoncreek"
    }
]

```

ガッツリ脂の乗ったjsonですね。こいつぁ活きが良いや！

> Note that the ContentView type from Creating and Combining Views is now named LandmarkDetail.

その次に[前回](https://qiita.com/mtzack/items/1b216646161afd11a6ec)作った`contentView.swift`をrenameしましょう。ファイルを開いてContentViewの部分を右クリックし、refactorからrenameでパッと変えられます。

このまま次に行く前にImageStoreってなんやねんて怒られたままなので、定義しましょう。
チュートリアルのproject filesに`Data.swift`というファイルがありますのでまるまるコピーします。内容は各自確認してください。

```swift
import UIKit
import SwiftUI
import CoreLocation

let landmarkData: [Landmark] = load("landmarkData.json")

func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
    let data: Data
    
    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
        else {
            fatalError("Couldn't find \(filename) in main bundle.")
    }
    
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }
    
    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}

final class ImageStore {
    fileprivate typealias _ImageDictionary = [String: [Int: CGImage]]
    fileprivate var images: _ImageDictionary = [:]
    
    fileprivate static var originalSize = 250
    fileprivate static var scale = 2
    
    static var shared = ImageStore()
    
    func image(name: String, size: Int) -> Image {
        let index = _guaranteeInitialImage(name: name)
        
        let sizedImage = images.values[index][size]
            ?? _sizeImage(images.values[index][ImageStore.originalSize]!, to: size * ImageStore.scale)
        images.values[index][size] = sizedImage
        
        return Image(sizedImage, scale: Length(ImageStore.scale), label: Text(verbatim: name))
    }
    
    fileprivate func _guaranteeInitialImage(name: String) -> _ImageDictionary.Index {
        if let index = images.index(forKey: name) { return index }
        
        guard
            let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
            let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
            let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
            else {
                fatalError("Couldn't load image \(name).jpg from main bundle.")
        }
        
        images[name] = [ImageStore.originalSize: image]
        return images.index(forKey: name)!
    }
    
    fileprivate func _sizeImage(_ image: CGImage, to size: Int) -> CGImage {
        guard
            let colorSpace = image.colorSpace,
            let context = CGContext(
                data: nil,
                width: size, height: size,
                bitsPerComponent: image.bitsPerComponent,
                bytesPerRow: image.bytesPerRow,
                space: colorSpace,
                bitmapInfo: image.bitmapInfo.rawValue)
            else {
                fatalError("Couldn't create graphics context.")
        }
        context.interpolationQuality = .high
        context.draw(image, in: CGRect(x: 0, y: 0, width: size, height: size))
        
        if let sizedImage = context.makeImage() {
            return sizedImage
        } else {
            fatalError("Couldn't resize image.")
        }
    }
}

```

チュートリアルとして綺麗なとこだけ見せたかったんですかね。

# Create the Row View
リストの元となる行Viewを作っていきましょう。
多分その行をクリックするとDetailViewが表示されるとかそんなやつ。

> Create a new SwiftUI view, named LandmarkRow.swift.

LandmarkRow.swiftを`SwiftUI View`で作っていきます。
デフォルトで

```swift
import SwiftUI

struct LandmarkRow : View {
    var body: some View {
        Text("Hello World!")
    }
}

#if DEBUG
struct LandmarkRow_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkRow()
    }
}
#endif
```

こんな感じになっているはず。ここでpreviewを有効化してくださいとか抜かしてくるので無視します。男は黙ってViewは勘で作る。（使える方は使える方は使ってください）

```swift
import SwiftUI

struct LandmarkRow : View {
    var landmark: Landmark
    
    var body: some View {
        HStack { //HorizontalStack横にViewが積み上がる
            landmark.image(forSize: 50) //Landmarkから画像Viewの呼び出し
            Text(landmark.name)
        }
    }
}

#if DEBUG
struct LandmarkRow_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkRow(landmark: landmarkData[0]) // jsonデータから先頭データを取得
    }
}
#endif
```

こんな感じになりましたね。
これでリストに表示される行Viewができました。
あってるかどうかXcodeのlintがギンギンになってるので不安しかないですが進めていきましょう。

# Customize the Row Preview

preview用に見た目をカスタマイズすることができます！なんてすごい機能なんだ！やばいね！

```swift
struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        Group { // まとめて
            LandmarkRow(landmark: landmarkData[0])
            LandmarkRow(landmark: landmarkData[1])
        }
        .previewLayout(.fixed(width: 300, height: 70)) //見た目変えられる
    }
}
```

# Create the List of Landmarks

> Create a new SwiftUI view, named LandmarkList.swift.

毎度おなじみViewを作りましょう。ここまででファイル構成がメチャクチャなのでディレクトリのベストプラクティスが知りたい。
内容はこんな感じ

```swift
import SwiftUI

struct LandmarkList : View {
    var body: some View {
        List {
            LandmarkRow(landmark: landmarkData[0])
            LandmarkRow(landmark: landmarkData[1])
        }
    }
}

#if DEBUG
struct LandmarkList_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
#endif
```

もしかして`List {}`だけでリストViewが作れるのか？

# Make the List Dynamic
いい加減チュートリアルクソ長くないですか？そう思ってるの僕だけですか？
動的にリストを作りましょうのコーナーです。
先程の`LandmarkList`をちょっと手を加えてデータ配列によって動的にリストが表示されるようにします。

```swift
import SwiftUI

struct LandmarkList : View {
    var body: some View {
         List(landmarkData.identified(by: \.id)) { landmark in
             LandmarkRow(landmark: landmark)
         }
    }

}

#if DEBUG
struct LandmarkList_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
#endif
```

Listにデータ配列を渡してクロージャでViewを返すようにしてあげればいいみたいです。

チュートリアルの方では

```swift
         List(landmarkData) { landmark in
             LandmarkRow(landmark: landmark)
         }
```
とスッキリ書かれていて非常に気持ちいいですが、弊環境ではbuildできませんでした。なんで？

# Set Up Navigation Between List and Detail
リストとデテールをセットアップしましょう！

> Embed the dynamically generated list of landmarks in a NavigationView

まずナヴィゲーションViewでListViewをくるんでやります。

```swift
struct LandmarkList : View {
    var body: some View {
        NavigationView{
            List(landmarkData.identified(by: \.id)) { landmark in
                LandmarkRow(landmark: landmark)
            }
        }
    }
}
```

> Call the navigationBarTitle(_:) modifier method to set the title of the navigation bar when displaying the list.

ナビゲーションViewにタイトルをつけましょう。

```swift
  NavigationView{
            List...
  }.navigationBarTitle(Text("Landmarks"))
```
簡単ですね。

> Inside the list’s closure, wrap the returned row in a NavigationButton, specifying the LandmarkDetail view as the destination.

中のクロージャでナヴィゲーションButtonでラップしてデテールとデスティネーションだそうです。

僕の場合はなのですが、以下のような状態になりました。

```swift
import SwiftUI

struct LandmarkList : View {
    var body: some View {
        NavigationView{
            List(landmarkData.identified(by: \.id)) { landmark in
                NavigationButton(destination: LandmarkDetail()) {
                    LandmarkRow(landmark: landmark)
                }
            }
        }.navigationBarTitle(Text("Landmarks"))
    }
}

#if DEBUG
struct LandmarkList_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkList()
    }
}
#endif
```

うーん、コレでまじで動くのか？

# Pass Data into Child Views
子Viewをいじっていくよ。
前回作ったCircleImageをいじいじするよ。

```swift
import SwiftUI

struct CircleImage : View {
    var image :Image
    var body: some View {
        image
        .clipShape(Circle())
        .overlay(Circle().stroke(Color.gray, lineWidth: 4))
        .shadow(radius: 10)
    }
}

#if DEBUG
struct CircleImage_Previews : PreviewProvider {
    static var previews: some View {
        CircleImage(image: Image("turtlerock"))
    }
}
#endif
```

なんとなくわかってきたのはViewを継承したstructはbodyにViewをハメることでViewをカスタマイズできるということだと思う。`some`の意味がわからん。
structの要素がデータだったりViewだったりがもててカスタムViewに使用できる。そんな感じじゃあないかと思った。

次Map

```swift
import SwiftUI
import MapKit

struct Map : UIViewRepresentable {
    var coordinate: CLLocationCoordinate2D
    
    func makeUIView(context: Context) -> MKMapView {
        MKMapView(frame: .zero)
    }
    
    func updateUIView(_ view: MKMapView, context: Context) {
        
        let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        view.setRegion(region, animated: true)
    }
}


#if DEBUG
struct Map_Previews : PreviewProvider {
    static var previews: some View {
        Map(coordinate: landmarkData[0].locationCoordinate)
    }
}
#endif
```
var coordinateを追加することによって決め打ちだった地図上のポイントが外部から設定できるようになったね。


次デテールViewを編集、今までMap,CircleImageに対する引数を追加する変更だったから、それを修正して、更に引数でもらったデータで表示情報を変えることにしよう。

```swift
import SwiftUI

struct LandmarkDetail : View {
    var landmark: Landmark //要素追加
    var body: some View {
        VStack{
            Map(coordinate: landmark.locationCoordinate) //landmarkからデータを引っ張ってくる
                .frame(height: 300)
            CircleImage(image: landmark.image(forSize: 250)) //landmarkからデータを引っ張ってくる
                .offset(y: -130)
                .padding(.bottom, -130)
            VStack(alignment: .leading){
                Text(landmark.name) //landmarkからデータを引っ張ってくる
                HStack{
                    Text(landmark.park) //landmarkからデータを引っ張ってくる
                    Spacer()
                    Text(landmark.state) //landmarkからデータを引っ張ってくる
                }
            }.padding()
            Spacer()
        }.navigationBarTitle(Text(landmark.name), displayMode: .inline)
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarkData[0])
    }
}
#endif
```
いい感じ。


そして、最後に重要なのは、アプリの最初の画面をどのViewに紐付けるか！

`SceneDelegate.swift`の24行目

```swift
window.rootViewController = UIHostingController(rootView: 
LandmarkDetail())
```
を

```swift
window.rootViewController = UIHostingController(rootView: LandmarkList())
```

に書き換えよう！

よーし、buildや！
と、その前に、このチュートリアルでは、Resources配下にｊｐｇ画像が配置されていることが前提になっているので、`landmarkData.json`で設定したimageNameと同じ名前の画像.jpgを配置してください。
またResourcesディレクトリを選択肢、FileInspectorからLocation>Relative to projectを選択してください。
この記事の流れで作っている方はそうしないと実行時エラーになると思います。

気を取り直してbuild！
<img width="584" alt="スクリーンショット 2019-06-06 01.49.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101587/42c0f60b-55bb-2e27-7e4f-e414a339cb69.png"><img width="584" alt="スクリーンショット 2019-06-06 01.49.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101587/68bf1a21-b38b-50b0-39d2-46bbd07d8155.png">


# Generating Previews Dynamically
preview画面でiOS/iPad等別々に表示できますよ的なことなのでここだけ割愛

以上でチュートリアル２終了ですお疲れ様でございました。

# 所感
ListViewの簡単さ、NavigationViewの容易さはちょっとびっくりしました。
何ができて何ができないのか？が気になってきたので、もっと深ぼって行きたいと思いました。
うーん、SwiftUI、俺は嫌いじゃないぜ。