# Tutorials ⟩ Views

## ContentView

{% tabs %}
{% tab title="files" %}
* ContentView.swift (12)
{% endtab %}

{% tab title="12" %}
```swift
import SwiftUI

struct ContentView: View {
    
    /// ⭐️ 1. an enumeration of the tabs to display.
    enum Tab {
        case featured
        case list
    }
    
    /// ⭐️ 2. state variable for the tab selection
    @State private var selection: Tab = .featured
    
    var body: some View {
        /// ⭐️ 3. TabView
        TabView(selection: $selection) {
            
            /// ⭐️ 4. set tab item & tag for each view
            CategoryHome().tabItem{
                Label("Featured", systemImage: "star")
            }.tag(Tab.featured)
            
            LandmarkList().tabItem{
                Label("List", systemImage: "list.bullet")
            }.tag(Tab.list)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    
    static var previews: some View {
        ContentView()
            .environmentObject(ModelData())
    }
}
sw
```
{% endtab %}
{% endtabs %}

## Profile

{% tabs %}
{% tab title="files" %}
* ProfileHost.swift (13)
* ProfileEditor.swift (14)
* ProfileSummary.swift (15)
{% endtab %}

{% tab title="13" %}
```swift
import SwiftUI

/// host both a static, summary view of profile information
/// and an edit mode.
struct ProfileHost: View {
    
    /// ⭐️ 1. copy data
    /// To avoid updating the global app state before confirming any edits,
    /// the editing view operates on a **copy** of the user's profile.
    @State private var draftProfile = Profile.default
    
    /// ⭐️ 2. access edit mode:
    /// an `@Environment` view property that keys off of the environment’s `\.editMode`.
    /// - key off: to take something as a controlling input datum. (意思跟「控制...」差不多)
    /// See [FreeDictionary][1]
    ///
    /// Storing the edit mode in the environment makes it simple for **multiple views**
    /// to update when the user enters and exits edit mode.
    ///
    /// SwiftUI provides storage in the environment for values you can access
    /// using the `@Environment` property wrapper. Access the `editMode` value
    /// to read or write the edit scope.
    ///
    /// [1]: https://idioms.thefreedictionary.com/key+off
    @Environment(\.editMode) var editMode
    
    /// ⭐️ global app state
    /// Read the user’s profile data from the environment
    /// to pass control of the data to the profile host.
    @EnvironmentObject var modelData: ModelData
    
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            
            HStack {
                
                /// ⭐️ 3. cancel button
                if editMode?.wrappedValue == .active {
                    Button("Cancel", role: .cancel) {
                        draftProfile = modelData.profile
                        editMode?.animation().wrappedValue = .inactive
                    }
                }
                
                Spacer()
                
                /// ⭐️ 4. edit button
                /// button that toggles the environment’s `editMode` value on and off.
                EditButton()
            }
            
            /// ⭐️ 5. normal/editor view
            /// displays either the static profile or
            /// the view for Edit mode.
            if editMode?.wrappedValue == .inactive {
                ProfileSummary(profile: modelData.profile)
            } else {
                ProfileEditor(profile: $draftProfile)
                    /// ⭐️ 6. a draft copy of real data
                    .onAppear { draftProfile = modelData.profile }
                    /// ⭐️ 7. write draft data back to real data
                    .onDisappear { modelData.profile = draftProfile }
            }
        }
        .padding()
    }
}

struct ProfileHost_Previews: PreviewProvider {
    static var previews: some View {
        ProfileHost()
            /// Even though this view doesn’t use a `@EnvironmentObject`,
            /// `ProfileSummary`, a child of this view, does. So without it,
            /// the preview fails.
            .environmentObject(ModelData())
    }
}

```
{% endtab %}

