🍎
ios.dev
ios.dev
  • 🍎ios.dev
    • 🛠️tools
    • 📰News
    • 📅Events
      • WWDC22
    • 🔲todo
      • 📖to read ...
      • 🧪研究中 ...
        • Formatter
        • Dependency Injection
        • 🔸Never
        • 🧪Google Sheet as JSON
      • 💈Examples
        • 💈MonthView
        • 💈CircleText
        • 💈Collapsible
      • 📓課程筆記
        • 📚SwiftUI Tutorials
          • Tutorials ⟩ Model
          • Tutorials ⟩ Views
          • Tutorials ⟩ Extensions
        • 📚Swift Animation Mastery
          • Triggers
            • 💈example ⟩ Picker
            • 💈example ⟩ SubButtons
        • Emoji Memory Game 📚
        • 設計卡片 📚
          • 排版
          • 元件化
          • 設定動畫
          • 設定手勢
          • 動畫的屬性
        • 新擬物風 (Neumorphic Design)
          • 漸層設計
            • Neu 🔸
          • 卡片設計
            • NeuCard 📦
          • 按鈕設計
            • IconCard 📦
            • ButtonUp, ButtonDown 📦
          • 設計進度條
            • NeuProgressBarTitle 📦
            • MyProgressBar2 📦
            • NeuProgressBar2 📦
      • 自訂型別
        • @KilometersPerHour
        • 🐶PlaygroundConsole
        • 👔EmojiTextFieldStyle
        • 🌅Outlined
        • 🌅BlurView
        • 🌅 SystemImage
        • 🌅 Fit
        • 🌅 Unwrap
        • ✨Custom Circle
    • 🔰templates
      • 🗒️page template
    • 🧪lab
    • 🔰terms
      • 🔰roles
        • 🔸implementer
        • 🔸user
      • 🔸state
      • 🔸data separation
      • 🔸data model
      • 🔸source of truth
      • 🔸user interaction
      • 🔸user interface
  • ⭐features
    • ⭐Swift
      • ⭐️ Swift 5.6
        • any
        • type placeholders
      • ⭐️ Swift 5.7
    • ⭐SwiftUI
      • ⭐SwiftUI 4.0
    • ⭐Xcode
      • Xcode 14
    • ⭐iOS
      • ⭐️ iOS 15
        • ⭐️ Live Text
      • ⭐️ iOS 16
    • Global Variables
    • Destructuring
    • Variadic Parameter
    • autoclosure
    • 🔰Key Path
      • Key Path Expressions as Functions
    • Calling Types As Functions
  • 👔custom
    • 🌀extension
      • 🌀View+
        • 👔view.if(_:then:)
        • 👔view.if(let:then:)
        • 👔view.inverseMask()
        • 👔view.overlayText()
        • 👔view.resizableFont()
        • 👔view.watermark()
        • 🚫view.foreground()
      • 🌀Color+
        • 🌀system colors
          • 🖼️SystemColorsView
          • 🖼️Swatch_0
        • 🌀Color.rgb()
        • 🌀color.hue
      • 🌀Gradient+
        • 🌀Gradient.linear()
        • 🌀Gradient + View
      • 🌀Image+
        • 🌀Image(playground:)
      • 🌀Formatter+
      • 🌀RoundedRectangle+
      • 🌀Text+
        • 🌀Text(symbol:)
    • 🔰snippets
      • 💾view + previews
      • 💾view modifier (template)
    • 🎁package
      • 📕如何自製 Swift package
      • 🏛️GeometryKit
        • 🅿️MetricSpace
        • 🅿️Vector
        • 🅿️ComplexNumber
        • 🅿️Vector2D
        • 🅿️Frame
    • 🅿️protocol
      • 🅿️Repeatable
    • 🖼️view
      • 👔HLine / VLine
      • 👔Swatch
      • 👔ShapeStyleView
    • 🎛️control
      • 🎛️SlidersForSize
      • 🎛️SliderWithLabel
  • 🐦Swift
    • 🎲math functions
      • abs()
      • .rounded()
      • .squareRoot()
      • pow()
    • 🔰scope
      • 🔸global scope
      • 🔸local scope
      • 🔸module
      • 🔹import
      • 🔸framework
        • 🧩built-in frameworks
          • 🧩Foundation
            • 📦Calendar
            • 📅Date
              • .sevenDaysOut
              • Date.from(year:month:day:)
              • Date.roundedHoursFromNow(hours)
            • 📦URL
              • 🌀 URL+ext
              • .isImage
            • 🗃️FileManager
              • 🌀 FileManager+ext
              • .documentDirectory
            • 🎁Bundle
            • 🅿️FormatStyle
          • 🧩Core Graphics
            • 🌀CGRect
              • rect.inset(by:)
            • 🌀CGSize
            • 🌀CGVector
            • 🌀CGPoint
          • 🧩SpriteKit
          • 🧩Combine
            • 🅿️Publisher
              • 🐶Future
              • 📦URLSession.DataTaskPublisher
            • ⚙️ Promise
            • 🔸Result
            • ⏰Timer
              • ✨cancellable timer
              • 👔TimerView
              • 👔TimerView (scheduledTimer)
          • 🧩AVFoundation
          • 🧩Swift Charts
          • 🧩Metal
          • 🧩WeatherKit
    • ➕operator
      • 🔸compound assignment operator
      • ✖️Character * Int
      • ✖️a^^n
      • ➕ternary conditional operator (a ? b : c)
    • 🍀type
      • 🍀type category
        • 🍀basic types
          • ✅Bool
            • .iOS14
          • ⁉️Optional
            • optional binding
            • optional chaining
            • nil coalescing
            • compare with Optional
          • 🔤String
            • 🔰String Interpolation
              • Custom String Interpolation
              • Expressible by String Interpolation
              • specifier / formatter
            • 📦AttributedString
            • 😃Emojis
            • String.Index
            • str.capitalized
            • str.split()
            • str.trim()
            • str[i], str[i..<j], str[i...j]
            • str.pad()
          • 📦Int
          • 📦Array
            • 🌀arr.split(size:)
            • 🌀arr.first()
            • arr.split(where:)
            • arr.index(of:)
            • arr.allElementsEqual
            • .allElementsSameLength
          • 📦enum
            • ⭐Comparable Enums
            • compound cases
            • 💡filter cases
            • 💡用 enum 封裝不同型別
          • 🚥tuple
          • 🔰function
            • 🔸default parameter
            • 🔸argument label
            • 🔸return value
              • #️discardable result
            • 🔰function as variable
            • 🔰fully qualified name
          • 🔰closure
            • 🔰closure expression
            • 🔰escaping closure
          • 🍄class
            • 🔰weak self
          • 🔢Numbers
            • 🅿️FloatingPoint
              • 🌀floating +−⨉÷ int
              • .decimalPlaces()
            • 🅿️BinaryFloatingPoint
              • .binaryBitPattern
              • 🖼️BinaryBitPatternView
            • 🅿️BinaryInteger
              • .binary(digits:)
            • range.contains()
          • ⚖️struct vs. class
        • 🅿️protocol
          • 🔰protocol inheritance
            • 🔰protocol hierarchy
            • 🚫can't change builtin hierarchy
          • 🔰protocol requirements
          • 🔴Protocol Extensions
          • 🔰generic protocol
            • 📘some, any
          • 🔰associated type
          • 🔴Conditional Conformance
          • 🔴Combining Protocols
          • 🔰Typecasting with Protocols
          • ⚠️retroactive conformance
          • 🅿️Any
          • 🅿️Comparable
            • .clamped(in:)
          • 🅿️Hashable
          • 🅿️Identifiable
          • 🅿️CaseIterable+ext
          • 🎬POP 介紹
          • ExpressibleByLiteral
        • ⚖️some╱any╱generics
          • 🔸some (opaque type)
          • 🔸any (boxed protocol type)
        • 🍀nested types
          • extension of nested types
          • ❓to nest or not to nest❓
        • 🍀generics
          • ⚠️generics & subtypes
          • ❗specialize generic function
          • ❓default type parameter
        • 🍀non-nominal types
      • 🔰property
        • 🔸public property
        • 🔸stored property
          • 🔸property observer (willSet/didSet)
        • 🔰computed property (get/set)
          • local computed variables
          • mutating getter / nonmutating setter
        • ⚖️stored vs. computed
        • ⚖️willSet/didSet vs. get/set
        • 🔰property wrapper
          • ✨Clamped
          • ✨Trimmed
          • ✨UnitInterval
      • 🐣inheritance
        • 🔸Self
      • 🔰type alias
      • 🔰extension
        • extension of typealias❓
      • ⭐implicit return
      • 🔰type erasure
      • 🔰initialization
        • 🔰initializers
          • 🔸required initializer
        • 🔰class initializer inheritance
          • 兩階段初始化
          • 自動繼承初始化程序
    • 🔰statement
      • 📘do
      • 🔤semicolon (;)
      • 🔰simple statement
        • 🔰expression
          • 🔰primary expressions
        • 🔰declaration
      • 🔰compiler control statement
    • 🔰flow control
      • 🔄loop
        • 📘for...in
        • 🔰for...in vs. forEach
      • 🔀branch
        • 📘if
        • 📘guard
        • 🔹switch
      • ↩️control transfer
        • 📘break
        • 📘continue
        • 📘return
        • 📘throw
        • 📘fallthrough
    • 🔰Collections
      • 🅿️Sequence
        • seq.forEach()
        • seq.reduce(_:_:)
        • seq.reduce(into:_:)
        • reduce() vs. reduce(into:)
        • seq.grouped(by:)
        • seq.sorted()
        • seq.sorted(by:)
        • seq.sorted(_:)
        • seq.foldMap(_:_:)
        • seq.stateMap(_:_:)
        • seq.sum
      • 🅿️Collection
        • collection[safe: index]
        • collection.allElementsEqual
        • collection.allElementsSameLength
        • collection.lengthsOfElements
        • collection.columnWidths
      • 🅿️IteratorProtocol
      • 🅿️OptionSet
      • Collection Types
    • 🔰Subscripts
      • Subscripts with Default Arguments
    • 🔰Attributes
      • ⭐Result Builders
        • result-building methods
        • various result types
        • result builder transform
        • custom result-builder attributes
    • 🔰Pattern Matching
      • 🔴Sentence Patterns
        • ... as! ...
        • (... as? ...)?.method()
        • if let ... as?
        • if/guard case let
        • for case let ... where
        • switch case
        • switch case is ...
        • switch case let ... where
        • switch case let ... as
        • switch on other types
      • operator (~=)
      • enum case pattern
    • 🔰Exceptions
    • 🏛️Standard Library
      • 🅿️Codable
    • 🔰Concurrency
      • 🅿️Sendable
        • 🐞has non-Sendable type
    • 🔰File System
    • 🔰Networking
    • 🐞Debugging
      • 🔮Mirror
        • Mirror.handleChildren()
      • 👔Logger
      • 👔HasMirrors
      • 👔log()
      • 👔CaseReflectable
      • 👔NonNominalTypeWrapper
      • Metatype
    • 🔰Input/Output
    • 🔰Regex
    • 🔰Access Control
  • 🔰SwiftUI
    • 🔰SwiftUI terms
      • 🔸built-in view
    • 🔰introduction
      • SwiftUI Essentials
    • 🚩app
      • 🅿️App
      • 🔰scenes
        • 🅿️Scene
      • 🔰windows
        • 📦WindowGroup
    • 🔰views
      • 🔰view hierarchy
        • 🔸container view
        • 🔸composed view
        • 🔸root view
        • 🔸child view
        • 🔸parent view
        • 🔸destination view
      • 🔰view state
        • 🔸state value
          • @State
            • 🔰init State
            • ✨dynamic underline
        • 🔸state object
          • @StateObject
        • 🔸observable object
          • 🅿️ObservableObject
          • 🔸published value
          • 🔸observer
          • @Published
        • 🔰animate state changes
        • 🔸binding
          • @Binding
            • 🔰Binding State
            • 🔰Binding from StateObject
            • 🔰init Binding
            • ❗return Binding
      • 🔰view layout
        • 🔹.padding( )
        • 🔴Position
        • 🔸frame
          • 🌀view + .frame()
          • 🔹.frame()
            • ✨TestIdealSizeView
            • ✨TestFrameView
        • 🔸view size
          • 🔴Size Classes
          • 🔴Dynamic Type Sizes
            • 🌀DynamicTypeSize+ext
          • 🔸ideal size
        • 🎁stacks
          • 📦ZStack
          • 👔AdaptiveHStack
          • 👔StackForEach
        • 🎁grids
          • 📦LazyVGrid
            • 🖼️StandardGradientGrid
          • 🟠grid layout algorithm
          • 📦GridItem
            • 🔰adaptive column
            • 🔰flexible column
            • ✨NColumns
          • 👔VGridForEach
            • ✨TestLittleSquares
          • 👔ScrollVGridForEach
            • ✨TestSVGFE
          • ✨Grids ⟩ examples
            • ✨Smileys
            • ✨Grid
              • 📦GridLayout
            • ✨MyGrid
              • ⛔Generic parameter '...' could not be inferred.
            • 🐞problem with .readSize()
            • ✨ItemsView
              • 🖼️TestItemsView
              • 📦RatioRetainingLayout
              • 🎛️SizeRatioControl
              • 🅿️IndexedGridLayout
        • Adaptive Layout
        • Alignment
          • Implicit Alignment
          • Custom Alignment within ZStack
          • 🔰各種對齊 (alignment)
        • 🌿Layout ⟩ Types
          • 📦TimelineView
        • ⛔Cannot convert value of type 'Self' to expected argument type 'Binding<C>'
      • 🔰view actions
        • 🔰delete item from list
        • 🔰add item to list
      • 🔰view environment
        • 🔸environment object
        • 🔸environment value
          • 📦dismiss
        • @Environment
          • .editMode
        • @EnvironmentObject
      • 🔰view groupings
        • 🖼️ForEach
          • 🔹.onDelete
          • ⚖️List vs. ForEach
          • ✨ForEach examples
      • 🔰navigation
        • 🔸navigation bar
        • 📦NavigationStack
        • 📦NavigationSplitView
        • 📦NavigationLink
      • 🔰view modifier
        • ✨examples
          • .nsfw()
          • .roundedBorder()
          • .neumorphic()
        • 🔰add modifiers to Xcode
        • 🔰conditional modifier
        • 🔰layout modifier
        • 🔰rendering modifier
        • 🔰adjusting text
        • 🅿️ViewModifier
      • 🔰drawing views
        • 🔸foreground
          • 🔸foreground element
          • 🔸foreground style
            • 🔹view.foregroundStyle()
            • 🔹ShapeStyle.foreground
        • 🔸background
          • 🔸background style
          • 🔹.background()
        • 🔸blend mode
          • ✨.destinationOut
        • 🔸mask
          • 🔹view.mask()
        • 🔸shadow
          • 🔰container + shadow
          • 📦ShadowStyle
          • 🔹view.shadow()
        • 🔹view.clipShape()
          • 🐞mismatching types
        • 🔹veiw.compositingGroup()
      • 🔰measuring views
        • 📦GeometryProxy
          • 🌀GeometryProxy+ext
        • 📦 GeometryReader
          • 🔰GeometryReader 的對齊方式
      • 🔰Configuring Views
      • 🔰events
        • 🔹.allowsHitTesting()
      • 📦ViewBuilder
        • ViewBuilder transforms
        • support for stored properties
      • 🔰Optional Views
      • 🅿️View
        • .dimension()
          • 🌀Path+
          • 👔DimensionPositions
          • 👔WidthDimension
          • 👔HeightDimension
          • 👔LeftArrow
          • 👔LeftArrowShape
          • 👔UpArrow
          • 👔UpArrowShape
        • id(_:)
        • .badge()
        • .border(), .borderLeft() ...
        • .hideIf()
        • .logType()
        • .getSize()
        • .grids()
        • .offset()
        • .onChangeSize()
        • .onDrag
        • ❌.readSize()
        • resizableFont()
        • .show(if:)
        • .shadowedBorder()
        • .testFrame()
        • view.actOnSelfWidth()
      • 📦Link
      • 🔰Swipe Actions
      • 🔰3D transform
      • ⭐Live Activites
      • 🔰life cycle
    • 🔰styles
      • 🔸current style
    • 🔰lists
      • 📦List
        • 🅿️ListStyle
        • ⭐list background color
    • 🎛️controls
      • 🔰pickers
        • 🎛️Picker
        • 🎛️DatePicker
      • 🔰toolbar
      • 🔰FocusState
      • 📦Divider
      • 📦Text
        • 📦Font
          • 🔰custom font
        • 🔰text formats
          • 🔸no wrap
        • 🔰Markdown
        • ❓math equations❓
        • ⭐number of lines
      • 📦Image
        • 🔹image.renderingMode()
        • 🔸template image
        • 🔰SF Symbols
        • 🍄ImageRenderer
      • 📦Label
        • Label Styles
          • Label ⟩ custom styles
      • 📦GroupBox
      • 📦ControlGroup
      • 📦Section
      • 📦ScrollView
      • ⭐ShareLink
      • 🎛️Button
        • 🔰Button Roles
        • 🔰Button Styles
          • 🅿️PrimitiveButtonStyle
          • 🔰Button with Materials
          • Button ⟩ custom styles
      • 🎛️EditButton
      • 🎛️ProgressView
      • 🎛️TextField
        • TextField ⟩ formats
          • ✨.currency
        • TextField ⟩ styles
          • textField.style(_:)
      • 🎛️Toggle
        • 🔰Toggle Styles
      • 🎛️SecureField
      • 🎛️Slider
        • 🔰Slider label position
      • 🎛️Gauge
    • 🔰shapes
      • 🔰transform shape
        • 🔹shape.stroke()
          • 🌀StrokeStyle
          • ✨MarchingAnts
      • 💈正圓變成正五邊形
      • 🅿️Shape
        • 📦AnyShape
        • 🔸fill
          • ✨even-odd fill mode
          • .evenOddFill
        • 🌀Shape + ext
          • shape.outlined()
        • ✨RoundedCorners
        • ✨Helper Shapes
          • 💜LineShape
          • 💜GridLinesShape
          • 🖼️GridLines
          • 🖼️Line
          • 🖼️Pin
          • 🖼️Point
        • ✨Sports Car in Sunset
          • 💜Vehicle.CarBodyShape
          • 🖼️Vehicle
        • 👔Polygon
      • 🅿️ShapeStyle
        • 📦Color
          • 🔸color.gradient
          • 🔰Theme
        • 📦Gradient
        • 📦ImagePaint
        • ♦️.shadow()
        • .white()
        • ShapeStyle+LinearGradient
        • 📦Material
      • 🔰Matched Geometry Effect
        • ✨MGE ⟩ animation
      • 📦Path
        • path.fit()
        • path.line()
        • path.transform
      • 📦Canvas
      • 📦Anchor<T>
    • 🎁container
      • 🎁collection
      • 🎁layout
        • ⭐Grid
        • ⭐ViewThatFits
        • ⭐AnyLayout
        • ⭐Layout
      • 🎁presentation
        • ⭐bottom sheet
        • 🎁TabView
          • ✨tabs with tag
    • 🚦data flow
      • 🚥Preferences
        • 👔ViewPreference
        • 🅿️PreferenceKey
          • FirstNonNil<T>
          • AllValues<T>
          • MaxValue<T>
        • 🚥Anchor Preferences
        • ✨Preferences ⟩ examples
          • ✨views with same width
            • .reportWidth() ...
          • ✨selected button with underline
    • 🏎️animations
      • 🔰implicit vs. explicit
      • 🅿️Animatable
        • 🅿️GeometryEffect
        • 🔸Animatable Modifiers
          • ✨.tapToShake()
      • 📦Animation
      • 🔰delay
      • 🔸Animation Curves
        • 🔸Bezier Curves
        • easeInOutExp
      • ✨Animations ⟩ examples
        • ✨Spinner
        • ✨Wheel of Fortune
        • ✨Loading Indicators
      • 🔰Transitions
        • 🔰custom transitions
        • 📦AnyTransition
          • AnyTransition.combined()
        • ✨transition ⟩ scale
        • ✨combining transitions
    • 👆Gestures
      • 🔰update gesture states
      • 🔰update view states
      • 🚥GestureState
      • 👆Long Press
      • 👆Drag
        • 👔.draggable()
          • 🖼️HandlePoint
      • 🔰Combining Gestures
        • 👆.sequenced()
          • ✨long press + drag
    • 🔰Presentations
      • 🔰Launch Screen
      • 🔰External Screens
      • 🔰Refreshable Views
      • 🔰Alert
      • 🎛️Confirmation Dialog
    • 🔰Debugging in SwiftUI
    • 🎹keyboard
    • 🔰for mac
    • 🚫deprecated
      • 🎁NavigationView
        • .navigationViewStyle(_:)
  • 🎛️widgets
  • 🎁Data Structures
    • Graph
      • Tree
  • 🧠Algorithms
    • Searching
      • Binary Search
    • Hashing
    • Sorting
      • sort using key paths
      • ranking list
      • Merge Sort
    • Filtering
    • Path Finding
      • A*
  • ⛱️Swift Playgrounds
    • Playground Book
      • Structuring Content
    • 檔案結構
      • 💾Manifest.plist
    • 使用模組
    • 📚Learn to Code 2
      • 型別
        • 🔸CharacterName
        • 🍄NodeWrapper
        • 👔ItemID
        • 🅿️Learn to Code 2 ⟩ Animatable
        • 🅿️Item
        • 🍄Actor
        • 🍄Expert
        • 🍄Portal
      • 關卡
        • Variables
          • 檢查相等值
          • 計算開關數量
          • 收集總數
        • Types
          • 開啟與關閉傳送門
          • 關閉傳送門
        • 創造世界
    • 編碼社群
    • Swift Playgrounds Subscription
  • 🥁Audio / Video
    • .speak()
    • Language Tags
  • ⛔Errors
    • ⛔CompileDylibError: Failed to build XXX.swift
    • ⛔Fatal error: String index is out of bounds
    • ❌Internal error: missingPackageDescriptionModule
    • ❌Argument type 'xxx' does not conform to expected type '_FormatSpecifiable'
    • ⛔️ escaping closure captures mutating 'self' parameter
    • ❓iPad/iPhone is Busy: Fetching debug symbols for ...
    • ❓Class _PointQueue is implemented in both ...
    • ⛔Protocol '...' can only be used as a generic constraint because it has Self or associated type
    • ⛔non-nominal type `...` cannot be extended
    • ⛔cannot assign value of type 'T' to type 'T'
  • 附錄
    • 🧩Swift Package
      • Package.swift
      • version number
      • Swift Package Index
      • 3rd party packages
    • 🔰Design Patterns
      • MVC
      • MVVM
      • DIP
    • 🛠️Xcode
      • 🔰build schemes
      • 🔧Instruments
      • 🤖Compiler Directives
        • @available, #available
        • #if ... #else ... #endif
        • #file, #line, #function
      • 🎹shortcuts
      • 🔰Documentation
        • 🔰DocC
      • Source Control
        • Merge
      • Preview
        • Preview Devices
      • App Icon
      • Simulator
    • 💼Projects
    • ✅Testing System
      • ✅Swift Testing
      • ✅XCTest
        • XCTAssertEqual (==)
        • XCTUnwrap
      • Fixtures
    • Prototyping
    • 💡Tips
      • iOS version
      • Swift version
    • JSON
      • Load JSON
    • Terms
      • reachability
    • 其他
      • 待解問題❓
      • 測試用 🧪
      • 關注對象
      • 重要語錄
      • 圖例
    • 雜記 📥
      • 📦Measurement
      • Replicating Types in Swift
      • 字型專有名詞
      • State Machine
Powered by GitBook
On this page
  • ContentView
  • Profile
  • Category
  • Hikes
  • Badge
  • Parts
  • Landmarks

Was this helpful?

  1. ios.dev
  2. todo
  3. 課程筆記
  4. SwiftUI Tutorials

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())
        }
    }
}
PreviousTutorials ⟩ ModelNextTutorials ⟩ Extensions

Last updated 3 years ago

Was this helpful?

🍎
🔲
📓
📚