├── .github ├── CODEOWNERS └── workflows │ ├── validate_code.yml │ └── xcode.yml ├── .gitignore ├── Code Relay ├── .editorconfig ├── Code Relay.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Code Relay.xcscheme └── Code Relay │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── Frame 82.png │ ├── Avatar.imageset │ │ ├── Contents.json │ │ └── Image.png │ └── Contents.json │ ├── Code_RelayApp.swift │ ├── ContentView.swift │ ├── DetailsView.swift │ ├── FocusField.swift │ ├── HomeView.swift │ ├── HomeViewModel.swift │ ├── Localizable.xcstrings │ ├── Managers │ └── GithubNetworkManager.swift │ ├── Models │ ├── GithubContributor.swift │ └── Location.swift │ ├── NewLocationView.swift │ ├── NotificatonName.swift │ ├── PrawnConfettiView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── SettingsView.swift │ ├── SettingsViewModel.swift │ └── SpinWheel │ ├── FlushyChartData.swift │ ├── FlushyChartView.swift │ ├── FlushyPrize.swift │ ├── SpinWheelView.swift │ └── SpinWheelViewModel.swift └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. Unless a later match takes precedence, @global-owner1 and @global-owner2 will be requested for review when someone opens a pull request. 2 | * @roddymunro 3 | * @Oliver-Binns 4 | -------------------------------------------------------------------------------- /.github/workflows/validate_code.yml: -------------------------------------------------------------------------------- 1 | name: Validate Translations 2 | 3 | on: 4 | pull_request: 5 | types: [synchronize, opened, reopened, edited] 6 | 7 | jobs: 8 | main: 9 | name: Validate Translations 10 | runs-on: macOS-latest 11 | steps: 12 | - name: git checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Clone SwiftPolyglot 16 | run: git clone https://github.com/appdecostudio/SwiftPolyglot.git --branch=1.1.0 ../SwiftPolyglot 17 | 18 | - name: validate translations 19 | run: | 20 | swift build --package-path ../SwiftPolyglot --configuration release 21 | swift run --package-path ../SwiftPolyglot swiftpolyglot en --error-on-missing -------------------------------------------------------------------------------- /.github/workflows/xcode.yml: -------------------------------------------------------------------------------- 1 | name: Xcode - Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: macos-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Select Xcode 17 | run: sudo xcode-select -switch /Applications/Xcode_16.0.app 18 | 19 | - name: Xcode version 20 | run: /usr/bin/xcodebuild -version 21 | 22 | - name: Build Xcode 23 | run: xcodebuild clean build -project "Code Relay/Code Relay.xcodeproj" -scheme "Code Relay" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## User settings 2 | *.xcuserdata/* 3 | */xcuserdata/* 4 | xcuserdata/* 5 | xcuserdata 6 | *.pbxuser 7 | DerivedData 8 | .DS_Store 9 | 10 | ## App packaging 11 | *.ipa 12 | *.dSYM.zip 13 | *.dSYM 14 | 15 | .build/ -------------------------------------------------------------------------------- /Code Relay/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://editorconfig.org 2 | [*.swift] 3 | 4 | indent_style = space 5 | tab_width = 4 6 | indent_size = 4 7 | 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | max_line_length = 160 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /Code Relay/Code Relay.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C022E5E32CB6CEFB00CA8067 /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = C022E5E22CB6CEFB00CA8067 /* CachedAsyncImage */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXFileReference section */ 14 | E3FF2FC92C997CFD0018D12A /* Code Relay.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Code Relay.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 15 | /* End PBXFileReference section */ 16 | 17 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 18 | E3FF2FCB2C997CFD0018D12A /* Code Relay */ = { 19 | isa = PBXFileSystemSynchronizedRootGroup; 20 | path = "Code Relay"; 21 | sourceTree = ""; 22 | }; 23 | /* End PBXFileSystemSynchronizedRootGroup section */ 24 | 25 | /* Begin PBXFrameworksBuildPhase section */ 26 | E3FF2FC62C997CFD0018D12A /* Frameworks */ = { 27 | isa = PBXFrameworksBuildPhase; 28 | buildActionMask = 2147483647; 29 | files = ( 30 | C022E5E32CB6CEFB00CA8067 /* CachedAsyncImage in Frameworks */, 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | E3FF2FC02C997CFD0018D12A = { 38 | isa = PBXGroup; 39 | children = ( 40 | E3FF2FCB2C997CFD0018D12A /* Code Relay */, 41 | E3FF2FCA2C997CFD0018D12A /* Products */, 42 | ); 43 | sourceTree = ""; 44 | }; 45 | E3FF2FCA2C997CFD0018D12A /* Products */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | E3FF2FC92C997CFD0018D12A /* Code Relay.app */, 49 | ); 50 | name = Products; 51 | sourceTree = ""; 52 | }; 53 | /* End PBXGroup section */ 54 | 55 | /* Begin PBXNativeTarget section */ 56 | E3FF2FC82C997CFD0018D12A /* Code Relay */ = { 57 | isa = PBXNativeTarget; 58 | buildConfigurationList = E3FF2FD72C997CFF0018D12A /* Build configuration list for PBXNativeTarget "Code Relay" */; 59 | buildPhases = ( 60 | E3FF2FC52C997CFD0018D12A /* Sources */, 61 | E3FF2FC62C997CFD0018D12A /* Frameworks */, 62 | E3FF2FC72C997CFD0018D12A /* Resources */, 63 | ); 64 | buildRules = ( 65 | ); 66 | dependencies = ( 67 | ); 68 | fileSystemSynchronizedGroups = ( 69 | E3FF2FCB2C997CFD0018D12A /* Code Relay */, 70 | ); 71 | name = "Code Relay"; 72 | packageProductDependencies = ( 73 | C022E5E22CB6CEFB00CA8067 /* CachedAsyncImage */, 74 | ); 75 | productName = "Code Relay"; 76 | productReference = E3FF2FC92C997CFD0018D12A /* Code Relay.app */; 77 | productType = "com.apple.product-type.application"; 78 | }; 79 | /* End PBXNativeTarget section */ 80 | 81 | /* Begin PBXProject section */ 82 | E3FF2FC12C997CFD0018D12A /* Project object */ = { 83 | isa = PBXProject; 84 | attributes = { 85 | BuildIndependentTargetsInParallel = 1; 86 | LastSwiftUpdateCheck = 1600; 87 | LastUpgradeCheck = 1600; 88 | TargetAttributes = { 89 | E3FF2FC82C997CFD0018D12A = { 90 | CreatedOnToolsVersion = 16.0; 91 | }; 92 | }; 93 | }; 94 | buildConfigurationList = E3FF2FC42C997CFD0018D12A /* Build configuration list for PBXProject "Code Relay" */; 95 | developmentRegion = en; 96 | hasScannedForEncodings = 0; 97 | knownRegions = ( 98 | en, 99 | Base, 100 | ); 101 | mainGroup = E3FF2FC02C997CFD0018D12A; 102 | minimizedProjectReferenceProxies = 1; 103 | packageReferences = ( 104 | C022E5E12CB6CEFB00CA8067 /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */, 105 | ); 106 | preferredProjectObjectVersion = 77; 107 | productRefGroup = E3FF2FCA2C997CFD0018D12A /* Products */; 108 | projectDirPath = ""; 109 | projectRoot = ""; 110 | targets = ( 111 | E3FF2FC82C997CFD0018D12A /* Code Relay */, 112 | ); 113 | }; 114 | /* End PBXProject section */ 115 | 116 | /* Begin PBXResourcesBuildPhase section */ 117 | E3FF2FC72C997CFD0018D12A /* Resources */ = { 118 | isa = PBXResourcesBuildPhase; 119 | buildActionMask = 2147483647; 120 | files = ( 121 | ); 122 | runOnlyForDeploymentPostprocessing = 0; 123 | }; 124 | /* End PBXResourcesBuildPhase section */ 125 | 126 | /* Begin PBXSourcesBuildPhase section */ 127 | E3FF2FC52C997CFD0018D12A /* Sources */ = { 128 | isa = PBXSourcesBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXSourcesBuildPhase section */ 135 | 136 | /* Begin XCBuildConfiguration section */ 137 | E3FF2FD52C997CFF0018D12A /* Debug */ = { 138 | isa = XCBuildConfiguration; 139 | buildSettings = { 140 | ALWAYS_SEARCH_USER_PATHS = NO; 141 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 142 | CLANG_ANALYZER_NONNULL = YES; 143 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 144 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 145 | CLANG_ENABLE_MODULES = YES; 146 | CLANG_ENABLE_OBJC_ARC = YES; 147 | CLANG_ENABLE_OBJC_WEAK = YES; 148 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 149 | CLANG_WARN_BOOL_CONVERSION = YES; 150 | CLANG_WARN_COMMA = YES; 151 | CLANG_WARN_CONSTANT_CONVERSION = YES; 152 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 153 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 154 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 155 | CLANG_WARN_EMPTY_BODY = YES; 156 | CLANG_WARN_ENUM_CONVERSION = YES; 157 | CLANG_WARN_INFINITE_RECURSION = YES; 158 | CLANG_WARN_INT_CONVERSION = YES; 159 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 160 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 161 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 162 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 163 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 164 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 165 | CLANG_WARN_STRICT_PROTOTYPES = YES; 166 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 167 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 168 | CLANG_WARN_UNREACHABLE_CODE = YES; 169 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 170 | COPY_PHASE_STRIP = NO; 171 | DEBUG_INFORMATION_FORMAT = dwarf; 172 | ENABLE_STRICT_OBJC_MSGSEND = YES; 173 | ENABLE_TESTABILITY = YES; 174 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 175 | GCC_C_LANGUAGE_STANDARD = gnu17; 176 | GCC_DYNAMIC_NO_PIC = NO; 177 | GCC_NO_COMMON_BLOCKS = YES; 178 | GCC_OPTIMIZATION_LEVEL = 0; 179 | GCC_PREPROCESSOR_DEFINITIONS = ( 180 | "DEBUG=1", 181 | "$(inherited)", 182 | ); 183 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 184 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 185 | GCC_WARN_UNDECLARED_SELECTOR = YES; 186 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 187 | GCC_WARN_UNUSED_FUNCTION = YES; 188 | GCC_WARN_UNUSED_VARIABLE = YES; 189 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 190 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 191 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 192 | MTL_FAST_MATH = YES; 193 | ONLY_ACTIVE_ARCH = YES; 194 | SDKROOT = iphoneos; 195 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 196 | SWIFT_EMIT_LOC_STRINGS = YES; 197 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 198 | }; 199 | name = Debug; 200 | }; 201 | E3FF2FD62C997CFF0018D12A /* Release */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 206 | CLANG_ANALYZER_NONNULL = YES; 207 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 208 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 209 | CLANG_ENABLE_MODULES = YES; 210 | CLANG_ENABLE_OBJC_ARC = YES; 211 | CLANG_ENABLE_OBJC_WEAK = YES; 212 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_COMMA = YES; 215 | CLANG_WARN_CONSTANT_CONVERSION = YES; 216 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 217 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 218 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 219 | CLANG_WARN_EMPTY_BODY = YES; 220 | CLANG_WARN_ENUM_CONVERSION = YES; 221 | CLANG_WARN_INFINITE_RECURSION = YES; 222 | CLANG_WARN_INT_CONVERSION = YES; 223 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 224 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 225 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 226 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 227 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 228 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 229 | CLANG_WARN_STRICT_PROTOTYPES = YES; 230 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 231 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 232 | CLANG_WARN_UNREACHABLE_CODE = YES; 233 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 234 | COPY_PHASE_STRIP = NO; 235 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 236 | ENABLE_NS_ASSERTIONS = NO; 237 | ENABLE_STRICT_OBJC_MSGSEND = YES; 238 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 239 | GCC_C_LANGUAGE_STANDARD = gnu17; 240 | GCC_NO_COMMON_BLOCKS = YES; 241 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 242 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 243 | GCC_WARN_UNDECLARED_SELECTOR = YES; 244 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 245 | GCC_WARN_UNUSED_FUNCTION = YES; 246 | GCC_WARN_UNUSED_VARIABLE = YES; 247 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 248 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 249 | MTL_ENABLE_DEBUG_INFO = NO; 250 | MTL_FAST_MATH = YES; 251 | SDKROOT = iphoneos; 252 | SWIFT_COMPILATION_MODE = wholemodule; 253 | SWIFT_EMIT_LOC_STRINGS = YES; 254 | VALIDATE_PRODUCT = YES; 255 | }; 256 | name = Release; 257 | }; 258 | E3FF2FD82C997CFF0018D12A /* Debug */ = { 259 | isa = XCBuildConfiguration; 260 | buildSettings = { 261 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 262 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 263 | CODE_SIGN_STYLE = Automatic; 264 | CURRENT_PROJECT_VERSION = 1; 265 | DEVELOPMENT_ASSET_PATHS = "\"Code Relay/Preview Content\""; 266 | DEVELOPMENT_TEAM = ""; 267 | ENABLE_PREVIEWS = YES; 268 | GENERATE_INFOPLIST_FILE = YES; 269 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 270 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 271 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 272 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 273 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 274 | LD_RUNPATH_SEARCH_PATHS = ( 275 | "$(inherited)", 276 | "@executable_path/Frameworks", 277 | ); 278 | MARKETING_VERSION = 1.0; 279 | PRODUCT_BUNDLE_IDENTIFIER = com.swiftleeds.coderelay; 280 | PRODUCT_NAME = "$(TARGET_NAME)"; 281 | SWIFT_EMIT_LOC_STRINGS = YES; 282 | SWIFT_VERSION = 5.0; 283 | TARGETED_DEVICE_FAMILY = "1,2"; 284 | }; 285 | name = Debug; 286 | }; 287 | E3FF2FD92C997CFF0018D12A /* Release */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 291 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 292 | CODE_SIGN_STYLE = Automatic; 293 | CURRENT_PROJECT_VERSION = 1; 294 | DEVELOPMENT_ASSET_PATHS = "\"Code Relay/Preview Content\""; 295 | DEVELOPMENT_TEAM = ""; 296 | ENABLE_PREVIEWS = YES; 297 | GENERATE_INFOPLIST_FILE = YES; 298 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 299 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 300 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 301 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 302 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 303 | LD_RUNPATH_SEARCH_PATHS = ( 304 | "$(inherited)", 305 | "@executable_path/Frameworks", 306 | ); 307 | MARKETING_VERSION = 1.0; 308 | PRODUCT_BUNDLE_IDENTIFIER = com.swiftleeds.coderelay; 309 | PRODUCT_NAME = "$(TARGET_NAME)"; 310 | SWIFT_EMIT_LOC_STRINGS = YES; 311 | SWIFT_VERSION = 5.0; 312 | TARGETED_DEVICE_FAMILY = "1,2"; 313 | }; 314 | name = Release; 315 | }; 316 | /* End XCBuildConfiguration section */ 317 | 318 | /* Begin XCConfigurationList section */ 319 | E3FF2FC42C997CFD0018D12A /* Build configuration list for PBXProject "Code Relay" */ = { 320 | isa = XCConfigurationList; 321 | buildConfigurations = ( 322 | E3FF2FD52C997CFF0018D12A /* Debug */, 323 | E3FF2FD62C997CFF0018D12A /* Release */, 324 | ); 325 | defaultConfigurationIsVisible = 0; 326 | defaultConfigurationName = Release; 327 | }; 328 | E3FF2FD72C997CFF0018D12A /* Build configuration list for PBXNativeTarget "Code Relay" */ = { 329 | isa = XCConfigurationList; 330 | buildConfigurations = ( 331 | E3FF2FD82C997CFF0018D12A /* Debug */, 332 | E3FF2FD92C997CFF0018D12A /* Release */, 333 | ); 334 | defaultConfigurationIsVisible = 0; 335 | defaultConfigurationName = Release; 336 | }; 337 | /* End XCConfigurationList section */ 338 | 339 | /* Begin XCRemoteSwiftPackageReference section */ 340 | C022E5E12CB6CEFB00CA8067 /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */ = { 341 | isa = XCRemoteSwiftPackageReference; 342 | repositoryURL = "https://github.com/lorenzofiamingo/swiftui-cached-async-image.git"; 343 | requirement = { 344 | kind = upToNextMajorVersion; 345 | minimumVersion = 2.1.1; 346 | }; 347 | }; 348 | /* End XCRemoteSwiftPackageReference section */ 349 | 350 | /* Begin XCSwiftPackageProductDependency section */ 351 | C022E5E22CB6CEFB00CA8067 /* CachedAsyncImage */ = { 352 | isa = XCSwiftPackageProductDependency; 353 | package = C022E5E12CB6CEFB00CA8067 /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */; 354 | productName = CachedAsyncImage; 355 | }; 356 | /* End XCSwiftPackageProductDependency section */ 357 | }; 358 | rootObject = E3FF2FC12C997CFD0018D12A /* Project object */; 359 | } 360 | -------------------------------------------------------------------------------- /Code Relay/Code Relay.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Code Relay/Code Relay.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "c49517a3a917ff0505be51a11c3b9b6d5ff293b67ec086349c3d52a78b1fbd7a", 3 | "pins" : [ 4 | { 5 | "identity" : "swiftui-cached-async-image", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/lorenzofiamingo/swiftui-cached-async-image.git", 8 | "state" : { 9 | "revision" : "467a3d17479887943ab917a379e62bbaff60ac8a", 10 | "version" : "2.1.1" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Code Relay/Code Relay.xcodeproj/xcshareddata/xcschemes/Code Relay.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/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 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Frame 82.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "idiom" : "universal", 17 | "platform" : "ios", 18 | "size" : "1024x1024" 19 | }, 20 | { 21 | "appearances" : [ 22 | { 23 | "appearance" : "luminosity", 24 | "value" : "tinted" 25 | } 26 | ], 27 | "idiom" : "universal", 28 | "platform" : "ios", 29 | "size" : "1024x1024" 30 | } 31 | ], 32 | "info" : { 33 | "author" : "xcode", 34 | "version" : 1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/Assets.xcassets/AppIcon.appiconset/Frame 82.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/code-relay/d8c43783d289597216bc6750cb6ad353e29e4bbe/Code Relay/Code Relay/Assets.xcassets/AppIcon.appiconset/Frame 82.png -------------------------------------------------------------------------------- /Code Relay/Code Relay/Assets.xcassets/Avatar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Image.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/Assets.xcassets/Avatar.imageset/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/code-relay/d8c43783d289597216bc6750cb6ad353e29e4bbe/Code Relay/Code Relay/Assets.xcassets/Avatar.imageset/Image.png -------------------------------------------------------------------------------- /Code Relay/Code Relay/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/Code_RelayApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Code_RelayApp.swift 3 | // Code Relay 4 | // 5 | // Created by Adam Rush on 17/09/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct Code_RelayApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | 19 | extension UIWindow { 20 | open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { 21 | if motion == .motionShake { 22 | NotificationCenter.default.post(name: .shake, object: nil) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Code Relay 4 | // 5 | // Created by Adam Rush on 17/09/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @State private var isShowingAlert = false 12 | var body: some View { 13 | TabView { 14 | Tab("Home", systemImage: "house") { 15 | HomeView() 16 | } 17 | 18 | Tab("Favourites", systemImage: "star") { 19 | // TODO: Add Favourites View 🍤 20 | } 21 | 22 | Tab("Settings", systemImage: "gear") { 23 | SettingsView() 24 | } 25 | 26 | Tab("Lucky wheel", systemImage: "arcade.stick") { 27 | NavigationView { 28 | SpinWheelView() 29 | } 30 | } 31 | }.tint(.red) 32 | .onReceive(NotificationCenter.default.publisher(for: .shake)) { _ in 33 | isShowingAlert = true 34 | } 35 | .alert("Here is an Adam joke", isPresented: $isShowingAlert) { 36 | } message: { 37 | Text(randomAdamJoke) 38 | } 39 | } 40 | 41 | var randomAdamJoke: String { 42 | return [ 43 | "My boss said “dress for the job you want, not for the job you have.” So I went in as Batman.", 44 | "I went to the aquarium this weekend, but I didn’t stay long. There’s something fishy about that place.", 45 | "What do you call a sheep who can sing and dance? Lady Ba Ba.", 46 | "What do you call a French man wearing sandals? Philipe Fallop.", 47 | "Why can't dinosaurs clap their hands? Because they're extinct.", 48 | "I gave my handyman a to-do list, but he only did jobs 1, 3, and 5. Turns out he only does odd jobs.", 49 | "Why should you never take sides in an argument at the dinner table? Trick question. It's the perfect time to take sides because no one's paying attention. Bring Tupperware.", 50 | "Who won the neck decorating contest? It was a tie.", 51 | "Where do rainbows go when they've been bad? To prism, so they have time to reflect on what they've done.", 52 | "Dogs can't operate MRI machines. But catscan.", 53 | "What do mermaids use to wash their fins? Tide.", 54 | "What did the skillet eat on its birthday? Pan-cakes.", 55 | "Why couldn't the produce manager make it to work? He could drive, but he didn't avocado.", 56 | "I went to a silent auction. I won a dog whistle and two mimes.", 57 | "How is my wallet like an onion? Every time I open it, I cry.", 58 | "What do you call a dog who meditates? Aware wolf.", 59 | "What kind of fish do penguins catch at night? Star fish.", 60 | "Which vegetable has the best kung fu? Broc-lee.", 61 | "Can a frog jump higher than a house? Of course, a house can't jump.", 62 | "I was going to try an all almond diet, but that's just nuts.", 63 | "I once submitted 10 puns to a joke competition. I really thought with that many, one was sure to be a winner. Sadly, no pun in ten did.", 64 | "Why did the old man fall down the well? He couldn’t see that well.", 65 | "I tried to make up a joke about ghost but I couldn't. It had plenty of spirit but no body.", 66 | "Dad: What is the difference between a piano, a tuna, and a pot of glue? Me: I don't know. Dad: You can tuna piano but you can't piano a tuna. Me: What about the pot of glue? Dad: I knew you'd get stuck on that.", 67 | "Two windmills were sitting on a hill. One asks the other, \"Do you have a favorite song?\" The other replies, \"Well... all my life I have been a heavy metal fan.\"", 68 | "Today at the bank, an old lady asked me to check her balance... So I pushed her over.", 69 | "I got an A on my origami assignment when I turned my paper into my teacher.", 70 | "How many storm troopers does it take to change a lightbulb? None, because they are all on the dark side.", 71 | "If your house is cold, just stand in the corner. It’s always 90 degrees there.", 72 | "Hi, I’m Cliff. Drop over sometime.", 73 | "Did you hear about the guy who went to the doctor for a headache? The doctor examined his ear and found money. The doctor kept pulling and pulling it out until he had $1,999. Then the doctor said, \"No wonder you're not feeling two grand!\"", 74 | "Dad, when he puts the car in reverse: \"Ah, this takes me back.\"", 75 | "What do you call the security guards for Samsung? Guardians of the galaxy.", 76 | "I was making a joke about retirement. It did not work.", 77 | "The other day I bought a thesaurus, but when I got home and opened it, all the pages were blank. I have no word to describe how angry I am.", 78 | "The owner of the tuxedo store kept hovering over me when I was browsing, so I asked him to leave me alone. He said, “Fine, suit yourself.”", 79 | "Why did the egg have a day off? Because it was Fryday.", 80 | "Have you ever heard about the kidnapping at school? It's okay, he woke up.", 81 | "I found a book called How to Solve 50% of Your Problems. So I bought 2.", 82 | "Why did the coffee taste like dirt? Because it was ground just a few minutes ago.", 83 | "Why did the Rolling Stones stop making music? Because they got to bottom of the hill.", 84 | "What is the best present? Broken drums! You can't beat them.", 85 | "I made song about tortilla once, now it's more like a wrap.", 86 | "Did you know courdury pillows are in style? They're making headlines.", 87 | "What does a nosey pepper do? It gets jalapeño business.", 88 | "Did you hear about the fragile myth? It was busted.", 89 | "What word can you make shorter by adding two letters? Short.", 90 | "What do call a criminal landing an airplane? Condescending.", 91 | "I stayed up all night wondering where the sun went, and then it dawned on me.", 92 | "Why do people who live in Greece hate waking up at dawn? Because Dawn is tough on Greece." 93 | ] 94 | .randomElement() ?? "Adam is out of jokes!" 95 | } 96 | } 97 | 98 | #Preview { 99 | ContentView() 100 | } 101 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/DetailsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailsView.swift 3 | // Code Relay 4 | // 5 | // Created by Sebastian Bolling on 2024-10-08. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | struct DetailsView: View { 12 | 13 | @State private var showCheekyPrawn = false 14 | @State private var showConfetti = false 15 | @State private var region: MKCoordinateRegion 16 | @State private var lookAroundScene: MKLookAroundScene? 17 | @State private var isLookingAround = false 18 | 19 | private var location: Location 20 | 21 | init(location: Location) { 22 | self.location = location 23 | let spanDelta = location.coordinate != nil ? 0.01 : 0.20 24 | self.region = MKCoordinateRegion( 25 | center: location.coordinate ?? CLLocationCoordinate2D(latitude: 53.79826, 26 | longitude: -1.53343), 27 | span: MKCoordinateSpan(latitudeDelta: spanDelta, longitudeDelta: spanDelta)) 28 | } 29 | 30 | var body: some View { 31 | ZStack { 32 | VStack { 33 | Map(initialPosition: .region( 34 | MKCoordinateRegion( 35 | center: location.coordinate ?? CLLocationCoordinate2D(latitude: 0, longitude: 0), 36 | span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005) 37 | ) 38 | )) 39 | .frame(height: 400) 40 | .frame(maxWidth: .infinity) 41 | .onAppear { 42 | getLookAroundScene() 43 | } 44 | .lookAroundViewer(isPresented: $isLookingAround, initialScene: lookAroundScene) 45 | .overlay(alignment: .bottomTrailing) { 46 | if lookAroundScene != nil { 47 | Button { 48 | isLookingAround.toggle() 49 | } label: { 50 | Image(systemName: "binoculars.fill") 51 | .foregroundStyle(Color(.secondaryLabel)) 52 | .frame(width: 40, height: 40) 53 | .background(Color(.tertiarySystemGroupedBackground)) 54 | .clipShape(RoundedRectangle(cornerRadius: 8)) 55 | } 56 | .padding(.all, 8) 57 | } 58 | } 59 | 60 | Text(location.title).fontWeight(.bold) 61 | Text(location.details).onTapGesture { toggleCheekyPrawn() } 62 | if showCheekyPrawn { 63 | Text("🦐").font(.largeTitle) 64 | } 65 | 66 | Spacer() 67 | 68 | if let phoneNumber = location.phoneNumber { 69 | Link("Call", destination: URL(string: "tel:\(phoneNumber)")!) 70 | .padding() 71 | } 72 | } 73 | .frame(maxHeight: .infinity, alignment: .top) 74 | 75 | if showCheekyPrawn && showConfetti { 76 | PrawnConfettiView( 77 | numberOfConfetti: 200, 78 | animationDuration: 1, 79 | onComplete: { 80 | showConfetti = false 81 | } 82 | ) 83 | .edgesIgnoringSafeArea(.all) 84 | } 85 | } 86 | } 87 | 88 | private func toggleCheekyPrawn() { 89 | showCheekyPrawn.toggle() 90 | showConfetti = showCheekyPrawn 91 | } 92 | 93 | private func getLookAroundScene() { 94 | lookAroundScene = nil 95 | Task { 96 | if let coordinate = location.coordinate { 97 | let request = MKLookAroundSceneRequest(coordinate: coordinate) 98 | do { 99 | lookAroundScene = try await request.scene 100 | } catch { 101 | print("Error getting look around scene: \(error)") 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | #Preview { 109 | DetailsView( 110 | location: Location( 111 | title: "A Place With A Location", 112 | details: "The Place", 113 | coordinate: CLLocationCoordinate2D(latitude: 41.882683, longitude: -87.623321), 114 | phoneNumber: nil 115 | ) 116 | ) 117 | } 118 | 119 | #Preview { 120 | DetailsView( 121 | location: Location( 122 | title: "A Place Without A Location", 123 | details: "The Place", 124 | coordinate: nil, 125 | phoneNumber: nil 126 | ) 127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/FocusField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusField.swift 3 | // Code Relay 4 | // 5 | // Created by Petter vang Brakalsvålet on 09/10/2024. 6 | // 7 | 8 | enum FocusField: String, Identifiable { 9 | case title = "title" 10 | case details = "details" 11 | case lat = "lat" 12 | case long = "long" 13 | case phoneNumber = "phoneNumber" 14 | 15 | var id: String { rawValue } 16 | 17 | var next: FocusField? { 18 | switch self { 19 | case .title: 20 | .details 21 | case .details: 22 | .lat 23 | case .lat: 24 | .long 25 | case .long: 26 | .phoneNumber 27 | case .phoneNumber: 28 | nil 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/HomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeView.swift 3 | // Code Relay 4 | // 5 | // Created by Mikaela Caron on 10/8/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HomeView: View { 11 | @State var showAddNewLocation: Bool = false 12 | @StateObject var viewModel = HomeViewModel() 13 | 14 | var body: some View { 15 | NavigationStack { 16 | List { 17 | ForEach(viewModel.locationsData) { location in 18 | NavigationLink(location.id) { 19 | DetailsView(location: location) 20 | } 21 | } 22 | } 23 | .toolbar { 24 | ToolbarItem(placement: .navigationBarTrailing) { 25 | Button { 26 | showAddNewLocation = true 27 | } label: { 28 | Image(systemName: "plus") 29 | } 30 | .accessibilityLabel("Add new location") 31 | } 32 | } 33 | .navigationTitle("Guide to Leeds") 34 | } 35 | .sheet(isPresented: $showAddNewLocation) { 36 | NewLocationView(title: "", details: "", phoneNumber: "") 37 | } 38 | .onReceive(NotificationCenter.default.publisher(for: .newLocation)) { notification in 39 | guard let location = notification.userInfo?["location"] as? Location else { 40 | return 41 | } 42 | viewModel.locationsData.append(location) 43 | } 44 | 45 | } 46 | } 47 | 48 | #Preview { 49 | HomeView() 50 | } 51 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/HomeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewModel.swift 3 | // Code Relay 4 | // 5 | // Created by Sebastian Bolling on 2024-10-08. 6 | // 7 | 8 | import CoreLocation 9 | 10 | final class HomeViewModel: ObservableObject { 11 | 12 | init() { 13 | locationsData = [ 14 | Location( 15 | title: "Leeds Playhouse", 16 | details: "Good Conference", 17 | coordinate: CLLocationCoordinate2D(latitude: 53.797932, longitude: -1.534352), 18 | phoneNumber: "0044 113 213 7700" 19 | ), 20 | Location( 21 | title: "Leeds Art Gallery", 22 | details: "Good Art", 23 | coordinate: CLLocationCoordinate2D(latitude: 53.799992, longitude: -1.548128), 24 | phoneNumber: "0044 113 378 5350" 25 | ), 26 | Location( 27 | title: "Victoria Leeds", 28 | details: "Something good", 29 | coordinate: CLLocationCoordinate2D(latitude: 53.798014, longitude: -1.539134), 30 | phoneNumber: "0044 113 245 5333" 31 | ), 32 | Location( 33 | title: "Royal Armouries", 34 | details: "Good Armor", 35 | coordinate: CLLocationCoordinate2D(latitude: 53.791750, longitude: -1.532436), 36 | phoneNumber: "0044 113 220 1916" 37 | ), 38 | Location(title: "Brew Society", 39 | details: "Good Beer", 40 | coordinate: CLLocationCoordinate2D(latitude: 53.795610, longitude: -1.550365), 41 | phoneNumber: nil), 42 | Location( 43 | title: "Brew Dog", 44 | details: "Good Dog Beer", 45 | coordinate: CLLocationCoordinate2D(latitude: 53.801097, longitude: -1.538588), 46 | phoneNumber: "0044 113 391 2950" 47 | ), 48 | Location(title: "NQ64 Arcade Bar", details: "Retro Gaming Bar", coordinate: CLLocationCoordinate2D(latitude: 53.8004635, longitude: -1.5406172), phoneNumber: "0044 113 245 5333"), 49 | Location(title: "K-Cube", details: "Karaoke all night long", coordinate: CLLocationCoordinate2D(latitude: 53.8010051, longitude: -1.5471566), phoneNumber: "0044 113 322 6625"), 50 | ] 51 | } 52 | 53 | @Published var locationsData: [Location] 54 | } 55 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "%lld contributions" : { 5 | 6 | }, 7 | "🦐" : { 8 | "localizations" : { 9 | "en" : { 10 | "stringUnit" : { 11 | "state" : "translated", 12 | "value" : "🦐" 13 | } 14 | } 15 | } 16 | }, 17 | "Add new location" : { 18 | "localizations" : { 19 | "en" : { 20 | "stringUnit" : { 21 | "state" : "translated", 22 | "value" : "Add new location" 23 | } 24 | } 25 | } 26 | }, 27 | "Add New Location" : { 28 | "localizations" : { 29 | "en" : { 30 | "stringUnit" : { 31 | "state" : "translated", 32 | "value" : "Add New Location" 33 | } 34 | } 35 | } 36 | }, 37 | "Call" : { 38 | "localizations" : { 39 | "en" : { 40 | "stringUnit" : { 41 | "state" : "translated", 42 | "value" : "Call" 43 | } 44 | } 45 | } 46 | }, 47 | "Category" : { 48 | 49 | }, 50 | "Coordinate lat" : { 51 | "localizations" : { 52 | "en" : { 53 | "stringUnit" : { 54 | "state" : "translated", 55 | "value" : "Coordinate lat" 56 | } 57 | } 58 | } 59 | }, 60 | "Coordinate long" : { 61 | "localizations" : { 62 | "en" : { 63 | "stringUnit" : { 64 | "state" : "translated", 65 | "value" : "Coordinate long" 66 | } 67 | } 68 | } 69 | }, 70 | "Details" : { 71 | "localizations" : { 72 | "en" : { 73 | "stringUnit" : { 74 | "state" : "translated", 75 | "value" : "Details" 76 | } 77 | } 78 | } 79 | }, 80 | "Developed at " : { 81 | "localizations" : { 82 | "en" : { 83 | "stringUnit" : { 84 | "state" : "translated", 85 | "value" : "Developed at " 86 | } 87 | } 88 | } 89 | }, 90 | "Favourites" : { 91 | "localizations" : { 92 | "en" : { 93 | "stringUnit" : { 94 | "state" : "translated", 95 | "value" : "Favourites" 96 | } 97 | } 98 | } 99 | }, 100 | "Guide to Leeds" : { 101 | "localizations" : { 102 | "en" : { 103 | "stringUnit" : { 104 | "state" : "translated", 105 | "value" : "Guide to Leeds" 106 | } 107 | } 108 | } 109 | }, 110 | "Here is an Adam joke" : { 111 | 112 | }, 113 | "Home" : { 114 | "localizations" : { 115 | "en" : { 116 | "stringUnit" : { 117 | "state" : "translated", 118 | "value" : "Home" 119 | } 120 | } 121 | } 122 | }, 123 | "If you also desired to have one of those flushy cats, train your luck before!" : { 124 | 125 | }, 126 | "Lucky wheel" : { 127 | 128 | }, 129 | "Phone number" : { 130 | "localizations" : { 131 | "en" : { 132 | "stringUnit" : { 133 | "state" : "translated", 134 | "value" : "Phone number" 135 | } 136 | } 137 | } 138 | }, 139 | "Save" : { 140 | "localizations" : { 141 | "en" : { 142 | "stringUnit" : { 143 | "state" : "translated", 144 | "value" : "Save" 145 | } 146 | } 147 | } 148 | }, 149 | "Selected Coordinate" : { 150 | 151 | }, 152 | "Settings" : { 153 | "localizations" : { 154 | "en" : { 155 | "stringUnit" : { 156 | "state" : "translated", 157 | "value" : "Settings" 158 | } 159 | } 160 | } 161 | }, 162 | "Spin the wheel" : { 163 | 164 | }, 165 | "SwiftLeeds" : { 166 | "extractionState" : "stale", 167 | "localizations" : { 168 | "en" : { 169 | "stringUnit" : { 170 | "state" : "translated", 171 | "value" : "SwiftLeeds" 172 | } 173 | } 174 | }, 175 | "shouldTranslate" : false 176 | }, 177 | "SwiftLeeds ❤️" : { 178 | 179 | }, 180 | "Title" : { 181 | "localizations" : { 182 | "en" : { 183 | "stringUnit" : { 184 | "state" : "translated", 185 | "value" : "Title" 186 | } 187 | } 188 | }, 189 | "shouldTranslate" : false 190 | }, 191 | "Train your luck" : { 192 | 193 | }, 194 | "Value" : { 195 | 196 | } 197 | }, 198 | "version" : "1.0" 199 | } -------------------------------------------------------------------------------- /Code Relay/Code Relay/Managers/GithubNetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManager.swift 3 | // Code Relay 4 | // 5 | // Created by Gerald.Wood on 10/8/24. 6 | // 7 | 8 | import Foundation 9 | 10 | class GithubNetworkManager { 11 | static let shared = GithubNetworkManager() 12 | let decoder = JSONDecoder() 13 | 14 | private init() { 15 | decoder.keyDecodingStrategy = .convertFromSnakeCase 16 | decoder.dateDecodingStrategy = .iso8601 17 | } 18 | 19 | func getContributors(atURL urlString: String) async throws -> [Contributor] { 20 | guard let url = URL(string: urlString) else { 21 | throw NetworkError.invalidRepoURL 22 | } 23 | 24 | let (data, response) = try await URLSession.shared.data(from: url) 25 | 26 | guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { 27 | throw NetworkError.invalidResponse 28 | } 29 | 30 | do { 31 | let codingData = try decoder.decode([Contributor.CodingData].self, from: data) 32 | return codingData.map { $0.contributor }.sorted { $0.contributions > $1.contributions } 33 | } catch { 34 | throw NetworkError.invalidRepoData 35 | } 36 | } 37 | } 38 | 39 | enum NetworkError: Error { 40 | case invalidRepoURL 41 | case invalidResponse 42 | case invalidRepoData 43 | } 44 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/Models/GithubContributor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Contributor.swift 3 | // Code Relay 4 | // 5 | // Created by Gerald.Wood on 10/8/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Contributor: Identifiable { 11 | var id = UUID() 12 | let login: String 13 | let avatarUrl: String 14 | let contributions: Int 15 | } 16 | 17 | extension Contributor { 18 | struct CodingData: Decodable { 19 | let login: String 20 | let avatarUrl: String 21 | let contributions: Int 22 | 23 | var contributor: Contributor { 24 | Contributor(login: login, 25 | avatarUrl: avatarUrl, 26 | contributions: contributions) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/Models/Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data.swift 3 | // Code Relay 4 | // 5 | // Created by Sebastian Bolling on 2024-10-08. 6 | // 7 | import CoreLocation 8 | 9 | struct Location: Identifiable { 10 | 11 | let title: String 12 | let details: String 13 | let coordinate: CLLocationCoordinate2D? 14 | let phoneNumber: String? 15 | var id: String { title } 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/NewLocationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewLocationView.swift 3 | // Code Relay 4 | // 5 | // Created by Petter vang Brakalsvålet on 09/10/2024. 6 | // 7 | 8 | import SwiftUI 9 | import CoreLocation 10 | import MapKit 11 | 12 | public struct NewLocationView: View { 13 | @Environment(\.dismiss) var dismiss 14 | @FocusState var field: FocusField? 15 | 16 | @State var title: String 17 | @State var details: String 18 | @State var lat: Double? 19 | @State var long: Double? 20 | @State var phoneNumber: String 21 | 22 | // Init the location with the current coordinates if applicable 23 | @State var location: Location 24 | 25 | public var body: some View { 26 | 27 | var region: Binding = Binding { 28 | MKCoordinateRegion( 29 | center: CLLocationCoordinate2D(latitude: lat ?? 0, longitude: long ?? 0), 30 | span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) 31 | ) 32 | } set: {_ in } 33 | 34 | 35 | NavigationStack { 36 | VStack { 37 | List { 38 | TextField("Title", text: $title) 39 | .id(FocusField.title) 40 | .focused($field, equals: .title) 41 | TextField("Details", text: $details) 42 | .id(FocusField.details) 43 | .focused($field, equals: .details) 44 | TextField("Coordinate lat", value: $lat, format: .number) 45 | .id(FocusField.lat) 46 | .focused($field, equals: .lat) 47 | TextField("Coordinate long", value: $long, format: .number) 48 | .id(FocusField.long) 49 | .focused($field, equals: .long) 50 | TextField("Phone number", text: $phoneNumber) 51 | .id(FocusField.phoneNumber) 52 | .focused($field, equals: .phoneNumber) 53 | } 54 | .onSubmit { 55 | field = field?.next 56 | } 57 | 58 | Spacer() 59 | Map { 60 | Marker("Selected Coordinate", coordinate: CLLocationCoordinate2D(latitude: lat ?? 0, longitude: long ?? 0)) 61 | } 62 | .cornerRadius(10) 63 | .padding(.horizontal, 24) 64 | .padding(.top, 24) 65 | .padding(.bottom, 12) 66 | .frame(height: 250) 67 | .frame(maxWidth: .infinity) 68 | Spacer() 69 | 70 | Button { 71 | let coordinate: CLLocationCoordinate2D? = if let lat, let long { 72 | .init(latitude: lat, longitude: long) 73 | } else { 74 | nil 75 | } 76 | guard !title.isEmpty, !details.isEmpty else { return } 77 | NotificationCenter.default.post( 78 | name: .newLocation, 79 | object: self, 80 | userInfo: ["location": Location( 81 | title: title, 82 | details: details, 83 | coordinate: coordinate, 84 | phoneNumber: phoneNumber 85 | )] 86 | ) 87 | dismiss() 88 | } label: { 89 | HStack { 90 | Spacer() 91 | Text("Save") 92 | Spacer() 93 | } 94 | } 95 | .font(.title2) 96 | .bold() 97 | .buttonStyle(.borderedProminent) 98 | .padding(.horizontal, 24) 99 | } 100 | .navigationTitle("Add New Location") 101 | }.onChange(of: lat ?? 0) { oldValue, newValue in 102 | region.wrappedValue = MKCoordinateRegion( 103 | center: CLLocationCoordinate2D(latitude: lat ?? 0, longitude: long ?? 0), 104 | span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) 105 | ) 106 | } 107 | .onChange(of: long ?? 0) { oldValue, newValue in 108 | region.wrappedValue = MKCoordinateRegion( 109 | center: CLLocationCoordinate2D(latitude: lat ?? 0, longitude: long ?? 0), 110 | span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) 111 | ) 112 | } 113 | } 114 | 115 | init( 116 | title: String, 117 | details: String, 118 | lat: Double? = nil, 119 | long: Double? = nil, 120 | phoneNumber: String 121 | ) { 122 | self.title = title 123 | self.details = details 124 | self.lat = lat 125 | self.long = long 126 | self.phoneNumber = phoneNumber 127 | self.location = Location(title: "", details: "", coordinate: CLLocationCoordinate2D(latitude: lat ?? 0, longitude: long ?? 0), phoneNumber: "") 128 | } 129 | } 130 | 131 | #Preview { 132 | NewLocationView(title: "", details: "", lat: nil, long: nil, phoneNumber: "") 133 | } 134 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/NotificatonName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificatonName.swift 3 | // Code Relay 4 | // 5 | // Created by Petter vang Brakalsvålet on 09/10/2024. 6 | // 7 | import Foundation 8 | 9 | extension Notification.Name { 10 | static let newLocation = Notification.Name("newLocation") 11 | static let shake = Notification.Name("shake") 12 | } 13 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/PrawnConfettiView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrawnConfettiView.swift 3 | // Code Relay 4 | // 5 | // Created by John Martin on 10/10/2024. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | 11 | struct PrawnConfettiView: UIViewRepresentable { 12 | 13 | let numberOfConfetti: Int 14 | let animationDuration: Double 15 | let onComplete: () -> Void 16 | 17 | // For all your prawn tastes 18 | enum PrawnStyle: String { 19 | case 🦐 20 | case 🍤 21 | } 22 | let prawnStyle = PrawnStyle.🦐 23 | 24 | func makeUIView(context: Context) -> UIView { 25 | let view = UIView() 26 | 27 | let emitter = CAEmitterLayer() 28 | emitter.emitterShape = .line 29 | emitter.emitterPosition = CGPoint(x: UIScreen.main.bounds.width / 2, y: 0) 30 | emitter.emitterSize = CGSize(width: UIScreen.main.bounds.width, height: 1) 31 | emitter.renderMode = .oldestLast 32 | emitter.beginTime = CACurrentMediaTime() 33 | 34 | let velocity: CGFloat = 600 35 | let velocityRange: CGFloat = 200 36 | let screenHeight = UIScreen.main.bounds.height 37 | let maxVelocity = velocity + velocityRange 38 | let fallDuration = Double(screenHeight / maxVelocity) 39 | 40 | let cell = CAEmitterCell() 41 | cell.contents = prawnStyle.image().cgImage 42 | cell.birthRate = Float(numberOfConfetti) / Float(animationDuration) 43 | cell.lifetime = Float(fallDuration) + Float(animationDuration) 44 | cell.velocity = velocity 45 | cell.velocityRange = velocityRange 46 | cell.emissionLongitude = .pi 47 | cell.emissionRange = 0.6 48 | cell.scale = 0.6 49 | cell.scaleRange = 0.6 50 | cell.spin = 6 51 | cell.spinRange = 10 52 | cell.yAcceleration = 300 53 | 54 | emitter.emitterCells = [cell] 55 | 56 | view.layer.addSublayer(emitter) 57 | 58 | DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) { 59 | emitter.birthRate = 0 // Stop emitting new confetti 60 | } 61 | 62 | DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration + Double(cell.lifetime)) { 63 | onComplete() 64 | } 65 | 66 | return view 67 | } 68 | 69 | func updateUIView(_ uiView: UIView, context: Context) {} 70 | } 71 | 72 | extension PrawnConfettiView.PrawnStyle { 73 | func image() -> UIImage { 74 | let size = CGSize(width: 30, height: 30) 75 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 76 | UIColor.clear.set() 77 | let rect = CGRect(origin: .zero, size: size) 78 | UIRectFill(rect) 79 | (self.rawValue as NSString).draw(in: rect, withAttributes: [.font: UIFont.systemFont(ofSize: 30)]) 80 | let image = UIGraphicsGetImageFromCurrentImageContext()! 81 | UIGraphicsEndImageContext() 82 | return image 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // Code Relay 4 | // 5 | // Created by Mikaela Caron on 10/8/24. 6 | // 7 | 8 | import CachedAsyncImage 9 | import SwiftUI 10 | 11 | struct SettingsView: View { 12 | @StateObject var viewModel = SettingsViewModel() 13 | 14 | var body: some View { 15 | VStack { 16 | Text("Developed at ") + 17 | Text("SwiftLeeds ❤️") 18 | .font(.title) 19 | .bold() 20 | .foregroundStyle(.red) 21 | ScrollView { 22 | ForEach(viewModel.contributors, id: \.id) { contributor in 23 | ContributorCardView(contributor: contributor) 24 | .padding(.horizontal) 25 | } 26 | } 27 | } 28 | .task { 29 | await viewModel.fetchContributors() 30 | } 31 | } 32 | } 33 | 34 | private struct ContributorCardView: View { 35 | let contributor: Contributor 36 | 37 | var body: some View { 38 | HStack { 39 | CachedAsyncImage(url: URL(string: contributor.avatarUrl)) { image in 40 | image 41 | .resizable() 42 | } placeholder: { 43 | Image(systemName: "person.circle.fill") 44 | .resizable() 45 | .foregroundStyle(.gray) 46 | } 47 | .aspectRatio(contentMode: .fit) 48 | .frame(width: 50, height: 50) 49 | .clipShape(Circle()) 50 | 51 | VStack(alignment: .leading) { 52 | Text(contributor.login) 53 | .font(.headline) 54 | Text("\(contributor.contributions) contributions") 55 | .font(.subheadline) 56 | .foregroundColor(.secondary) 57 | } 58 | 59 | Spacer() 60 | } 61 | .padding() 62 | .background(Color.secondary.opacity(0.1)) 63 | .cornerRadius(10) 64 | } 65 | } 66 | 67 | #Preview { 68 | SettingsView() 69 | } 70 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/SettingsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewModel.swift 3 | // Code Relay 4 | // 5 | // Created by Gerald.Wood on 10/8/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | @MainActor final class SettingsViewModel: ObservableObject { 12 | @Published var contributors = [Contributor]() 13 | 14 | func fetchContributors() async { 15 | let repoURL = "https://api.github.com/repos/SwiftLeeds/code-relay/contributors" 16 | 17 | do { 18 | contributors = try await GithubNetworkManager.shared.getContributors(atURL: repoURL) 19 | } catch { 20 | // We would never really do this, but YOLO!!! 21 | contributors = [] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/SpinWheel/FlushyChartData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlushyChartData.swift 3 | // GetYourFlushy 4 | // 5 | // Created by Arnaud Wurmel on 09/10/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | struct FlushyChartData: Identifiable { 11 | let id = UUID() 12 | let prize: FlushyPrize 13 | let value: Double = 1 14 | } 15 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/SpinWheel/FlushyChartView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlushyChartView.swift 3 | // GetYourFlushy 4 | // 5 | // Created by Arnaud Wurmel on 09/10/2024. 6 | // 7 | 8 | import Charts 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct FlushyChartView: View { 13 | let isAnimated: Bool 14 | 15 | let data: [FlushyChartData] 16 | 17 | private var total: Double { 18 | Double(data.count) 19 | } 20 | 21 | var body: some View { 22 | Chart { 23 | ForEach(data) { flushyChartData in 24 | if !isAnimated { 25 | SectorMark( 26 | angle: .value("Value", flushyChartData.value), 27 | innerRadius: .ratio(0.2), 28 | angularInset: 0 29 | ) 30 | .annotation(position: .overlay) { 31 | if !isAnimated { 32 | Text(flushyChartData.prize.rawValue) 33 | } 34 | } 35 | .foregroundStyle(by: .value("Category", flushyChartData.prize.rawValue)) 36 | } else { 37 | SectorMark( 38 | angle: .value("Value", flushyChartData.value), 39 | innerRadius: .ratio(0.2), 40 | angularInset: 1 41 | ) 42 | .foregroundStyle(by: .value("Category", flushyChartData.prize.rawValue)) 43 | } 44 | } 45 | } 46 | .chartLegend(.hidden) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/SpinWheel/FlushyPrize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartPrize.swift 3 | // GetYourFlushy 4 | // 5 | // Created by Arnaud Wurmel on 09/10/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | enum FlushyPrize: String, CaseIterable { 11 | case sticker 12 | case plushie 13 | case pin 14 | } 15 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/SpinWheel/SpinWheelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpinWheelView.swift 3 | // GetYourFlushy 4 | // 5 | // Created by Arnaud Wurmel on 09/10/2024. 6 | // 7 | 8 | import Charts 9 | import SwiftUI 10 | 11 | struct SpinWheelView: View { 12 | @StateObject var viewModel = SpinWheelViewModel() 13 | 14 | var body: some View { 15 | VStack { 16 | Text("If you also desired to have one of those flushy cats, train your luck before!") 17 | 18 | FlushyChartView(isAnimated: viewModel.isAnimating, data: viewModel.data) 19 | .animation(.smooth, value: viewModel.rotationAngle) 20 | .rotationEffect(.degrees(viewModel.rotationAngle)) 21 | .overlay(alignment: .top) { 22 | Image(systemName: "triangle.fill") 23 | .font(.largeTitle) 24 | .rotationEffect(.degrees(180)) 25 | .padding(.top, 70) 26 | .foregroundStyle(.red) 27 | .shadow(radius: 10) 28 | } 29 | 30 | Spacer() 31 | } 32 | .safeAreaInset(edge: .bottom) { 33 | Button("Spin the wheel") { 34 | viewModel.reset() 35 | 36 | withAnimation { 37 | viewModel.spinWheel() 38 | } completion: { 39 | viewModel.animationDidComplete() 40 | } 41 | } 42 | .buttonStyle(.borderedProminent) 43 | .disabled(viewModel.isAnimating) 44 | } 45 | .padding() 46 | .navigationTitle("Train your luck") 47 | } 48 | } 49 | 50 | #Preview { 51 | NavigationView { 52 | SpinWheelView() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Code Relay/Code Relay/SpinWheel/SpinWheelViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpinWheelViewModel.swift 3 | // GetYourFlushy 4 | // 5 | // Created by Arnaud Wurmel on 09/10/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | class SpinWheelViewModel: ObservableObject { 11 | let data: [FlushyChartData] = Array( 12 | repeating: FlushyPrize.allCases.map { 13 | FlushyChartData(prize: $0) 14 | }, 15 | count: 4 16 | ).flatMap { $0 } 17 | 18 | @Published private(set) var isAnimating: Bool = false 19 | @Published private(set) var rotationAngle: Double = 0 20 | 21 | func spinWheel() { 22 | let initialNumberOfSpins: Double = 5 23 | 24 | let wonItemRatio = Double.random(in: 0...1) 25 | 26 | isAnimating = true 27 | rotationAngle = (initialNumberOfSpins + wonItemRatio) * 360 28 | } 29 | 30 | func animationDidComplete() { 31 | isAnimating = false 32 | } 33 | 34 | func reset() { 35 | rotationAngle = 0 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Xcode - Build](https://github.com/SwiftLeeds/code-relay/actions/workflows/xcode.yml/badge.svg?event=push)](https://github.com/SwiftLeeds/code-relay/actions/workflows/xcode.yml) 2 | 3 | # Welcome to SwiftLeeds 24 4 | 5 | The ultimate coding relay hosted during the SwiftLeeds 2024 conference. You have probably landed here because you're an attendee at the conference, welcome 🙏🏼 6 | 7 | ## Code Relay 8 | 9 | As part of the SwiftLeeds 2024 conference theme, we're hosting a fun coding game during the 2 days of the conference, that we're calling Code Relay 🏆! 10 | 11 | Inside this repository you will find an Xcode Project, which is a very simple application. If you receive the baton during the conference then move to the next section. 12 | 13 | ## I have the baton, what now? 👀 14 | 15 | 1. Clone the repository 16 | 2. Open the Xcode project 17 | 3. Add some code 👩‍💻 18 | 4. Push your changes to a branch 19 | 5. Raise a Pull Request to main 20 | 21 | Your mission is to add _something_ to the application, it might be a very simple Settings screen, or a new feature. that you think would be awesome, anything. 22 | 23 | The only thing we ask is make sure it's compiles 😝! 24 | 25 | ## I have pushed some changes, what now? 26 | 27 | Pass the baton to **anyone** in the conference, don't be shy! If they don't want the baton find someone else! 28 | 29 | Let's build something great together, who knows we might just be able to ship it 🚢. 30 | 31 | ## Thanks, but what do I get? 32 | 33 | Satisfaction, enjoyment... maybe something else? 🤟🏼! 34 | --------------------------------------------------------------------------------