{% tab title="14" %}
```swift
import SwiftUI

struct ProfileEditor: View {
    
    /// a binding to the **draft copy** of the user’s profile.
    @Binding var profile: Profile
    
    var dateRange: ClosedRange<Date> {
        let min = Calendar.current.date(byAdding: .year, value: -1, to: profile.goalDate)!
        let max = Calendar.current.date(byAdding: .year, value: 1, to: profile.goalDate)!
        return min...max
    }
    
    var body: some View {
        List {
            
            /// username
            HStack {
                Text("Username").bold()
                Divider()
                TextField("Username", text: $profile.username)
            }
            
            /// user’s preference for receiving notifications
            Toggle(isOn: $profile.prefersNotifications) {
                Text("Enable Notifications").bold()
            }
            
            /// Seasonal Photo
            VStack(alignment: .leading, spacing: 20) {
                Text("Seasonal Photo").bold()
                
                Picker("Seasonal Photo", selection: $profile.seasonalPhoto) {
                    ForEach(Profile.Season.allCases) { season in
                        Text(season.rawValue).tag(season)
                    }
                }
                .pickerStyle(.segmented)
            }
            
            /// date picker
            /// make the landmark visitation goal date modifiable.
            DatePicker(
                selection: $profile.goalDate,
                in: dateRange,
                displayedComponents: .date
            ) {
                Text("Goal Date").bold()
            }
        }
    }
}

struct ProfileEditor_Previews: PreviewProvider {
    static var previews: some View {
        ProfileEditor(profile: .constant(.default))
    }
}

```
{% endtab %}

{% tab title="15" %}
```swift
import SwiftUI

struct ProfileSummary: View {
    
    @EnvironmentObject var modelData: ModelData
    
    /// The profile summary takes a `Profile` value rather than a **binding**
    /// to the profile because the parent view, `ProfileHost`, manages the state for this view.
    var profile: Profile

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 10) {
                Text(profile.username)
                    .bold()
                    .font(.title)
                Text("Notifications: \(profile.prefersNotifications ? "On": "Off" )")
                Text("Seasonal Photos: \(profile.seasonalPhoto.rawValue)")
                Text("Goal Date: ") + Text(profile.goalDate, style: .date)
                
                Divider()

                VStack(alignment: .leading) {
                    
                    Text("Completed Badges")
                        .font(.headline)
                    
                    ScrollView(.horizontal) {
                        HStack {
                            HikeBadge(name: "First Hike")
                            HikeBadge(name: "Earth Day")
                                .hueRotation(Angle(degrees: 90))
                            HikeBadge(name: "Tenth Hike")
                                .grayscale(0.5)
                                .hueRotation(Angle(degrees: 45))
                        }
                        .padding(.bottom)
                    }
                }
                
                Divider()

                VStack(alignment: .leading) {
                    Text("Recent Hikes")
                        .font(.headline)
                    HikeView(hike: modelData.hikes[0])
                }
            }
        }
    }
}

struct ProfileSummary_Previews: PreviewProvider {
    static var previews: some View {
        ProfileSummary(profile: Profile.default)
            .environmentObject(ModelData())
    }
}

```
{% endtab %}
{% endtabs %}

## Category

{% tabs %}
{% tab title="files" %}
* CategoryHome.swift (16)
* CategoryRow.swift (17)
* CategoryItem.swift (18)
{% endtab %}

{% tab title="16" %}
```swift
import SwiftUI

struct CategoryHome: View {
    
    @EnvironmentObject var modelData: ModelData
    
    /// ⭐️ 1. view state to control the presentation of the `ProfileHost` view
    @State private var showingProfile = false
    
    var body: some View {
        NavigationView {
            List {
                
                /// featured landmark
                modelData.features[0].image
                    .resizable()
                    .scaledToFill()
                    .frame(height: 150)
                    .cornerRadius(4)
                    /// set edge insets to zero so the content
                    /// can extend to the edges of the display.
                    .listRowInsets(EdgeInsets())
//                    .clipped()
                
                /// categories
                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    CategoryRow(categoryName: key, items: modelData.categories[key]!)
                }.listRowInsets(EdgeInsets())
            }
            .listStyle(.inset)
            .navigationTitle("Featured")
            
            /// ⭐️ 2. add a button to the navigation bar
            .toolbar {
                Button { showingProfile.toggle() } label: {
                    Label("User Profile", systemImage: "person.crop.circle")
                }
            }
            /// ⭐️ 3. present the `ProfileHost` view when the user taps the button.
            .sheet(isPresented: $showingProfile) {
                ProfileHost().environmentObject(modelData)
            }
        }
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
            .environmentObject(ModelData())
    }
}

```
{% endtab %}

