├── README.md ├── flighty.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── chris.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── flighty ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ └── Contents.json ├── Contents.json ├── border.colorset │ └── Contents.json ├── flightyBlue.colorset │ └── Contents.json ├── flightyLightBlue.colorset │ └── Contents.json ├── flightyPathPrimary.colorset │ └── Contents.json ├── flightyPathSecondary.colorset │ └── Contents.json ├── lightGrey.colorset │ └── Contents.json ├── reference.imageset │ ├── Contents.json │ └── IMG_8691.jpeg └── united-logo.imageset │ ├── Contents.json │ └── united-logo.png ├── ContentView.swift ├── Extensions & Utilities └── StrokedCapsuleButtonStyle.swift ├── Models └── UIModel.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── Views ├── ArrivalForecast.swift ├── BookingAndSeatDetails.swift ├── DepartureAndArrivalDetails.swift ├── FlightDetails.swift ├── GateDepartureBanner.swift ├── GoodToKnowSection.swift └── HorizontalActions.swift └── flightyApp.swift /README.md: -------------------------------------------------------------------------------- 1 | # Recreating Flighty with SwiftUI 2 | 3 | This repo is a SwiftUI playground for recreating bits of the lovely (and Apple-Design-Award-winning) iOS app, [Flighty](https://www.flightyapp.com). 4 | 5 | All code is written with the utmost respect for the Flighty crew's amazing work. 6 | 7 | https://github.com/chrisfree/flightySwiftUI/assets/604059/719da752-2299-4d88-a060-f021ad6c5b13 8 | -------------------------------------------------------------------------------- /flighty.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 835E2D6E2BD5D6D500064605 /* DepartureAndArrivalDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E2D6D2BD5D6D500064605 /* DepartureAndArrivalDetails.swift */; }; 11 | 835E2D712BD7093200064605 /* UIModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E2D702BD7093200064605 /* UIModel.swift */; }; 12 | 835E2D732BD709C600064605 /* FlightDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E2D722BD709C600064605 /* FlightDetails.swift */; }; 13 | 835E2D762BD70A1500064605 /* StrokedCapsuleButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E2D752BD70A1500064605 /* StrokedCapsuleButtonStyle.swift */; }; 14 | 835E2D782BD70A3F00064605 /* HorizontalActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E2D772BD70A3F00064605 /* HorizontalActions.swift */; }; 15 | 835E2D7A2BD70A6500064605 /* GateDepartureBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E2D792BD70A6500064605 /* GateDepartureBanner.swift */; }; 16 | 835E2D7C2BD7525A00064605 /* BookingAndSeatDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E2D7B2BD7525A00064605 /* BookingAndSeatDetails.swift */; }; 17 | 835E2D7E2BD759B800064605 /* GoodToKnowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E2D7D2BD759B800064605 /* GoodToKnowSection.swift */; }; 18 | 835E2D802BD84C1100064605 /* ArrivalForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835E2D7F2BD84C1100064605 /* ArrivalForecast.swift */; }; 19 | 83D0D38A2BC969C500D0D6AB /* flightyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D0D3892BC969C500D0D6AB /* flightyApp.swift */; }; 20 | 83D0D38C2BC969C500D0D6AB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D0D38B2BC969C500D0D6AB /* ContentView.swift */; }; 21 | 83D0D38E2BC969C600D0D6AB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 83D0D38D2BC969C600D0D6AB /* Assets.xcassets */; }; 22 | 83D0D3912BC969C600D0D6AB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 83D0D3902BC969C600D0D6AB /* Preview Assets.xcassets */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 835E2D6D2BD5D6D500064605 /* DepartureAndArrivalDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DepartureAndArrivalDetails.swift; sourceTree = ""; }; 27 | 835E2D702BD7093200064605 /* UIModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIModel.swift; sourceTree = ""; }; 28 | 835E2D722BD709C600064605 /* FlightDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlightDetails.swift; sourceTree = ""; }; 29 | 835E2D752BD70A1500064605 /* StrokedCapsuleButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokedCapsuleButtonStyle.swift; sourceTree = ""; }; 30 | 835E2D772BD70A3F00064605 /* HorizontalActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalActions.swift; sourceTree = ""; }; 31 | 835E2D792BD70A6500064605 /* GateDepartureBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GateDepartureBanner.swift; sourceTree = ""; }; 32 | 835E2D7B2BD7525A00064605 /* BookingAndSeatDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookingAndSeatDetails.swift; sourceTree = ""; }; 33 | 835E2D7D2BD759B800064605 /* GoodToKnowSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoodToKnowSection.swift; sourceTree = ""; }; 34 | 835E2D7F2BD84C1100064605 /* ArrivalForecast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrivalForecast.swift; sourceTree = ""; }; 35 | 83D0D3862BC969C500D0D6AB /* flighty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flighty.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 83D0D3892BC969C500D0D6AB /* flightyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = flightyApp.swift; sourceTree = ""; }; 37 | 83D0D38B2BC969C500D0D6AB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 38 | 83D0D38D2BC969C600D0D6AB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 39 | 83D0D3902BC969C600D0D6AB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 40 | /* End PBXFileReference section */ 41 | 42 | /* Begin PBXFrameworksBuildPhase section */ 43 | 83D0D3832BC969C500D0D6AB /* Frameworks */ = { 44 | isa = PBXFrameworksBuildPhase; 45 | buildActionMask = 2147483647; 46 | files = ( 47 | ); 48 | runOnlyForDeploymentPostprocessing = 0; 49 | }; 50 | /* End PBXFrameworksBuildPhase section */ 51 | 52 | /* Begin PBXGroup section */ 53 | 835E2D6C2BD5D6C100064605 /* Views */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | 835E2D6D2BD5D6D500064605 /* DepartureAndArrivalDetails.swift */, 57 | 835E2D722BD709C600064605 /* FlightDetails.swift */, 58 | 835E2D772BD70A3F00064605 /* HorizontalActions.swift */, 59 | 835E2D792BD70A6500064605 /* GateDepartureBanner.swift */, 60 | 835E2D7B2BD7525A00064605 /* BookingAndSeatDetails.swift */, 61 | 835E2D7D2BD759B800064605 /* GoodToKnowSection.swift */, 62 | 835E2D7F2BD84C1100064605 /* ArrivalForecast.swift */, 63 | ); 64 | path = Views; 65 | sourceTree = ""; 66 | }; 67 | 835E2D6F2BD7092000064605 /* Models */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | 835E2D702BD7093200064605 /* UIModel.swift */, 71 | ); 72 | path = Models; 73 | sourceTree = ""; 74 | }; 75 | 835E2D742BD709F300064605 /* Extensions & Utilities */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 835E2D752BD70A1500064605 /* StrokedCapsuleButtonStyle.swift */, 79 | ); 80 | path = "Extensions & Utilities"; 81 | sourceTree = ""; 82 | }; 83 | 83D0D37D2BC969C500D0D6AB = { 84 | isa = PBXGroup; 85 | children = ( 86 | 83D0D3882BC969C500D0D6AB /* flighty */, 87 | 83D0D3872BC969C500D0D6AB /* Products */, 88 | ); 89 | sourceTree = ""; 90 | }; 91 | 83D0D3872BC969C500D0D6AB /* Products */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 83D0D3862BC969C500D0D6AB /* flighty.app */, 95 | ); 96 | name = Products; 97 | sourceTree = ""; 98 | }; 99 | 83D0D3882BC969C500D0D6AB /* flighty */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 83D0D3892BC969C500D0D6AB /* flightyApp.swift */, 103 | 83D0D38B2BC969C500D0D6AB /* ContentView.swift */, 104 | 835E2D6F2BD7092000064605 /* Models */, 105 | 835E2D6C2BD5D6C100064605 /* Views */, 106 | 835E2D742BD709F300064605 /* Extensions & Utilities */, 107 | 83D0D38D2BC969C600D0D6AB /* Assets.xcassets */, 108 | 83D0D38F2BC969C600D0D6AB /* Preview Content */, 109 | ); 110 | path = flighty; 111 | sourceTree = ""; 112 | }; 113 | 83D0D38F2BC969C600D0D6AB /* Preview Content */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 83D0D3902BC969C600D0D6AB /* Preview Assets.xcassets */, 117 | ); 118 | path = "Preview Content"; 119 | sourceTree = ""; 120 | }; 121 | /* End PBXGroup section */ 122 | 123 | /* Begin PBXNativeTarget section */ 124 | 83D0D3852BC969C500D0D6AB /* flighty */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = 83D0D3942BC969C600D0D6AB /* Build configuration list for PBXNativeTarget "flighty" */; 127 | buildPhases = ( 128 | 83D0D3822BC969C500D0D6AB /* Sources */, 129 | 83D0D3832BC969C500D0D6AB /* Frameworks */, 130 | 83D0D3842BC969C500D0D6AB /* Resources */, 131 | ); 132 | buildRules = ( 133 | ); 134 | dependencies = ( 135 | ); 136 | name = flighty; 137 | productName = flighty; 138 | productReference = 83D0D3862BC969C500D0D6AB /* flighty.app */; 139 | productType = "com.apple.product-type.application"; 140 | }; 141 | /* End PBXNativeTarget section */ 142 | 143 | /* Begin PBXProject section */ 144 | 83D0D37E2BC969C500D0D6AB /* Project object */ = { 145 | isa = PBXProject; 146 | attributes = { 147 | BuildIndependentTargetsInParallel = 1; 148 | LastSwiftUpdateCheck = 1520; 149 | LastUpgradeCheck = 1520; 150 | TargetAttributes = { 151 | 83D0D3852BC969C500D0D6AB = { 152 | CreatedOnToolsVersion = 15.2; 153 | }; 154 | }; 155 | }; 156 | buildConfigurationList = 83D0D3812BC969C500D0D6AB /* Build configuration list for PBXProject "flighty" */; 157 | compatibilityVersion = "Xcode 14.0"; 158 | developmentRegion = en; 159 | hasScannedForEncodings = 0; 160 | knownRegions = ( 161 | en, 162 | Base, 163 | ); 164 | mainGroup = 83D0D37D2BC969C500D0D6AB; 165 | productRefGroup = 83D0D3872BC969C500D0D6AB /* Products */; 166 | projectDirPath = ""; 167 | projectRoot = ""; 168 | targets = ( 169 | 83D0D3852BC969C500D0D6AB /* flighty */, 170 | ); 171 | }; 172 | /* End PBXProject section */ 173 | 174 | /* Begin PBXResourcesBuildPhase section */ 175 | 83D0D3842BC969C500D0D6AB /* Resources */ = { 176 | isa = PBXResourcesBuildPhase; 177 | buildActionMask = 2147483647; 178 | files = ( 179 | 83D0D3912BC969C600D0D6AB /* Preview Assets.xcassets in Resources */, 180 | 83D0D38E2BC969C600D0D6AB /* Assets.xcassets in Resources */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXResourcesBuildPhase section */ 185 | 186 | /* Begin PBXSourcesBuildPhase section */ 187 | 83D0D3822BC969C500D0D6AB /* Sources */ = { 188 | isa = PBXSourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 835E2D802BD84C1100064605 /* ArrivalForecast.swift in Sources */, 192 | 835E2D732BD709C600064605 /* FlightDetails.swift in Sources */, 193 | 83D0D38C2BC969C500D0D6AB /* ContentView.swift in Sources */, 194 | 83D0D38A2BC969C500D0D6AB /* flightyApp.swift in Sources */, 195 | 835E2D6E2BD5D6D500064605 /* DepartureAndArrivalDetails.swift in Sources */, 196 | 835E2D762BD70A1500064605 /* StrokedCapsuleButtonStyle.swift in Sources */, 197 | 835E2D7C2BD7525A00064605 /* BookingAndSeatDetails.swift in Sources */, 198 | 835E2D7E2BD759B800064605 /* GoodToKnowSection.swift in Sources */, 199 | 835E2D782BD70A3F00064605 /* HorizontalActions.swift in Sources */, 200 | 835E2D712BD7093200064605 /* UIModel.swift in Sources */, 201 | 835E2D7A2BD70A6500064605 /* GateDepartureBanner.swift in Sources */, 202 | ); 203 | runOnlyForDeploymentPostprocessing = 0; 204 | }; 205 | /* End PBXSourcesBuildPhase section */ 206 | 207 | /* Begin XCBuildConfiguration section */ 208 | 83D0D3922BC969C600D0D6AB /* Debug */ = { 209 | isa = XCBuildConfiguration; 210 | buildSettings = { 211 | ALWAYS_SEARCH_USER_PATHS = NO; 212 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 213 | CLANG_ANALYZER_NONNULL = YES; 214 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 215 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 216 | CLANG_ENABLE_MODULES = YES; 217 | CLANG_ENABLE_OBJC_ARC = YES; 218 | CLANG_ENABLE_OBJC_WEAK = YES; 219 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 220 | CLANG_WARN_BOOL_CONVERSION = YES; 221 | CLANG_WARN_COMMA = YES; 222 | CLANG_WARN_CONSTANT_CONVERSION = YES; 223 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 224 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 225 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 226 | CLANG_WARN_EMPTY_BODY = YES; 227 | CLANG_WARN_ENUM_CONVERSION = YES; 228 | CLANG_WARN_INFINITE_RECURSION = YES; 229 | CLANG_WARN_INT_CONVERSION = YES; 230 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 231 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 232 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 233 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 234 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 235 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 236 | CLANG_WARN_STRICT_PROTOTYPES = YES; 237 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 238 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 239 | CLANG_WARN_UNREACHABLE_CODE = YES; 240 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 241 | COPY_PHASE_STRIP = NO; 242 | DEBUG_INFORMATION_FORMAT = dwarf; 243 | ENABLE_STRICT_OBJC_MSGSEND = YES; 244 | ENABLE_TESTABILITY = YES; 245 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 246 | GCC_C_LANGUAGE_STANDARD = gnu17; 247 | GCC_DYNAMIC_NO_PIC = NO; 248 | GCC_NO_COMMON_BLOCKS = YES; 249 | GCC_OPTIMIZATION_LEVEL = 0; 250 | GCC_PREPROCESSOR_DEFINITIONS = ( 251 | "DEBUG=1", 252 | "$(inherited)", 253 | ); 254 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 255 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 256 | GCC_WARN_UNDECLARED_SELECTOR = YES; 257 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 258 | GCC_WARN_UNUSED_FUNCTION = YES; 259 | GCC_WARN_UNUSED_VARIABLE = YES; 260 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 261 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 262 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 263 | MTL_FAST_MATH = YES; 264 | ONLY_ACTIVE_ARCH = YES; 265 | SDKROOT = iphoneos; 266 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 267 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 268 | }; 269 | name = Debug; 270 | }; 271 | 83D0D3932BC969C600D0D6AB /* Release */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | ALWAYS_SEARCH_USER_PATHS = NO; 275 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 276 | CLANG_ANALYZER_NONNULL = YES; 277 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 278 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 279 | CLANG_ENABLE_MODULES = YES; 280 | CLANG_ENABLE_OBJC_ARC = YES; 281 | CLANG_ENABLE_OBJC_WEAK = YES; 282 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 283 | CLANG_WARN_BOOL_CONVERSION = YES; 284 | CLANG_WARN_COMMA = YES; 285 | CLANG_WARN_CONSTANT_CONVERSION = YES; 286 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 287 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 288 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 289 | CLANG_WARN_EMPTY_BODY = YES; 290 | CLANG_WARN_ENUM_CONVERSION = YES; 291 | CLANG_WARN_INFINITE_RECURSION = YES; 292 | CLANG_WARN_INT_CONVERSION = YES; 293 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 294 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 295 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 296 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 297 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 298 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 299 | CLANG_WARN_STRICT_PROTOTYPES = YES; 300 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 301 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 302 | CLANG_WARN_UNREACHABLE_CODE = YES; 303 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 304 | COPY_PHASE_STRIP = NO; 305 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 306 | ENABLE_NS_ASSERTIONS = NO; 307 | ENABLE_STRICT_OBJC_MSGSEND = YES; 308 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 309 | GCC_C_LANGUAGE_STANDARD = gnu17; 310 | GCC_NO_COMMON_BLOCKS = YES; 311 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 312 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 313 | GCC_WARN_UNDECLARED_SELECTOR = YES; 314 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 315 | GCC_WARN_UNUSED_FUNCTION = YES; 316 | GCC_WARN_UNUSED_VARIABLE = YES; 317 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 318 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 319 | MTL_ENABLE_DEBUG_INFO = NO; 320 | MTL_FAST_MATH = YES; 321 | SDKROOT = iphoneos; 322 | SWIFT_COMPILATION_MODE = wholemodule; 323 | VALIDATE_PRODUCT = YES; 324 | }; 325 | name = Release; 326 | }; 327 | 83D0D3952BC969C600D0D6AB /* Debug */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 331 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 332 | CODE_SIGN_STYLE = Automatic; 333 | CURRENT_PROJECT_VERSION = 1; 334 | DEVELOPMENT_ASSET_PATHS = "\"flighty/Preview Content\""; 335 | DEVELOPMENT_TEAM = 5KZHQYWR66; 336 | ENABLE_PREVIEWS = YES; 337 | GENERATE_INFOPLIST_FILE = YES; 338 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 339 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 340 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 341 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 342 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 343 | LD_RUNPATH_SEARCH_PATHS = ( 344 | "$(inherited)", 345 | "@executable_path/Frameworks", 346 | ); 347 | MARKETING_VERSION = 1.0; 348 | PRODUCT_BUNDLE_IDENTIFIER = ChrisFree.flighty; 349 | PRODUCT_NAME = "$(TARGET_NAME)"; 350 | SWIFT_EMIT_LOC_STRINGS = YES; 351 | SWIFT_VERSION = 5.0; 352 | TARGETED_DEVICE_FAMILY = "1,2"; 353 | }; 354 | name = Debug; 355 | }; 356 | 83D0D3962BC969C600D0D6AB /* Release */ = { 357 | isa = XCBuildConfiguration; 358 | buildSettings = { 359 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 360 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 361 | CODE_SIGN_STYLE = Automatic; 362 | CURRENT_PROJECT_VERSION = 1; 363 | DEVELOPMENT_ASSET_PATHS = "\"flighty/Preview Content\""; 364 | DEVELOPMENT_TEAM = 5KZHQYWR66; 365 | ENABLE_PREVIEWS = YES; 366 | GENERATE_INFOPLIST_FILE = YES; 367 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 368 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 369 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 370 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 371 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 372 | LD_RUNPATH_SEARCH_PATHS = ( 373 | "$(inherited)", 374 | "@executable_path/Frameworks", 375 | ); 376 | MARKETING_VERSION = 1.0; 377 | PRODUCT_BUNDLE_IDENTIFIER = ChrisFree.flighty; 378 | PRODUCT_NAME = "$(TARGET_NAME)"; 379 | SWIFT_EMIT_LOC_STRINGS = YES; 380 | SWIFT_VERSION = 5.0; 381 | TARGETED_DEVICE_FAMILY = "1,2"; 382 | }; 383 | name = Release; 384 | }; 385 | /* End XCBuildConfiguration section */ 386 | 387 | /* Begin XCConfigurationList section */ 388 | 83D0D3812BC969C500D0D6AB /* Build configuration list for PBXProject "flighty" */ = { 389 | isa = XCConfigurationList; 390 | buildConfigurations = ( 391 | 83D0D3922BC969C600D0D6AB /* Debug */, 392 | 83D0D3932BC969C600D0D6AB /* Release */, 393 | ); 394 | defaultConfigurationIsVisible = 0; 395 | defaultConfigurationName = Release; 396 | }; 397 | 83D0D3942BC969C600D0D6AB /* Build configuration list for PBXNativeTarget "flighty" */ = { 398 | isa = XCConfigurationList; 399 | buildConfigurations = ( 400 | 83D0D3952BC969C600D0D6AB /* Debug */, 401 | 83D0D3962BC969C600D0D6AB /* Release */, 402 | ); 403 | defaultConfigurationIsVisible = 0; 404 | defaultConfigurationName = Release; 405 | }; 406 | /* End XCConfigurationList section */ 407 | }; 408 | rootObject = 83D0D37E2BC969C500D0D6AB /* Project object */; 409 | } 410 | -------------------------------------------------------------------------------- /flighty.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flighty.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flighty.xcodeproj/xcuserdata/chris.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | flighty.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/border.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "235", 9 | "green" : "235", 10 | "red" : "235" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "67", 27 | "green" : "65", 28 | "red" : "66" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/flightyBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "251", 9 | "green" : "121", 10 | "red" : "69" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "251", 27 | "green" : "121", 28 | "red" : "69" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/flightyLightBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "254", 9 | "green" : "149", 10 | "red" : "94" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "254", 27 | "green" : "149", 28 | "red" : "94" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/flightyPathPrimary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "253", 9 | "green" : "194", 10 | "red" : "165" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "253", 27 | "green" : "194", 28 | "red" : "165" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/flightyPathSecondary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "240", 9 | "green" : "148", 10 | "red" : "107" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "240", 27 | "green" : "148", 28 | "red" : "107" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/lightGrey.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "200", 9 | "green" : "200", 10 | "red" : "200" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "67", 27 | "green" : "67", 28 | "red" : "67" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/reference.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "IMG_8691.jpeg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/reference.imageset/IMG_8691.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisfree/flightySwiftUI/6a8edabb7a1bc1dd5abb8429c649794a203100bc/flighty/Assets.xcassets/reference.imageset/IMG_8691.jpeg -------------------------------------------------------------------------------- /flighty/Assets.xcassets/united-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "United-Logo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /flighty/Assets.xcassets/united-logo.imageset/united-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisfree/flightySwiftUI/6a8edabb7a1bc1dd5abb8429c649794a203100bc/flighty/Assets.xcassets/united-logo.imageset/united-logo.png -------------------------------------------------------------------------------- /flighty/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // flighty 4 | // 5 | // Created by Christopher Free on 4/12/24. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | struct ContentView: View { 12 | @StateObject var uiModel = UIModel() 13 | @State private var sheetPresented: Bool = true 14 | @State private var referenceOpacity = 0.0 15 | @State private var camera: MapCameraPosition = .camera(MapCamera(centerCoordinate: CLLocationCoordinate2D(latitude: 35.44722362595532, longitude: -86.03197387024086), distance: 5_000_000)) 16 | 17 | let locations: [AirportAnnotation] = [ 18 | AirportAnnotation(code: "ATL", name: "Atlanta", coordinate: .atl), 19 | AirportAnnotation(code: "ORD", name: "Chicago", coordinate: .ord) 20 | ] 21 | 22 | let flightRoute: [CLLocationCoordinate2D] = [.atl, .midPoint, .ord] 23 | 24 | var body: some View { 25 | ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)) { 26 | Map(position: $camera) { 27 | ForEach(locations) { location in 28 | Annotation("\(location.name)", coordinate: location.coordinate, anchor: .leading) { 29 | HStack(spacing: 3) { 30 | Circle() 31 | .fill(.flightyBlue) 32 | .stroke(.white, lineWidth: 2) 33 | .frame(height: 12) 34 | 35 | HStack(spacing: 0) { 36 | Text(location.code) 37 | .font(.system(size: 10).weight(.semibold)) 38 | .foregroundStyle(.white) 39 | .padding(.leading, 4) 40 | .padding(.trailing, 3) 41 | .frame(maxHeight: .infinity, alignment: .center) 42 | .background(.flightyBlue) 43 | 44 | Text(location.name) 45 | .font(.system(size: 12)) 46 | .foregroundStyle(.white) 47 | .padding(.vertical, 3) 48 | .padding(.leading, 3) 49 | .padding(.trailing, 4) 50 | .background(.flightyLightBlue) 51 | } 52 | .clipShape(RoundedRectangle(cornerRadius: 5.0)) 53 | .fixedSize() 54 | } 55 | .offset(x: -5) 56 | } 57 | 58 | MapPolyline(coordinates: flightRoute) 59 | .stroke(.flightyPathSecondary, lineWidth: 5) 60 | MapPolyline(coordinates: flightRoute) 61 | .stroke(.flightyPathPrimary, lineWidth: 2) 62 | } 63 | } 64 | .ignoresSafeArea() 65 | 66 | VStack(spacing: 10) { 67 | Image(systemName: "map.fill") 68 | Divider() 69 | .frame(maxWidth: 30) 70 | Image(systemName: "cloud.fill") 71 | } 72 | .padding(.horizontal, 6) 73 | .padding(.vertical, 12) 74 | .background( 75 | Capsule() 76 | .fill(.thickMaterial) 77 | ) 78 | .padding(.top, 70) 79 | .padding(.trailing, 20) 80 | .ignoresSafeArea() 81 | .sheet(isPresented: $sheetPresented) { 82 | FlightDetails(sheetPresented: $sheetPresented) 83 | .presentationDetents([.height(200), .medium, .fraction(0.95)], selection: $uiModel.selectedDetent) 84 | .presentationBackgroundInteraction( 85 | .enabled(upThrough: .medium) 86 | ) 87 | .presentationDragIndicator(.hidden) 88 | .presentationCornerRadius(21) 89 | .interactiveDismissDisabled() 90 | } 91 | 92 | Image("reference") 93 | .resizable() 94 | .aspectRatio(contentMode: .fill) 95 | .ignoresSafeArea() 96 | .opacity(referenceOpacity) 97 | .onTapGesture { 98 | referenceOpacity = 0.0 99 | } 100 | } 101 | .background( 102 | Rectangle() 103 | .fill(.red) 104 | .frame(maxWidth: .infinity, maxHeight: .infinity) 105 | ) 106 | .environmentObject(uiModel) 107 | } 108 | } 109 | 110 | #Preview { 111 | ContentView() 112 | } 113 | 114 | // A simple preference key for bubbling up the scrollview's offset so that the 115 | // header can dynamically set it's bottom border opacity as the user scrolls. 116 | struct ViewOffsetKey: PreferenceKey { 117 | typealias Value = CGFloat 118 | static var defaultValue = CGFloat.zero 119 | static func reduce(value: inout Value, nextValue: () -> Value) { 120 | value += nextValue() 121 | } 122 | } 123 | 124 | struct AirportAnnotation: Identifiable { 125 | let id = UUID() 126 | let code: String 127 | let name: String 128 | let coordinate: CLLocationCoordinate2D 129 | } 130 | 131 | extension CLLocationCoordinate2D { 132 | static let atl = CLLocationCoordinate2D(latitude: 33.640411, longitude: -84.419853) 133 | static let ord = CLLocationCoordinate2D(latitude: 41.978611, longitude: -87.904724) 134 | static let midPoint = CLLocationCoordinate2D(latitude: 39.06903755242377, longitude: -86.79896452319443) 135 | static let overLake = CLLocationCoordinate2D(latitude: 42.03559217412583, longitude: -86.82568547736001) 136 | } 137 | -------------------------------------------------------------------------------- /flighty/Extensions & Utilities/StrokedCapsuleButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StrokedCapsuleButtonStyle.swift 3 | // flighty 4 | // 5 | // Created by Christopher Free on 4/22/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // A simple button style that adds a subtle border to otherwise plain styled buttons. 11 | struct StrokedCapsule: ButtonStyle { 12 | func makeBody(configuration: Configuration) -> some View { 13 | configuration.label 14 | .padding(.vertical, 10) 15 | .padding(.horizontal, 15) 16 | .foregroundStyle(.primary) 17 | .background( 18 | Capsule() 19 | .stroke(.tertiary, lineWidth: 1) 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /flighty/Models/UIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIModel.swift 3 | // flighty 4 | // 5 | // Created by Christopher Free on 4/22/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // Simple model for synchronizing the state of the UI across the app. 11 | class UIModel: ObservableObject { 12 | @Published var selectedDetent: PresentationDetent = .height(200) 13 | } 14 | -------------------------------------------------------------------------------- /flighty/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /flighty/Views/ArrivalForecast.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrivalForecast.swift 3 | // flighty 4 | // 5 | // Created by Christopher Free on 4/23/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ArrivalForecast: View { 11 | 12 | var body: some View { 13 | // Arrival forecast. 14 | VStack(alignment: .leading, spacing: 7) { 15 | VStack(alignment: .leading) { 16 | Text("Arrival Forecast") 17 | .font(.title2.weight(.semibold)) 18 | .frame(maxWidth: .infinity, alignment: .leading) 19 | Text("UA 2534 performance over the last 60 days") 20 | .font(.footnote.weight(.semibold)) 21 | .foregroundStyle(.secondary) 22 | .frame(maxWidth: .infinity, alignment: .leading) 23 | } 24 | .padding(.horizontal, 5) 25 | 26 | HStack { 27 | // Late percentage. 28 | VStack(alignment: .leading, spacing: 4) { 29 | Text("Late") 30 | .font(.system(size: 14).weight(.medium)) 31 | .foregroundStyle(.secondary) 32 | HStack(spacing: 3.5) { 33 | Image(systemName: "clock") 34 | .fontWeight(.semibold) 35 | Text("27%") 36 | .font(.system(size: 15)) 37 | } 38 | } 39 | Spacer() 40 | // Average of Late timing. 41 | VStack(alignment: .leading, spacing: 4) { 42 | Text("Average of Late") 43 | .font(.system(size: 14).weight(.medium)) 44 | .foregroundStyle(.secondary) 45 | HStack(spacing: 3.5) { 46 | Image(systemName: "stopwatch.fill") 47 | .fontWeight(.semibold) 48 | Text("12m") 49 | .font(.system(size: 15)) 50 | } 51 | } 52 | Spacer() 53 | // Late percentage. 54 | VStack(alignment: .leading, spacing: 4) { 55 | Text("Observed") 56 | .font(.system(size: 14).weight(.medium)) 57 | .foregroundStyle(.secondary) 58 | HStack(spacing: 3.5) { 59 | Image(systemName: "airplane.circle") 60 | .fontWeight(.semibold) 61 | Text("11") 62 | .font(.system(size: 15)) 63 | } 64 | } 65 | } 66 | .padding(.top, 7) 67 | .padding(.horizontal, 5) 68 | 69 | Grid(alignment: .leading) { 70 | ForEach(ArrivalData.generateSampleData()) { arrivalData in 71 | GridRow { 72 | Text("\(arrivalData.name)") 73 | GeometryReader { geometry in 74 | ZStack(alignment: .leading) { 75 | RoundedRectangle(cornerRadius: 5, style: .continuous) 76 | .fill(.tertiary.opacity(0.35)) 77 | 78 | RoundedRectangle(cornerRadius: 5, style: .continuous) 79 | .fill(arrivalData.arrivalStatusColor) 80 | .frame(width: geometry.size.width * arrivalData.value) 81 | } 82 | } 83 | Text("\(arrivalData.value.formatted(.percent.precision(.fractionLength(0))))") 84 | .gridColumnAlignment(.trailing) 85 | } 86 | } 87 | } 88 | .font(.footnote) 89 | .padding(.top, 20) 90 | 91 | } 92 | .padding(.vertical, 15) 93 | .padding(.horizontal, 15) 94 | .overlay( 95 | RoundedRectangle(cornerRadius: 20, style: .continuous) 96 | .stroke(.border, lineWidth: 1) 97 | ) 98 | .padding(.horizontal, 15) 99 | } 100 | } 101 | 102 | struct ArrivalData: Identifiable { 103 | let id = UUID() 104 | let name: String 105 | let value: Double 106 | let arrivalStatusColor: Color 107 | 108 | static func generateSampleData() -> [ArrivalData] { 109 | var sampleData = [ArrivalData]() 110 | for status in FlightStatus.allCases { 111 | sampleData.append(ArrivalData(name: status.description, value: Double.random(in: 0.0...0.4), arrivalStatusColor: status.color)) 112 | } 113 | return sampleData 114 | } 115 | } 116 | 117 | enum FlightStatus: CaseIterable { 118 | case early 119 | case onTime 120 | case fifteenMinLate 121 | case thirtyMinLate 122 | case fortyFiveMinLate 123 | case cancelled 124 | case diverted 125 | 126 | var description: String { 127 | switch self { 128 | case .early: return "Early" 129 | case .onTime: return "On Time" 130 | case .fifteenMinLate: return "15m late" 131 | case .thirtyMinLate: return "30m late" 132 | case .fortyFiveMinLate: return "45m+ late" 133 | case .cancelled: return "Canceled" 134 | case .diverted: return "Diverted" 135 | } 136 | } 137 | 138 | var color: Color { 139 | switch self { 140 | case .early: return .green 141 | case .onTime: return .green.opacity(0.80) 142 | case .fifteenMinLate: return .yellow 143 | case .thirtyMinLate: return .orange 144 | case .fortyFiveMinLate: return .red 145 | case .cancelled: return .red 146 | case .diverted: return .gray 147 | } 148 | } 149 | } 150 | 151 | #Preview { 152 | ArrivalForecast() 153 | } 154 | -------------------------------------------------------------------------------- /flighty/Views/BookingAndSeatDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookingAndSeatDetails.swift 3 | // flighty 4 | // 5 | // Created by Christopher Free on 4/22/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BookingAndSeatDetails: View { 11 | var body: some View { 12 | HStack(spacing: 20) { 13 | VStack(alignment: .leading) { 14 | HStack { 15 | Image(systemName: "ticket.fill") 16 | .frame(maxWidth: .infinity, alignment: .leading) 17 | Button("Paste") { } 18 | .tint(.blue) 19 | .textCase(.uppercase) 20 | .font(.caption2) 21 | .buttonStyle(.bordered) 22 | .buttonBorderShape(.roundedRectangle(radius: 10)) 23 | } 24 | Text("Booking Code") 25 | .fontWeight(.bold) 26 | Text("Tap to Edit") 27 | .font(.footnote) 28 | .foregroundStyle(.secondary) 29 | } 30 | .padding() 31 | .overlay { 32 | RoundedRectangle(cornerRadius: 20, style: .continuous) 33 | .stroke(.border, lineWidth: 1) 34 | } 35 | 36 | VStack(alignment: .leading) { 37 | Image(systemName: "carseat.right.fill") 38 | // Ensures this HStack is the same height as the preceding. 39 | .frame(maxHeight: .infinity) 40 | Text("Seat") 41 | .fontWeight(.bold) 42 | .frame(minWidth: 130, alignment: .leading) 43 | Text("Tap to Edit") 44 | .font(.footnote) 45 | .foregroundStyle(.secondary) 46 | } 47 | .padding() 48 | .overlay { 49 | RoundedRectangle(cornerRadius: 20, style: .continuous) 50 | .stroke(.border, lineWidth: 1) 51 | } 52 | } 53 | .padding(.horizontal, 15) 54 | } 55 | } 56 | 57 | #Preview { 58 | BookingAndSeatDetails() 59 | } 60 | -------------------------------------------------------------------------------- /flighty/Views/DepartureAndArrivalDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView.swift 3 | // flighty 4 | // 5 | // Created by Christopher Free on 4/21/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DepartureAndArrivalDetails: View { 11 | @State private var sectionHeight: CGFloat = 0 12 | @State private var departurePosition: CGFloat = 0 13 | @State private var arrivalPosition: CGFloat = 0 14 | @State private var departureIcon: CGFloat = 0 15 | @State private var arrivalIcon: CGFloat = 0 16 | 17 | var body: some View { 18 | HStack(alignment: .top) { 19 | // Departure/Arrival Arrows 20 | ZStack { 21 | Image(systemName: "arrow.up.right.circle.fill") 22 | .foregroundStyle(.background, .green) 23 | .background( 24 | Circle() 25 | .fill(.background) 26 | ) 27 | .offset(y: departurePosition - 10) 28 | 29 | Image(systemName: "arrow.down.right.circle.fill") 30 | .foregroundStyle(.background, .green) 31 | .background( 32 | Circle() 33 | .fill(.background) 34 | ) 35 | .offset(y: arrivalPosition - 10) 36 | } 37 | .fontWeight(.bold) 38 | .background( 39 | Rectangle() 40 | .frame(width: 0.5, alignment: .center) 41 | .frame(height: arrivalPosition - 10 - 10) 42 | .offset(y: departurePosition) 43 | .foregroundStyle(.primary.opacity(0.25)), 44 | alignment: .top 45 | ) 46 | 47 | VStack { 48 | // Departure details. 49 | HStack { 50 | Text("ATL") 51 | .font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/.weight(.bold)) 52 | .overlay( 53 | Rectangle() 54 | .fill(.clear) 55 | .frame(height: 0.5) 56 | .overlay( 57 | GeometryReader { proxy in 58 | Color.clear.preference(key: TextCenter.self, value: proxy.frame(in: .named("DepartureAndArrivalDetailsSection")).midY) 59 | } 60 | ) 61 | .onPreferenceChange(TextCenter.self) { offset in 62 | departurePosition = offset 63 | } 64 | ) 65 | Spacer() 66 | Text("8:20 pm") 67 | .font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/.weight(.semibold)) 68 | .textCase(.uppercase) 69 | .foregroundStyle(.green) 70 | } 71 | 72 | Group { 73 | HStack { 74 | Text("Hartsfield Jackson Atlanta Intl") 75 | .font(.caption.weight(.regular)) 76 | Spacer() 77 | Text("Scheduled") 78 | .font(.caption.weight(.bold)) 79 | } 80 | 81 | HStack { 82 | Text("Terminal 3 • Gate 4B") 83 | .font(.caption.weight(.bold)) 84 | Spacer() 85 | Text("in 1h 16m") 86 | .font(.caption.weight(.regular)) 87 | } 88 | } 89 | .foregroundStyle(.secondary) 90 | 91 | ZStack { 92 | Rectangle().frame(height: 0.5).opacity(0.15) 93 | Text("Total 2h 6m • 606 mi") 94 | .font(.caption.weight(.regular)) 95 | .foregroundStyle(.secondary) 96 | .padding(.vertical, 10) 97 | .padding(.horizontal, 3) 98 | .background(.background) 99 | } 100 | 101 | VStack { 102 | // Arrival details. 103 | HStack { 104 | Text("ORD") 105 | .font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/.weight(.bold)) 106 | .overlay( 107 | Rectangle() 108 | .fill(.clear) 109 | .frame(height: 0.5) 110 | .overlay( 111 | GeometryReader { proxy in 112 | Color.clear.preference(key: TextCenter.self, value: proxy.frame(in: .named("DepartureAndArrivalDetailsSection")).midY) 113 | } 114 | ) 115 | .onPreferenceChange(TextCenter.self) { offset in 116 | arrivalPosition = offset 117 | } 118 | ) 119 | 120 | Spacer() 121 | Text("9:26 pm") 122 | .font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/.weight(.semibold)) 123 | .textCase(.uppercase) 124 | .foregroundStyle(.green) 125 | } 126 | 127 | Group { 128 | HStack { 129 | Text("Chicago O'Hare Intl") 130 | .font(.caption.weight(.regular)) 131 | Spacer() 132 | Text("Scheduled") 133 | .font(.caption.weight(.bold)) 134 | } 135 | 136 | HStack { 137 | Text("Terminal 3 • Gate 4B") 138 | .font(.caption.weight(.bold)) 139 | Spacer() 140 | Text("in 1h 16m") 141 | .font(.caption.weight(.regular)) 142 | } 143 | 144 | Text("Baggage Belt: 8") 145 | .font(.caption.weight(.regular)) 146 | .frame(maxWidth: .infinity, alignment: .leading) 147 | } 148 | .foregroundStyle(.secondary) 149 | } 150 | 151 | } 152 | .padding(.leading, 10) 153 | } 154 | .frame(maxWidth: .infinity) 155 | .overlay( 156 | GeometryReader { proxy in 157 | Color.clear.preference(key: SectionHeight.self, value: proxy.frame(in: .named("DepartureAndArrivalDetailsSection")).midY) 158 | } 159 | ) 160 | .onPreferenceChange(SectionHeight.self) { value in 161 | sectionHeight = value 162 | } 163 | .coordinateSpace(name: "DepartureAndArrivalDetailsSection") 164 | .padding(.leading, 10) 165 | .padding(.trailing) 166 | .padding(.bottom, 20) 167 | } 168 | } 169 | 170 | #Preview { 171 | DepartureAndArrivalDetails() 172 | } 173 | 174 | 175 | struct TextCenter: PreferenceKey { 176 | typealias Value = CGFloat 177 | 178 | static var defaultValue = CGFloat.zero 179 | 180 | static func reduce(value: inout Value, nextValue: () -> Value) { 181 | value = nextValue() 182 | } 183 | } 184 | 185 | struct SectionHeight: PreferenceKey { 186 | typealias Value = CGFloat 187 | 188 | static var defaultValue = CGFloat.zero 189 | 190 | static func reduce(value: inout Value, nextValue: () -> Value) { 191 | value = nextValue() 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /flighty/Views/FlightDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlightDetails.swift 3 | // flighty 4 | // 5 | // Created by Christopher Free on 4/22/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FlightDetails: View { 11 | @EnvironmentObject var uiModel: UIModel 12 | @State private var previousScrollOffset: CGFloat = 0 13 | @State private var closeOpacity: Double = 0 14 | @Binding var sheetPresented: Bool 15 | let minimumOffset: CGFloat = 5 16 | 17 | var body: some View { 18 | ScrollView { 19 | LazyVStack(spacing: 10, pinnedViews: [.sectionHeaders]) { 20 | Section { 21 | HorizontalActions() 22 | 23 | GateDepartureBanner() 24 | 25 | DepartureAndArrivalDetails() 26 | 27 | BookingAndSeatDetails() 28 | 29 | GoodToKnowSection() 30 | 31 | ArrivalForecast() 32 | } header: { 33 | flightDetailsHeader 34 | } 35 | } 36 | .background( 37 | GeometryReader { proxy in 38 | Color.clear.preference(key: ViewOffsetKey.self, value: -proxy.frame(in: .named("container")).origin.y) 39 | }) 40 | .onPreferenceChange(ViewOffsetKey.self) { currentOffset in 41 | let offsetDifference: CGFloat = self.previousScrollOffset - currentOffset 42 | if (abs(offsetDifference) > minimumOffset) { 43 | self.previousScrollOffset = currentOffset 44 | } 45 | } 46 | } 47 | .scrollIndicators(.hidden) 48 | .coordinateSpace(name: "container") 49 | } 50 | 51 | fileprivate var flightDetailsHeader: some View { 52 | ZStack { 53 | HStack(spacing: 15) { 54 | Image("united-logo") 55 | .resizable() 56 | .aspectRatio(contentMode: .fit) 57 | .background(.white) 58 | .frame(width: 40) 59 | VStack(alignment: .leading) { 60 | HStack { 61 | Text("DL 2534 • Fri, 12") 62 | .foregroundStyle(.secondary) 63 | .font(.caption.weight(.medium)) 64 | .textCase(.uppercase) 65 | .frame(maxWidth: .infinity, alignment: .leading) 66 | } 67 | Text("Atlanta to Chicago") 68 | .font(.title2.weight(.semibold)) 69 | .fontDesign(.rounded) 70 | .frame(maxWidth: .infinity, alignment: .leading) 71 | } 72 | .padding(.top, 5) 73 | } 74 | Button { 75 | uiModel.selectedDetent = .height(200) 76 | } label: { 77 | Image(systemName: "xmark.circle.fill") 78 | .symbolRenderingMode(.hierarchical) 79 | .font(.title) 80 | .opacity(0.5) 81 | .frame(maxWidth: .infinity, alignment: .trailing) 82 | .offset(x: 5, y: -15) 83 | .opacity(closeOpacity) 84 | } 85 | .buttonStyle(.plain) 86 | } 87 | .padding(.horizontal, 20) 88 | .padding(.top, 17) 89 | .padding(.bottom, 10) 90 | .background(Rectangle().fill(.background)) 91 | .overlay( 92 | Rectangle() 93 | .frame(width: nil, height: 0.5, alignment: .top) 94 | .foregroundColor(.lightGrey.opacity((previousScrollOffset - minimumOffset) / Double(10))), 95 | alignment: .bottom) 96 | .onChange(of: uiModel.selectedDetent) { 97 | withAnimation(.easeOut(duration: 0.2)) { 98 | closeOpacity = uiModel.selectedDetent == .height(200) ? 0 : 1 99 | } 100 | } 101 | } 102 | } 103 | 104 | #Preview { 105 | FlightDetails(sheetPresented: .constant(true)) 106 | } 107 | 108 | extension View { 109 | func hidden(_ shouldHide: Bool) -> some View { 110 | opacity(shouldHide ? 0 : 1) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /flighty/Views/GateDepartureBanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GateDepartureBanner.swift 3 | // flighty 4 | // 5 | // Created by Christopher Free on 4/22/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // The banner that displays the gate, relative departure time, and status. 11 | struct GateDepartureBanner: View { 12 | // This should be stateful, with a property that indicates whether a flight 13 | // is on time, late, cancelled, etc. 14 | var body: some View { 15 | HStack { 16 | Text("Gate Departure in 1h 16m") 17 | .foregroundStyle(.green) 18 | .fontWeight(.semibold) 19 | .frame(maxWidth: .infinity, alignment: .leading) 20 | Label("B1A", systemImage: "figure.walk.circle.fill") 21 | .foregroundStyle(.black) 22 | .fontWeight(.semibold) 23 | .padding(5) 24 | .background( 25 | RoundedRectangle(cornerRadius: 5) 26 | .fill(.yellow) 27 | ) 28 | } 29 | .padding(.vertical, 13) 30 | .padding(.horizontal, 15) 31 | .background( 32 | Rectangle() 33 | .fill(.green.opacity(0.05)) 34 | ) 35 | .overlay(Rectangle().frame(width: nil, height: 0.25, alignment: .top).foregroundColor(.green.opacity(0.5)), alignment: .top) 36 | .overlay(Rectangle().frame(width: nil, height: 0.5, alignment: .top).foregroundColor(.green.opacity(0.5)), alignment: .bottom) 37 | } 38 | } 39 | 40 | #Preview { 41 | GateDepartureBanner() 42 | } 43 | -------------------------------------------------------------------------------- /flighty/Views/GoodToKnowSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GoodToKnowSection.swift 3 | // flighty 4 | // 5 | // Created by Christopher Free on 4/22/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GoodToKnowSection: View { 11 | var body: some View { 12 | VStack(alignment: .leading, spacing: 7) { 13 | VStack(alignment: .leading) { 14 | Text("Good to Know") 15 | .font(.title2.weight(.semibold)) 16 | .frame(maxWidth: .infinity, alignment: .leading) 17 | Text("Information about this flight") 18 | .font(.callout.weight(.semibold)) 19 | .foregroundStyle(.secondary) 20 | .frame(maxWidth: .infinity, alignment: .leading) 21 | } 22 | .padding(.horizontal, 5) 23 | 24 | HStack(spacing: 5) { 25 | Image(systemName: "clock") 26 | .font(.title2) 27 | .padding(.leading,5) 28 | .padding(.trailing, 15) 29 | VStack(alignment: .leading, spacing:7) { 30 | Text("No Timezone Change") 31 | .font(.subheadline) 32 | .fontWeight(.bold) 33 | .frame(maxWidth: .infinity, alignment: .leading) 34 | Text("Both airports are in the same timezone") 35 | .font(.caption) 36 | .foregroundStyle(.secondary) 37 | } 38 | .padding(.vertical, 20) 39 | .overlay( 40 | Rectangle() 41 | .frame(width: nil, height: 0.5, alignment: .top) 42 | .foregroundColor(.border) 43 | , alignment: .bottom) 44 | } 45 | 46 | HStack(spacing: 5) { 47 | Image(systemName: "moon.fill") 48 | .font(.title2) 49 | .padding(.leading,5) 50 | .padding(.trailing, 15) 51 | VStack(alignment: .leading, spacing:7) { 52 | Text("Arrival Weather") 53 | .font(.subheadline) 54 | .fontWeight(.bold) 55 | .frame(maxWidth: .infinity, alignment: .leading) 56 | Text("57°F and clear sky") 57 | .font(.caption) 58 | .foregroundStyle(.secondary) 59 | } 60 | .padding(.vertical, 10) 61 | } 62 | } 63 | .padding(.vertical, 15) 64 | .padding(.horizontal, 15) 65 | .overlay( 66 | RoundedRectangle(cornerRadius: 20, style: .continuous) 67 | .stroke(.border, lineWidth: 1) 68 | ) 69 | .padding(.horizontal, 15) 70 | .padding(.vertical, 10) 71 | } 72 | } 73 | #Preview { 74 | GoodToKnowSection() 75 | } 76 | -------------------------------------------------------------------------------- /flighty/Views/HorizontalActions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalActions.swift 3 | // flighty 4 | // 5 | // Created by Christopher Free on 4/22/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // A view that renders all of the horizontally scrollable actions within the flight detail sheet. 11 | struct HorizontalActions: View { 12 | var body: some View { 13 | ScrollView(.horizontal) { 14 | HStack { 15 | // Live Share button 16 | Button { } label: { 17 | Label("Live Share", systemImage: "square.and.arrow.up") 18 | .padding(.vertical, 2) 19 | .padding(.horizontal, 5) 20 | } 21 | .fontDesign(.rounded) 22 | .fontWeight(.semibold) 23 | .buttonStyle(.borderedProminent) 24 | .buttonBorderShape(.capsule) 25 | 26 | // My flight. 27 | Button { } label: { 28 | Label("My Flight", systemImage: "person.fill") 29 | } 30 | .fontDesign(.rounded) 31 | .fontWeight(.regular) 32 | .buttonStyle(StrokedCapsule()) 33 | .buttonBorderShape(.capsule) 34 | 35 | // Alternatives button. 36 | Button { } label: { 37 | Label("Alternatives", systemImage: "arrow.triangle.2.circlepath") 38 | } 39 | .fontDesign(.rounded) 40 | .fontWeight(.regular) 41 | .buttonStyle(StrokedCapsule()) 42 | .buttonBorderShape(.capsule) 43 | 44 | // Live activity. 45 | Button { } label: { 46 | Label("Live Activity", systemImage: "bolt.fill") 47 | } 48 | .fontDesign(.rounded) 49 | .fontWeight(.regular) 50 | .buttonStyle(StrokedCapsule()) 51 | .buttonBorderShape(.capsule) 52 | 53 | Button { } label: { 54 | Label("More", systemImage: "ellipsis") 55 | } 56 | .fontDesign(.rounded) 57 | .fontWeight(.regular) 58 | .buttonStyle(StrokedCapsule()) 59 | .buttonBorderShape(.capsule) 60 | } 61 | .font(.system(size: 12)) 62 | } 63 | .contentMargins(.leading, 15) 64 | .scrollClipDisabled() 65 | .scrollIndicators(.hidden) 66 | } 67 | } 68 | 69 | #Preview { 70 | HorizontalActions() 71 | } 72 | -------------------------------------------------------------------------------- /flighty/flightyApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // flightyApp.swift 3 | // flighty 4 | // 5 | // Created by Christopher Free on 4/12/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct flightyApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | --------------------------------------------------------------------------------