Tutorials ⟩ Views
ContentView
ContentView.swift (12)
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
Profile
ProfileHost.swift (13)
ProfileEditor.swift (14)
ProfileSummary.swift (15)
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())
}
}
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))
}
}
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())
}
}
Category
CategoryHome.swift (16)
CategoryRow.swift (17)
CategoryItem.swift (18)
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())
}
}
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))
)
}
}
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])
}
}
Hikes
GraphCapsule.swift (19)
HikeGraph.swift (20)
HikeDetail.swift (21)
HikeView.swift (22)
HikeBadge.swift (23)
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)
}
}
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)
}
}
}
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])
}
}
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()
}
}
}
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")
}
}
Badge
HexagonParameters.swift (24)
BadgeBackground.swift (25)
BadgeSymbol.swift (26)
RotatedBadgeSymbol.swift (27)
Badge.swift (28)
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)
)
]
}
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()
}
}
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()
}
}
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))
}
}
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()
}
}
Parts
CircleImage.swift (29)
MapView.swift (30)
FavoriteButton.swift (31)
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)
}
}
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
))
}
}
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)
}
}
}
Landmarks
LandmarkList.swift (32)
LandmarkRow.swift (33)
LandmarkDetail.swift (34)
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())
}
}
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))
}
}
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())
}
}
}
Last updated