{% tab title="17" %}
```swift
import SwiftUI

/// Landmarks displays each category in a row
/// that scrolls horizontally.
struct CategoryRow: View {
    
    /// category name
    var categoryName: String
    
    /// list of items in the category
    var items: [Landmark]
    
    var body: some View {
        VStack(alignment: .leading) {
            
            Text(categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)
            
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(items) { landmark in
                        NavigationLink {
                            LandmarkDetail(landmark: landmark)
                        } label: {
                            CategoryItem(landmark: landmark)
                        }
                    }
                }
            }.frame(height: 185)
        }
    }
}

struct CategoryRow_Previews: PreviewProvider {
    static var landmarks = ModelData().landmarks
    static var previews: some View {
        CategoryRow(
            categoryName: landmarks[0].category.rawValue,
            items: Array(landmarks.prefix(4))
        )
    }
}

```
{% endtab %}

{% tab title="18" %}
```swift
import SwiftUI

struct CategoryItem: View {
    var landmark: Landmark

    var body: some View {
        VStack(alignment: .leading) {
            landmark.image
                /// ⭐️ force images render as original, not as "template images"
                .renderingMode(.original)
                .resizable()
                .frame(width: 155, height: 155)
                .cornerRadius(5)
            Text(landmark.name)
                /// ⭐️ override the accent color when embeded in a `NavigationLink`
                .foregroundColor(.primary)
                .font(.caption)
        }
        .padding(.leading, 15)
    }
}

struct CategoryItem_Previews: PreviewProvider {
    static var previews: some View {
        CategoryItem(landmark: ModelData().landmarks[0])
    }
}


```
{% endtab %}
{% endtabs %}

## Hikes

{% tabs %}
{% tab title="files" %}
* GraphCapsule.swift (19)
* HikeGraph.swift (20)
* HikeDetail.swift (21)
* HikeView.swift (22)
* HikeBadge.swift (23)
{% endtab %}

{% tab title="19" %}
```swift
import SwiftUI

struct GraphCapsule: View, Equatable {
    var index: Int
    var color: Color
    var height: CGFloat
    var range: Range<Double>
    var overallRange: Range<Double>

    var heightRatio: CGFloat {
        max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
    }

    var offsetRatio: CGFloat {
        CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
    }

    var body: some View {
        Capsule()
            .fill(color)
            .frame(height: height * heightRatio)
            .offset(x: 0, y: height * -offsetRatio)
    }
}

struct GraphCapsule_Previews: PreviewProvider {
    static var previews: some View {
        GraphCapsule(
            index: 0,
            color: .blue,
            height: 150,
            range: 10..<50,
            overallRange: 0..<100)
    }
}

```
{% endtab %}

{% tab title="20" %}
```swift
import SwiftUI

struct HikeGraph: View {
    var hike: Hike
    var path: KeyPath<Hike.Observation, Range<Double>>

    var color: Color {
        switch path {
        case \.elevation:
            return .gray
        case \.heartRate:
            return Color(hue: 0, saturation: 0.5, brightness: 0.7)
        case \.pace:
            return Color(hue: 0.7, saturation: 0.4, brightness: 0.7)
        default:
            return .black
        }
    }

    var body: some View {
        let data = hike.observations
        let overallRange = rangeOfRanges(data.lazy.map { $0[keyPath: path] })
        let maxMagnitude = data.map { magnitude(of: $0[keyPath: path]) }.max()!
        let heightRatio = 1 - CGFloat(maxMagnitude / magnitude(of: overallRange))

        return GeometryReader { proxy in
            HStack(alignment: .bottom, spacing: proxy.size.width / 120) {
                ForEach(Array(data.enumerated()), id: \.offset) { index, observation in
                    GraphCapsule(
                        index: index,
                        color: color,
                        height: proxy.size.height,
                        range: observation[keyPath: path],
                        overallRange: overallRange
                    )
                        .animation(.ripple(index: index))
                }
                .offset(x: 0, y: proxy.size.height * heightRatio)
            }
        }
    }
}

func rangeOfRanges<C: Collection>(_ ranges: C) -> Range<Double>
    where C.Element == Range<Double> {
    guard !ranges.isEmpty else { return 0..<0 }
    let low = ranges.lazy.map { $0.lowerBound }.min()!
    let high = ranges.lazy.map { $0.upperBound }.max()!
    return low..<high
}

func magnitude(of range: Range<Double>) -> Double {
    range.upperBound - range.lowerBound
}

struct HikeGraph_Previews: PreviewProvider {
    static var hike = ModelData().hikes[0]

    static var previews: some View {
        Group {
            HikeGraph(hike: hike, path: \.elevation)
                .frame(height: 200)
            HikeGraph(hike: hike, path: \.heartRate)
                .frame(height: 200)
            HikeGraph(hike: hike, path: \.pace)
                .frame(height: 200)
        }
    }
}

```
{% endtab %}

{% tab title="21" %}
```swift
import SwiftUI

struct HikeDetail: View {
    let hike: Hike
    @State var dataToShow = \Hike.Observation.elevation

    var buttons = [
        ("Elevation", \Hike.Observation.elevation),
        ("Heart Rate", \Hike.Observation.heartRate),
        ("Pace", \Hike.Observation.pace)
    ]

    var body: some View {
        VStack {
            HikeGraph(hike: hike, path: dataToShow)
                .frame(height: 200)

            HStack(spacing: 25) {
                ForEach(buttons, id: \.0) { value in
                    Button {
                        dataToShow = value.1
                    } label: {
                        Text(value.0)
                            .font(.system(size: 15))
                            .foregroundColor(value.1 == dataToShow
                                ? .gray
                                : .accentColor)
                            .animation(nil)
                    }
                }
            }
        }
    }
}

struct HikeDetail_Previews: PreviewProvider {
    static var previews: some View {
        HikeDetail(hike: ModelData().hikes[0])
    }
}

```
{% endtab %}

{% tab title="22" %}
```swift
import SwiftUI

struct HikeView: View {
    var hike: Hike
    @State private var showDetail = true

    var body: some View {
        VStack {
            HStack {
                HikeGraph(hike: hike, path: \.elevation)
                    .frame(width: 50, height: 30)

                VStack(alignment: .leading) {
                    Text(hike.name)
                        .font(.headline)
                    Text(hike.distanceText)
                }

                Spacer()

                Button {
                    
                    /// Both of the views affected by `showDetail`
                    /// - disclosure button
                    /// - HikeDetail view
                    /// now have animated transitions.
                    withAnimation {
                        showDetail.toggle()
                    }
                    
                } label: {
                    Label("Graph", systemImage: "chevron.right.circle")
                        .labelStyle(.iconOnly)
                        .imageScale(.large)
                        .rotationEffect(.degrees(showDetail ? 90 : 0))
                        // turn off animation for the rotation
//                        .animation(nil, value: showDetail)
                        .scaleEffect(showDetail ? 1.5 : 1)
                        .padding()
//                        .animation(.spring(), value: showDetail)
                }
            }

            if showDetail {
                HikeDetail(hike: hike)
                    /// By default, views transition on/offscreen by fading in/out.
                    /// You can customize this by using `transition(_:)`.
                    .transition(.moveAndFade)
            }
        }
    }
}

struct HikeView_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            HikeView(hike: ModelData().hikes[0])
                .padding()
            Spacer()
        }
    }
}

```
{% endtab %}

{% tab title="23" %}
```swift
import SwiftUI

struct HikeBadge: View {
    
    var name: String

    var body: some View {
        VStack(alignment: .center) {
            Badge()
                .frame(width: 300, height: 300)
                .scaleEffect(1.0 / 3.0)
                .frame(width: 100, height: 100)
            Text(name)
                .font(.caption)
                .accessibilityLabel("Badge for \(name).")
        }
    }
}

struct HikeBadge_Previews: PreviewProvider {
    static var previews: some View {
        HikeBadge(name: "Preview Testing")
    }
}
```
{% endtab %}
{% endtabs %}

## Badge

{% tabs %}
{% tab title="files" %}
* HexagonParameters.swift (24)
* BadgeBackground.swift (25)
* BadgeSymbol.swift (26)
* RotatedBadgeSymbol.swift (27)
* Badge.swift (28)
{% endtab %}

{% tab title="24" %}
```swift
import CoreGraphics

/// define the shape of a hexagon.
struct HexagonParameters {
    /// hold three points that represent one side of the hexagon.
    ///
    /// Each side starts where the previous ends, moves in a straight line
    /// to the first point, and then moves over a Bézier curve at the corner
    /// to the second point. The third point controls the shape of the curve.
    struct Segment {
        let line: CGPoint
        let curve: CGPoint
        let control: CGPoint
    }
    
    /// an adjustment value to tune the shape of the hexagon.
    static let adjustment: CGFloat = 0.085
    
    /// data for the six segments,
    /// the values are stored as a fraction of a **unit square**.
    static let segments = [
        Segment(
            line:    CGPoint(x: 0.60, y: 0.05),
            curve:   CGPoint(x: 0.40, y: 0.05),
            control: CGPoint(x: 0.50, y: 0.00)
        ),
        Segment(
            line:    CGPoint(x: 0.05, y: 0.20 + adjustment),
            curve:   CGPoint(x: 0.00, y: 0.30 + adjustment),
            control: CGPoint(x: 0.00, y: 0.25 + adjustment)
        ),
        Segment(
            line:    CGPoint(x: 0.00, y: 0.70 - adjustment),
            curve:   CGPoint(x: 0.05, y: 0.80 - adjustment),
            control: CGPoint(x: 0.00, y: 0.75 - adjustment)
        ),
        Segment(
            line:    CGPoint(x: 0.40, y: 0.95),
            curve:   CGPoint(x: 0.60, y: 0.95),
            control: CGPoint(x: 0.50, y: 1.00)
        ),
        Segment(
            line:    CGPoint(x: 0.95, y: 0.80 - adjustment),
            curve:   CGPoint(x: 1.00, y: 0.70 - adjustment),
            control: CGPoint(x: 1.00, y: 0.75 - adjustment)
        ),
        Segment(
            line:    CGPoint(x: 1.00, y: 0.30 + adjustment),
            curve:   CGPoint(x: 0.95, y: 0.20 + adjustment),
            control: CGPoint(x: 1.00, y: 0.25 + adjustment)
        )
    ]
}

```
{% endtab %}

{% tab title="25" %}
```swift
import SwiftUI

struct BadgeBackground: View {
    
    static let start = Color(red: 239.0 / 255, green: 120.0 / 255, blue: 221.0 / 255)
    static let end = Color(red: 239.0 / 255, green: 172.0 / 255, blue: 120.0 / 255)
    
    var body: some View {
        
        // wrap the path in a `GeometryReader` so the badge can use
        // the size of its containing view
        GeometryReader { geo in
            
            // use paths to combine lines, curves, and other drawing primitives
            // to form more complex shapes.
            Path { path in

                // Rectangular (inscribedSquare) + CGSize+ext (size.point)
                let p = geo.inscribedSquare.size.point
                let a = HexagonParameters.adjustment
                
                // squeeze x direction
                let xScale = 0.8
                let s = CGPoint(xScale, 1)
                let v = CGPoint((1 - xScale) / 2, 0) ** p
                
                // p[x, y]: Rectangular protocol
                path.move(to: p[0.95, 0.20 + a] ** s + v)
                
                HexagonParameters.segments.forEach { segment in
                    // p ** q : (Vector2D) non-proportional scale
                    path.addLine(to: p ** segment.line ** s + v)
                    path.addQuadCurve(
                        to     : p ** segment.curve ** s + v,
                        control: p ** segment.control ** s + v
                    )
                }
            }
            .fill(.linearGradient(
                colors    : [Self.start, Self.end],
                startPoint: [0.5, 0.0],                 // UnitPoint+Vector2D
                endPoint  : [0.5, 1.0]
            ))
        }
        /// By preserving a 1:1 aspect ratio, the badge maintains its position
        /// at the center of the view
        .aspectRatio(1, contentMode: .fit)
    }
}

struct BadgeBackground_Previews: PreviewProvider {
    static var previews: some View {
        BadgeBackground()
    }
}

```
{% endtab %}

{% tab title="26" %}
```swift
import SwiftUI

struct BadgeSymbol: View {
    
    static let symbolColor = Color(red: 79.0 / 255, green: 79.0 / 255, blue: 191.0 / 255)

    var body: some View {
        GeometryReader { geometry in
            Path { path in
                
                let width = min(geometry.size.width, geometry.size.height)
                let height = width * 0.75
                let spacing = width * 0.030
                let middle = width * 0.5
                let topWidth = width * 0.226
                let topHeight = height * 0.45
                
                path.addLines([
                    CGPoint(x: middle, y: spacing),
                    CGPoint(x: middle - topWidth, y: topHeight - spacing),
                    CGPoint(x: middle, y: topHeight / 2 + spacing),
                    CGPoint(x: middle + topWidth, y: topHeight - spacing),
                    CGPoint(x: middle, y: spacing)
                ])
                
                /// use `move(to:)` to insert a gap between multiple shapes
                /// in the same path.
                path.move(to: CGPoint(x: middle, y: topHeight / 2 + spacing * 3))
                
                path.addLines([
                    CGPoint(x: middle - topWidth, y: topHeight + spacing),
                    CGPoint(x: spacing, y: height - spacing),
                    CGPoint(x: width - spacing, y: height - spacing),
                    CGPoint(x: middle + topWidth, y: topHeight + spacing),
                    CGPoint(x: middle, y: topHeight / 2 + spacing * 3)
                ])
            }.fill(Self.symbolColor)
        }
    }
}

struct BadgeSymbol_Previews: PreviewProvider {
    static var previews: some View {
        BadgeSymbol()
    }
}

```
{% endtab %}

{% tab title="27" %}
```swift
import SwiftUI

struct RotatedBadgeSymbol: View {
    let angle: Angle
    var body: some View {
        BadgeSymbol()
            .padding(-60)       // ⭐️ 向外擴張
            .rotationEffect(angle, anchor: .bottom)
    }
}

struct RotatedBadgeSymbol_Previews: PreviewProvider {
    static var previews: some View {
        RotatedBadgeSymbol(angle: .degrees(5))
    }
}

```
{% endtab %}

{% tab title="28" %}
```swift
import SwiftUI

struct Badge: View {
    
    var body: some View {
        ZStack {
            
            BadgeBackground()
            
            GeometryReader { geometry in
                let p = geometry.size.point
                badgeSymbols
                    .scaleEffect(1.0 / 4.0, anchor: .top)
                    .position(p ** [0.5, 0.75])
            }
        }
        .scaledToFit()
    }
    
    var badgeSymbols: some View {
        ForEach(0 ..< 8) { i in
            RotatedBadgeSymbol(angle: .degrees(Double(i) / 8.0) * 360.0)
        }
        .opacity(0.5)
    }
}

struct Badge_Previews: PreviewProvider {
    static var previews: some View {
        Badge()
    }
}

```
{% endtab %}
{% endtabs %}

## Parts

{% tabs %}
{% tab title="files" %}
* CircleImage.swift (29)
* MapView.swift (30)
* FavoriteButton.swift (31)
{% endtab %}

{% tab title="29" %}
```swift
import SwiftUI

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

struct CircleImage_Previews: PreviewProvider {
    static var previews: some View {
        CircleImage(image: ModelData().landmarks[1].image)
    }
}

```
{% endtab %}

{% tab title="30" %}
```swift
import SwiftUI
import MapKit

struct MapView: View {
    
    /// center of map region
    var coordinate: CLLocationCoordinate2D
    
    /// map region (updated onAppear)
    @State private var region = MKCoordinateRegion()
    
    var body: some View {
        Map(coordinateRegion: $region)
            .onAppear { setRegion(coordinate) }
    }
    
    /// updates the region based on a coordinate value.
    /// - Parameter coordinate: center of the region
    private func setRegion(_ coordinate: CLLocationCoordinate2D) {
        region = MKCoordinateRegion(
            center: coordinate,
            span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
        )
    }
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView(coordinate: CLLocationCoordinate2D(
            latitude: 34.011_286,
            longitude: -116.166_868
        ))
    }
}

```
{% endtab %}

{% tab title="31" %}
```swift
import SwiftUI

struct FavoriteButton: View {
    
    /// changes made inside this view propagate back to the data source.
    @Binding var isSet: Bool
    
    var imageName: String { isSet ? "star.fill" : "star" }
    var imageColor: Color { isSet ? .yellow : .gray }
    
    var body: some View {
        Button {
            isSet.toggle()
        } label: {
            /// title string doesn’t appear in the UI with `iconOnly`,
            /// but VoiceOver uses it to improve accessibility.
            Label("Favorite", systemImage: imageName)
                .labelStyle(.iconOnly)
                .foregroundColor(imageColor)
        }

    }
}

struct FavoriteButton_Previews: PreviewProvider {
    
    // doesn't work!
    @State static private var isSet = false
    
    static var previews: some View {
        HStack {
            FavoriteButton(isSet: .constant(true))
            FavoriteButton(isSet: $isSet)
        }
    }
}

```
{% endtab %}
{% endtabs %}

## Landmarks

{% tabs %}
{% tab title="files" %}
* LandmarkList.swift (32)
* LandmarkRow.swift (33)
* LandmarkDetail.swift (34)
{% endtab %}

{% tab title="32" %}
```swift
import SwiftUI

struct LandmarkList: View {
    
    // 4. adopt the observable object
    /// The `modelData` property gets its value automatically, as long as
    /// the `environmentObject(_:)` modifier has been applied to a *parent*.
    @EnvironmentObject var modelData: ModelData
    
    @State private var showFavoritesOnly = false
    
    var body: some View {
        
        // `NavigationView` and `NavigationLink` work together
        NavigationView {
            List {
                
                // static view
                Toggle(isOn: $showFavoritesOnly) {
                    Text("Favorites Only")
                }
                
                // dynamic views
                ForEach(filteredLandmarks) { landmark in
                    // `NavigationLink` works with `NavigationView`
                    NavigationLink {
                        LandmarkDetail(landmark: landmark)
                    } label: {
                        LandmarkRow(landmark: landmark)
                    }
                }
                
            }.navigationTitle("Landmarks")
        }
    }
    
    /// filtered landmarks
    var filteredLandmarks: [Landmark] {
        modelData.landmarks.filter { landmark in
            (!showFavoritesOnly || landmark.isFavorite)
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkList()
            // 5. inject the observable object in a parent view
            .environmentObject(ModelData())
    }
}

```
{% endtab %}

{% tab title="33" %}
```swift
import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark
    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)
            
            Spacer()
            
            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .foregroundColor(.yellow)
            }
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var landmarks = ModelData().landmarks
    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarks[0])
            LandmarkRow(landmark: landmarks[1])
        }
        // set a size that approximates a row in a list.
        .previewLayout(.fixed(width: 300, height: 70))
    }
}

```
{% endtab %}

{% tab title="34" %}
```swift
import SwiftUI
import Neumorphic

struct LandmarkDetail: View {
    
    var landmark: Landmark
    
    /// get model object from environment
    @EnvironmentObject var modelData: ModelData
    
    /// index in the `landmarks` array
    var i: Int {
        modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }
    
    var body: some View {
        VStack {
            MapView(coordinate: landmark.locationCoordinate)
                .ignoresSafeArea(edges: .top)
                .frame(height: 300)
            CircleImage(image: landmark.image)
                .offset(y: -130)
                .padding(.bottom, -130)
            VStack(alignment: .leading) {
                HStack {
                    FavoriteButton(isSet: $modelData.landmarks[i].isFavorite)
                    Text(landmark.name)
                        .font(.title)
                }
                HStack {
                    Text(landmark.park)
                    Spacer()
                    Text(landmark.state)
                }
                .font(.subheadline)
                .foregroundColor(.secondary)
                
                Divider()
                
                Text("About \(landmark.name)")
                    .font(.title2)
                ScrollView {
                    Text(landmark.description).padding(10)
                }.background(
                    Rectangle()
                        .fill(.white)
                        // `Neumorphic` framework
                        .softInnerShadow(Rectangle(), spread: 0.15, radius: 5)
                ).border(.gray)
            }
            .padding()
            Spacer()
        }
        .navigationTitle(landmark.name)
        // navigation title 字體變小、置中
        .navigationBarTitleDisplayMode(.inline)
    }
}

struct LandmarkDetail_Previews: PreviewProvider {
    
    /// preview device names
    static let deviceNames = [
        "iPhone SE (2nd generation)",
        "iPhone XS Max",
        "iPhone 13 Pro Max"
    ]
    
    static var previews: some View {
        ForEach(deviceNames, id: \.self) { deviceName in
            LandmarkDetail(landmark: ModelData().landmarks[1])
                // 1. .previewDevice() 要接收一個 PreviewDevice? 參數
                // 2. 可以使用 `.previewDevice("iPhone XS Max")` 的語法，因為 PreviewDevice: ExpressibleByStringLiteral
                // 3. 但不能使用 `.previewDevice(deviceName)` 的語法，因為 `deviceName` 是一個 String variable，並不是 String literal。
//                .previewDevice(PreviewDevice(rawValue: deviceName))
                .previewDevice(deviceName)  // View+ext
                .previewDisplayName(deviceName)
                .environmentObject(ModelData())
        }
    }
}

```
{% endtab %}
{% endtabs %}
