├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── Icon.png ├── LICENSE ├── README.md ├── SECURITY.md ├── Settings.bundle ├── Acknowledgements.plist ├── Root.plist └── en.lproj │ └── Root.strings ├── SwiftLeeds.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── SwiftLeeds.xcscheme │ └── SwiftLeedsAppClip.xcscheme ├── SwiftLeeds ├── App │ ├── AppState.swift │ ├── Constants.swift │ ├── Info.plist │ └── SwiftLeedsApp.swift ├── Data │ └── Model │ │ ├── Activity.swift │ │ ├── Local.swift │ │ ├── Presentation.swift │ │ ├── Schedule.swift │ │ ├── Speaker.swift │ │ └── Sponsor.swift ├── Extension │ ├── Calendar.swift │ ├── Color.swift │ ├── Date.swift │ ├── LinearGradient.swift │ ├── String.swift │ └── View+MeasureSize.swift ├── Network │ ├── Endpoints.swift │ ├── HttpMethod.swift │ ├── Request.swift │ ├── Requests.swift │ └── URLSession.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Push │ ├── AppDelegate+Push.swift │ └── TokenDetails.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── AppIcon.png │ │ │ └── Contents.json │ │ ├── CarriageworksTheatre.imageset │ │ │ ├── CarriageworksTheatre.jpg │ │ │ └── Contents.json │ │ ├── Clock.imageset │ │ │ ├── Clock.pdf │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Icon.imageset │ │ │ ├── Contents.json │ │ │ ├── Icon-Dark.pdf │ │ │ └── Icon.pdf │ │ ├── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchScreen-Dark.png │ │ │ ├── LaunchScreen-Dark@2x.png │ │ │ ├── LaunchScreen-Dark@3x.png │ │ │ ├── LaunchScreen.png │ │ │ ├── LaunchScreen@2x.png │ │ │ └── LaunchScreen@3x.png │ │ ├── LeedsPlayhouse.imageset │ │ │ ├── Contents.json │ │ │ └── LeedsPlayhouse.jpeg │ │ ├── SwiftLeedsIcon.imageset │ │ │ ├── Contents.json │ │ │ └── SwiftLeedsIcon.pdf │ │ └── wineglass.fill.symbolset │ │ │ ├── Contents.json │ │ │ └── wineglass.fill.svg │ └── Colors.xcassets │ │ ├── AccentColor.colorset │ │ └── Contents.json │ │ ├── Background.colorset │ │ └── Contents.json │ │ ├── CellBackground.colorset │ │ └── Contents.json │ │ ├── CellBorder.colorset │ │ └── Contents.json │ │ ├── CellForeground.colorset │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Gradients │ │ ├── BuyTicketGradientEnd.colorset │ │ │ └── Contents.json │ │ ├── BuyTicketGradientStart.colorset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── LaunchScreenBackground.colorset │ │ └── Contents.json │ │ ├── ListBackground.colorset │ │ └── Contents.json │ │ └── TabBarBackground.colorset │ │ └── Contents.json ├── Style │ └── SquishyButtonStyle.swift ├── SwiftLeeds.entitlements └── Views │ ├── About │ └── AboutView.swift │ ├── Common │ ├── Helper.swift │ ├── SectionHeader.swift │ └── SwiftLeedsContainer.swift │ ├── Components │ ├── CommonTileButton.swift │ ├── CommonTileView.swift │ ├── FancyHeaderView.swift │ ├── HeaderView.swift │ ├── StackedTileView.swift │ └── WebView.swift │ ├── Local │ ├── BottomSheetView.swift │ ├── LocalCell.swift │ ├── LocalView.swift │ └── LocalViewModel.swift │ ├── My Conference │ ├── ActivityView.swift │ ├── AnnouncementCell.swift │ ├── MyConferenceView.swift │ ├── MyConferenceViewModel.swift │ ├── ScheduleView.swift │ ├── SpeakerView.swift │ └── TalkCell.swift │ ├── Sponsors │ ├── SponsorTileView.swift │ ├── SponsorsView.swift │ └── SponsorsViewModel.swift │ └── Tab │ ├── SidebarMainView.swift │ ├── SidebarView.swift │ ├── Tabs.swift │ └── TabsMainView.swift ├── SwiftLeedsAppClip ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SwiftLeedsAppClip.entitlements └── SwiftLeedsAppClipApp.swift ├── SwiftLeedsTests └── SwiftLeedsTests.swift ├── SwiftLeedsUITests ├── SwiftLeedsUITests.swift └── SwiftLeedsUITestsLaunchTests.swift ├── SwiftLeedsWidget ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── AppIcon.png │ │ └── Contents.json │ ├── Contents.json │ └── WidgetBackground.colorset │ │ └── Contents.json ├── Info.plist ├── SwiftLeedsMediumWidgetView.swift ├── SwiftLeedsSmallWidgetView.swift ├── SwiftLeedsWidget.swift ├── SwiftLeedsWidgetEntryView.swift └── WidgetSetup │ ├── SwiftLeedsWidgetEntry.swift │ ├── TimeineProvider.swift │ └── WidgetConstants.swift ├── SwiftLeedsWidgetExtension.entitlements ├── appClipCode.svg ├── fastlane ├── Appfile ├── Fastfile └── README.md └── media └── swift-leeds-logo.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: SwiftLeeds 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: iOS CI Build Workflow 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: Build SwiftLeeds (DEBUG) 12 | runs-on: macos-13 13 | steps: 14 | - name: Checkout iOS Repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Build iOS Project via Fastlane 18 | uses: maierj/fastlane-action@v2.2.1 19 | with: 20 | lane: build_debug 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## Obj-C/Swift specific 9 | *.hmap 10 | 11 | ## App packaging 12 | *.ipa 13 | *.dSYM.zip 14 | *.dSYM 15 | 16 | ## Playgrounds 17 | timeline.xctimeline 18 | playground.xcworkspace 19 | 20 | # Swift Package Manager 21 | # 22 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 23 | # Packages/ 24 | # Package.pins 25 | # Package.resolved 26 | # *.xcodeproj 27 | # 28 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 29 | # hence it is not needed unless you have added a package configuration file to your project 30 | # .swiftpm 31 | 32 | .build/ 33 | *.DS_Store 34 | 35 | # CocoaPods 36 | # 37 | # We recommend against adding the Pods directory to your .gitignore. However 38 | # you should judge for yourself, the pros and cons are mentioned at: 39 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 40 | # 41 | # Pods/ 42 | # 43 | # Add this line if you want to avoid checking in source code from the Xcode workspace 44 | # *.xcworkspace 45 | 46 | # Carthage 47 | # 48 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 49 | # Carthage/Checkouts 50 | 51 | Carthage/Build/ 52 | 53 | # Accio dependency management 54 | Dependencies/ 55 | .accio/ 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. 60 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots/**/*.png 67 | fastlane/test_output 68 | 69 | # Code Injection 70 | # 71 | # After new code Injection tools there's a generated folder /iOSInjectionProject 72 | # https://github.com/johnno1962/injectionforxcode 73 | 74 | iOSInjectionProject/ 75 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.4 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'fastlane', '2.206.2' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | addressable (2.8.0) 7 | public_suffix (>= 2.0.2, < 5.0) 8 | artifactory (3.0.15) 9 | atomos (0.1.3) 10 | aws-eventstream (1.2.0) 11 | aws-partitions (1.601.0) 12 | aws-sdk-core (3.131.2) 13 | aws-eventstream (~> 1, >= 1.0.2) 14 | aws-partitions (~> 1, >= 1.525.0) 15 | aws-sigv4 (~> 1.1) 16 | jmespath (~> 1, >= 1.6.1) 17 | aws-sdk-kms (1.57.0) 18 | aws-sdk-core (~> 3, >= 3.127.0) 19 | aws-sigv4 (~> 1.1) 20 | aws-sdk-s3 (1.114.0) 21 | aws-sdk-core (~> 3, >= 3.127.0) 22 | aws-sdk-kms (~> 1) 23 | aws-sigv4 (~> 1.4) 24 | aws-sigv4 (1.5.0) 25 | aws-eventstream (~> 1, >= 1.0.2) 26 | babosa (1.0.4) 27 | claide (1.1.0) 28 | colored (1.2) 29 | colored2 (3.1.2) 30 | commander (4.6.0) 31 | highline (~> 2.0.0) 32 | declarative (0.0.20) 33 | digest-crc (0.6.4) 34 | rake (>= 12.0.0, < 14.0.0) 35 | domain_name (0.5.20190701) 36 | unf (>= 0.0.5, < 1.0.0) 37 | dotenv (2.7.6) 38 | emoji_regex (3.2.3) 39 | excon (0.92.3) 40 | faraday (1.10.0) 41 | faraday-em_http (~> 1.0) 42 | faraday-em_synchrony (~> 1.0) 43 | faraday-excon (~> 1.1) 44 | faraday-httpclient (~> 1.0) 45 | faraday-multipart (~> 1.0) 46 | faraday-net_http (~> 1.0) 47 | faraday-net_http_persistent (~> 1.0) 48 | faraday-patron (~> 1.0) 49 | faraday-rack (~> 1.0) 50 | faraday-retry (~> 1.0) 51 | ruby2_keywords (>= 0.0.4) 52 | faraday-cookie_jar (0.0.7) 53 | faraday (>= 0.8.0) 54 | http-cookie (~> 1.0.0) 55 | faraday-em_http (1.0.0) 56 | faraday-em_synchrony (1.0.0) 57 | faraday-excon (1.1.0) 58 | faraday-httpclient (1.0.1) 59 | faraday-multipart (1.0.4) 60 | multipart-post (~> 2) 61 | faraday-net_http (1.0.1) 62 | faraday-net_http_persistent (1.2.0) 63 | faraday-patron (1.0.0) 64 | faraday-rack (1.0.0) 65 | faraday-retry (1.0.3) 66 | faraday_middleware (1.2.0) 67 | faraday (~> 1.0) 68 | fastimage (2.2.6) 69 | fastlane (2.206.2) 70 | CFPropertyList (>= 2.3, < 4.0.0) 71 | addressable (>= 2.8, < 3.0.0) 72 | artifactory (~> 3.0) 73 | aws-sdk-s3 (~> 1.0) 74 | babosa (>= 1.0.3, < 2.0.0) 75 | bundler (>= 1.12.0, < 3.0.0) 76 | colored 77 | commander (~> 4.6) 78 | dotenv (>= 2.1.1, < 3.0.0) 79 | emoji_regex (>= 0.1, < 4.0) 80 | excon (>= 0.71.0, < 1.0.0) 81 | faraday (~> 1.0) 82 | faraday-cookie_jar (~> 0.0.6) 83 | faraday_middleware (~> 1.0) 84 | fastimage (>= 2.1.0, < 3.0.0) 85 | gh_inspector (>= 1.1.2, < 2.0.0) 86 | google-apis-androidpublisher_v3 (~> 0.3) 87 | google-apis-playcustomapp_v1 (~> 0.1) 88 | google-cloud-storage (~> 1.31) 89 | highline (~> 2.0) 90 | json (< 3.0.0) 91 | jwt (>= 2.1.0, < 3) 92 | mini_magick (>= 4.9.4, < 5.0.0) 93 | multipart-post (~> 2.0.0) 94 | naturally (~> 2.2) 95 | optparse (~> 0.1.1) 96 | plist (>= 3.1.0, < 4.0.0) 97 | rubyzip (>= 2.0.0, < 3.0.0) 98 | security (= 0.1.3) 99 | simctl (~> 1.6.3) 100 | terminal-notifier (>= 2.0.0, < 3.0.0) 101 | terminal-table (>= 1.4.5, < 2.0.0) 102 | tty-screen (>= 0.6.3, < 1.0.0) 103 | tty-spinner (>= 0.8.0, < 1.0.0) 104 | word_wrap (~> 1.0.0) 105 | xcodeproj (>= 1.13.0, < 2.0.0) 106 | xcpretty (~> 0.3.0) 107 | xcpretty-travis-formatter (>= 0.0.3) 108 | gh_inspector (1.1.3) 109 | google-apis-androidpublisher_v3 (0.23.0) 110 | google-apis-core (>= 0.6, < 2.a) 111 | google-apis-core (0.6.0) 112 | addressable (~> 2.5, >= 2.5.1) 113 | googleauth (>= 0.16.2, < 2.a) 114 | httpclient (>= 2.8.1, < 3.a) 115 | mini_mime (~> 1.0) 116 | representable (~> 3.0) 117 | retriable (>= 2.0, < 4.a) 118 | rexml 119 | webrick 120 | google-apis-iamcredentials_v1 (0.12.0) 121 | google-apis-core (>= 0.6, < 2.a) 122 | google-apis-playcustomapp_v1 (0.9.0) 123 | google-apis-core (>= 0.6, < 2.a) 124 | google-apis-storage_v1 (0.16.0) 125 | google-apis-core (>= 0.6, < 2.a) 126 | google-cloud-core (1.6.0) 127 | google-cloud-env (~> 1.0) 128 | google-cloud-errors (~> 1.0) 129 | google-cloud-env (1.6.0) 130 | faraday (>= 0.17.3, < 3.0) 131 | google-cloud-errors (1.2.0) 132 | google-cloud-storage (1.36.2) 133 | addressable (~> 2.8) 134 | digest-crc (~> 0.4) 135 | google-apis-iamcredentials_v1 (~> 0.1) 136 | google-apis-storage_v1 (~> 0.1) 137 | google-cloud-core (~> 1.6) 138 | googleauth (>= 0.16.2, < 2.a) 139 | mini_mime (~> 1.0) 140 | googleauth (1.2.0) 141 | faraday (>= 0.17.3, < 3.a) 142 | jwt (>= 1.4, < 3.0) 143 | memoist (~> 0.16) 144 | multi_json (~> 1.11) 145 | os (>= 0.9, < 2.0) 146 | signet (>= 0.16, < 2.a) 147 | highline (2.0.3) 148 | http-cookie (1.0.5) 149 | domain_name (~> 0.5) 150 | httpclient (2.8.3) 151 | jmespath (1.6.1) 152 | json (2.6.2) 153 | jwt (2.4.1) 154 | memoist (0.16.2) 155 | mini_magick (4.11.0) 156 | mini_mime (1.1.2) 157 | multi_json (1.15.0) 158 | multipart-post (2.0.0) 159 | nanaimo (0.3.0) 160 | naturally (2.2.1) 161 | optparse (0.1.1) 162 | os (1.1.4) 163 | plist (3.6.0) 164 | public_suffix (4.0.7) 165 | rake (13.0.6) 166 | representable (3.2.0) 167 | declarative (< 0.1.0) 168 | trailblazer-option (>= 0.1.1, < 0.2.0) 169 | uber (< 0.2.0) 170 | retriable (3.1.2) 171 | rexml (3.2.5) 172 | rouge (2.0.7) 173 | ruby2_keywords (0.0.5) 174 | rubyzip (2.3.2) 175 | security (0.1.3) 176 | signet (0.17.0) 177 | addressable (~> 2.8) 178 | faraday (>= 0.17.5, < 3.a) 179 | jwt (>= 1.5, < 3.0) 180 | multi_json (~> 1.10) 181 | simctl (1.6.8) 182 | CFPropertyList 183 | naturally 184 | terminal-notifier (2.0.0) 185 | terminal-table (1.8.0) 186 | unicode-display_width (~> 1.1, >= 1.1.1) 187 | trailblazer-option (0.1.2) 188 | tty-cursor (0.7.1) 189 | tty-screen (0.8.1) 190 | tty-spinner (0.9.3) 191 | tty-cursor (~> 0.7) 192 | uber (0.1.0) 193 | unf (0.1.4) 194 | unf_ext 195 | unf_ext (0.0.8.2) 196 | unicode-display_width (1.8.0) 197 | webrick (1.7.0) 198 | word_wrap (1.0.0) 199 | xcodeproj (1.22.0) 200 | CFPropertyList (>= 2.3.3, < 4.0) 201 | atomos (~> 0.1.3) 202 | claide (>= 1.0.2, < 2.0) 203 | colored2 (~> 3.1) 204 | nanaimo (~> 0.3.0) 205 | rexml (~> 3.2.4) 206 | xcpretty (0.3.0) 207 | rouge (~> 2.0.7) 208 | xcpretty-travis-formatter (1.0.1) 209 | xcpretty (~> 0.2, >= 0.0.7) 210 | 211 | PLATFORMS 212 | x86_64-darwin-21 213 | 214 | DEPENDENCIES 215 | fastlane (= 2.206.2) 216 | 217 | BUNDLED WITH 218 | 2.2.33 219 | -------------------------------------------------------------------------------- /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/Icon.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SwiftLeeds 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ![Platforms](https://img.shields.io/badge/Platforms-iOS-lightgrey.svg) 4 | ![Swift](https://img.shields.io/badge/Swift-5.8-F16D39.svg) 5 | ![Contributions welcome](https://img.shields.io/badge/contributions-welcome-orange.svg) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 7 | 8 | 9 | ## Introduction 👋🏼 10 | 11 | [SwiftLeeds](https://swiftleeds.co.uk) is a brand new Swift conference that started in 2021. It's a truly unique conference that is built by the community for the community. Hosted in the heart of Leeds City, UK. 12 | 13 | ## Developer Setup 💻 14 | 15 | Follow these steps to get your development environment setup to run SwiftLeeds iOS app locally. 16 | 17 | ### Prerequisites 18 | 19 | You'll need the following installed to get started: 20 | 21 | ``` 22 | Xcode 14.3.1 23 | ``` 24 | 25 | #### Xcode 26 | 27 | Xcode 14.0 can be downloaded directly from the [Apple Developer Downloads Page](https://download.developer.apple.com/Developer_Tools/Xcode_14.3.1/Xcode_14.3.1.xip). 28 | 29 | _You will need to have access to your developer account in order to download this._ 30 | 31 | - Once downloaded and unzipped, move the app to your Applications folder and run it. 32 | - Accept the prompt to install additional tools to allow it to run until you see the Xcode welcome pane with the new project button. 33 | 34 | ## Contributing 🏗 35 | 36 | We welcome all contributions to this repository. Please raise a PR so our Lead Maintainer (Matthew Gallagher) can help get this pushed through, alternatively please raise an Issue or Discussion topic. 37 | 38 | ### Branch Stratergy 39 | 40 | We branch off the *main* branch into a *Feature branch* and then generate a PR to merge the changes back into *main*. We do not merge from any branch other than *main*. 41 | 42 | 43 | ## Contributors 44 | 45 | ### Active Contributors 46 | - [Matthew Gallagher](https://github.com/pdamonkey) 47 | - [Adam Rush](https://github.com/adamrushy) 48 | - [Karim Ebrahem](https://github.com/KarimEbrahemAbdelaziz) 49 | - [Lucky Agarwal](https://github.com/luckyagarwal) 50 | - [Muralidharan Kathiresan](https://github.com/kmuralidharan91) 51 | 52 | ### Previous Contributors 53 | - [Alex Logan](https://github.com/SwiftyAlex) 54 | - [Kannan Prasad](https://github.com/kannanprasad87) 55 | 56 | Thanks to all the effors from our App Contributors: 57 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /Settings.bundle/Acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | MIT License 10 | 11 | Copyright (c) 2021 Lorenzo Fiamingo 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | License 19 | MIT 20 | Title 21 | SwiftUI CachedAsyncImage 22 | Type 23 | PSGroupSpecifier 24 | 25 | 26 | StringsTable 27 | Acknowledgements 28 | Title 29 | Acknowledgements 30 | 31 | 32 | -------------------------------------------------------------------------------- /Settings.bundle/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StringsTable 6 | Root 7 | PreferenceSpecifiers 8 | 9 | 10 | Type 11 | PSChildPaneSpecifier 12 | Title 13 | ACKNOWLEDGEMENTS 14 | File 15 | Acknowledgements 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Settings.bundle/en.lproj/Root.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/Settings.bundle/en.lproj/Root.strings -------------------------------------------------------------------------------- /SwiftLeeds.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftLeeds.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftLeeds.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "readabilitymodifier", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/yazio/ReadabilityModifier", 7 | "state" : { 8 | "revision" : "ce162150a090d5ae54a682d1f6be3862a3ad3ad4", 9 | "version" : "1.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swiftui-cached-async-image", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/lorenzofiamingo/swiftui-cached-async-image", 16 | "state" : { 17 | "revision" : "467a3d17479887943ab917a379e62bbaff60ac8a", 18 | "version" : "2.1.1" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /SwiftLeeds.xcodeproj/xcshareddata/xcschemes/SwiftLeeds.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /SwiftLeeds.xcodeproj/xcshareddata/xcschemes/SwiftLeedsAppClip.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /SwiftLeeds/App/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // SwiftLeeds 4 | // 5 | // Created by karim ebrahim on 25/07/2023. 6 | // 7 | 8 | import Foundation 9 | enum TabItems: Int { 10 | case conference, location, about, sponsors 11 | } 12 | 13 | final class AppState: ObservableObject { 14 | var selectedTab: TabItems = .conference 15 | } 16 | -------------------------------------------------------------------------------- /SwiftLeeds/App/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 29/06/2022. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | enum Constants { 11 | static let cellRadius: CGFloat = 12 12 | static let compactCellMinimumHeight: CGFloat = 53 13 | static let cellMinimumHeight: CGFloat = 65 14 | static let bottomSheetRadius: CGFloat = 30 15 | static let minHeightRatio: CGFloat = 0.3 16 | static let maxHeightRatio: CGFloat = 0.5 17 | static let snapRatio: CGFloat = 0.25 18 | } 19 | 20 | enum Padding { 21 | static let screen: CGFloat = 16 22 | static let cell: CGFloat = 12 23 | static let cellGap: CGFloat = 16 24 | static let stackGap: CGFloat = 4 25 | } 26 | 27 | enum Strings { 28 | static let aboutSwiftLeeds = """ 29 | Adam Rush founded SwiftLeeds in 2019, born from over ten years of experience attending conferences. The inspiration was bringing a modern, inclusive conference in the North of the UK to be more accessible for all. 30 | 31 | SwiftLeeds is now run with over ten community volunteers building the website, iOS applications and making sure we cover all the bases on the day. SwiftLeeds is entirely non-profit, and the funds make sure we can deliver the best experience possible. 32 | 33 | In-person conferences are the best way to meet like-minded people who enjoy building apps with Swift. You can also learn from the best people in the industry and chat about all things Swift. 34 | """ 35 | static let aboutContributor = """ 36 | SwiftLeeds is a conference for the community, by the community. Here's the people who helped to bring you the conference this year. 37 | """ 38 | } 39 | 40 | enum Assets { 41 | enum Image { 42 | static let carriageworksTheatre = "CarriageworksTheatre" 43 | static let leedsPlayhouse = "LeedsPlayhouse" 44 | static let swiftLeedsIcon = "SwiftLeedsIcon" 45 | static let swiftLeedsIconWithNoBackground = "Icon" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SwiftLeeds/App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UILaunchScreen 6 | 7 | UIColorName 8 | LaunchScreenBackground 9 | UIImageName 10 | LaunchImage 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /SwiftLeeds/App/SwiftLeedsApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLeedsApp.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 14/11/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SwiftLeedsApp: App { 12 | @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate 13 | 14 | @StateObject private var appState = AppState() 15 | 16 | var body: some Scene { 17 | WindowGroup { 18 | Tabs() 19 | .environmentObject(appState) 20 | .onContinueUserActivity(NSUserActivityTypeBrowsingWeb, perform: handleUserActivity) 21 | } 22 | } 23 | } 24 | 25 | // MARK: - AppDelegate 26 | final class AppDelegate: NSObject, UIApplicationDelegate { 27 | static let pushURL: String = "https://www.swiftleeds.co.uk/push" 28 | 29 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 30 | URLCache.shared.diskCapacity = 100_000_000 31 | 32 | UITabBar.appearance().backgroundColor = UIColor(named: "TabBarBackground") 33 | 34 | requestPushAuthorization(application: application) 35 | 36 | return true 37 | } 38 | 39 | func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 40 | guard let url = URL(string: Self.pushURL) else { print("⛔️ Invalid push URL"); return } 41 | sendPushRegistrationDatails(to: url, deviceToken: deviceToken) 42 | } 43 | 44 | func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { 45 | print("⛔️ Push registration failed:", error) 46 | handleFailedRegistration(application: application, error: error) 47 | } 48 | } 49 | 50 | private extension SwiftLeedsApp { 51 | func handleUserActivity(_ userActivity: NSUserActivity) { 52 | guard let incomingURL = userActivity.webpageURL, let components = URLComponents( 53 | url: incomingURL, resolvingAgainstBaseURL: true), let queryItems = components.queryItems 54 | else { return } 55 | print(queryItems) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SwiftLeeds/Data/Model/Activity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Activity.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 31/08/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Activity 11 | struct Activity: Codable, Identifiable { 12 | let id: UUID 13 | let title: String 14 | let subtitle: String? 15 | let description: String? 16 | let image: String? 17 | let metadataURL: String? 18 | } 19 | 20 | // MARK: - Static Data 21 | extension Activity { 22 | static let lunch = Activity(id: UUID(), 23 | title: "Lunch 🍕", 24 | subtitle: "It's time for some well deserved food", 25 | description: "We have partnered with the venue to provide us with handmade food. The venue has an incredible chef who will produce food to cater to everyone. They have access to a stone-baked pizza oven to provide fresh pizza slices and handmade buffet food with a vast selection. Don't forget your handmade brownie or Bakewell slice 😋", 26 | image: "IMG_6298.jpg-93D1F0E2-6F47-4149-944B-FB824EFB2549", 27 | metadataURL: "") 28 | } 29 | -------------------------------------------------------------------------------- /SwiftLeeds/Data/Model/Local.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Local.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 08/08/2022. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | 11 | struct Local: Decodable { 12 | let data: [LocationCategory] 13 | 14 | struct LocationCategory: Decodable, Identifiable, Equatable { 15 | let id: UUID 16 | let name: String 17 | let symbolName: String 18 | let locations: [Location] 19 | } 20 | 21 | struct Location: Decodable, Identifiable, Equatable { 22 | let id: UUID 23 | let name: String 24 | let url: URL 25 | let location: CLLocation 26 | } 27 | } 28 | 29 | extension Local.Location { 30 | init(from decoder: Decoder) throws { 31 | let values = try decoder.container(keyedBy: CodingKeys.self) 32 | 33 | id = try values.decode(UUID.self, forKey: .id) 34 | name = try values.decode(String.self, forKey: .name) 35 | url = try values.decode(URL.self, forKey: .url) 36 | 37 | let latitude = try values.decode(Double.self, forKey: .latitude) 38 | let longitude = try values.decode(Double.self, forKey: .longitude) 39 | location = CLLocation(latitude: latitude, longitude: longitude) 40 | } 41 | 42 | private enum CodingKeys: String, CodingKey { 43 | case id, name, latitude = "lat", longitude = "lon", url 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SwiftLeeds/Data/Model/Presentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Presentation.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 31/08/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Presentation 11 | struct Presentation: Codable, Identifiable { 12 | let id: UUID 13 | let title: String 14 | let synopsis: String 15 | let speakers: [Speaker] 16 | let image: String? 17 | let slidoURL: String? 18 | let videoURL: String? 19 | } 20 | 21 | // MARK: - Static Data 22 | extension Presentation { 23 | static let donnyWalls = Presentation(id: UUID(), title: "Building (and testing) custom property wrappers for SwiftUI", synopsis: "In this talk, you will learn everything you need to know about using DynamicProperty to build custom property wrappers that integrate with SwiftUI’s view lifecycle and environment beautifully. And more importantly, you will learn how you can write unit tests for your custom property wrappers as well.", speakers: [.init(id: UUID(), name: "Donny Wals", biography: "I'm a curious, passionate iOS Developer from The Netherlands who loves learning and sharing knowledge.", profileImage: "https://swiftleeds-speakers.s3.eu-west-2.amazonaws.com/jOaeQ1Og_400x400.jpeg-AEAB9C2A-9572-4E6A-A63E-C3534EE5C321", organisation: "DonnyWals.com", twitter: "donnywals")], image: nil, slidoURL: "https://app.sli.do/event/2x7itwrn", videoURL: nil) 24 | 25 | static let skyBet = Presentation(id: UUID(), title: "UI automation with XCUItest", synopsis: "In this duo talk with Poornima and Sanaa, we'll be exploring how to create the most efficient UI automation using Apple's UI automation frameworks. We'll be covering:\r\n\r\n- What is UI automation testing\r\n- Setting up the framework\r\n- BDD\r\n- Base Class\r\n- Page Object Model\r\n- Data mocking\r\n- Execution: Test Plans/test schemes\r\n- Test Results: Reporting\r\n- Debugging - breakpoints, prints, etc\r\n- CI\r\n- Pros and cons", speakers: [.init(id: UUID(), name: "Poornima Suraj", biography: "16 years of experience in IT with more than 14 years working exclusively on Automation testing.", profileImage: "https://swiftleeds-speakers.s3.eu-west-2.amazonaws.com/4769-0o0o0-YroTPVoSrXCZvVwZXEvMUt.png-0DCB39D9-EA7C-4DCA-B4B2-B4712D2A8FCE", organisation: "Sky Betting & Gaming", twitter: nil), .init(id: UUID(), name: "Sanaa Shahzadi", biography: "Currently, an iOS Engineer at Sky Betting and Gaming previously worked as a Software Engineer in Test (SEiT).", profileImage: "https://swiftleeds-speakers.s3.eu-west-2.amazonaws.com/5b00-0o0o0-nwCWuRFWhXd8QZVQBSB5G2.jpeg-3A19F66F-6A15-44E7-810E-EDF37463B537", organisation: "Sky Betting & Gaming", twitter: "SanaaShahzadi")], image: nil, slidoURL: nil, videoURL: "https://www.youtube.com/watch?v=x4ZAh-iNQO8&t=2s") 26 | } 27 | -------------------------------------------------------------------------------- /SwiftLeeds/Data/Model/Schedule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Schedule.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 01/08/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Schedule: Decodable { 11 | let data: Data 12 | 13 | struct Data: Decodable { 14 | let event: Event 15 | let events: [Event] 16 | let slots: [Slot] 17 | } 18 | 19 | struct Event: Decodable, Identifiable { 20 | let id: UUID 21 | let name: String 22 | let location: String 23 | let date: Date 24 | 25 | var daysUntil: Int { 26 | Calendar.current.numberOfDays(to: date) 27 | } 28 | } 29 | 30 | struct Slot: Identifiable { 31 | let id: UUID 32 | let date: Date? 33 | let startTime: String 34 | let duration: Int 35 | let activity: Activity? 36 | let presentation: Presentation? 37 | 38 | private enum CodingKeys: CodingKey { 39 | case id, activity, presentation, date, startTime, duration 40 | } 41 | 42 | static var timeFormat: DateFormatter = { 43 | let dateFormatter = DateFormatter() 44 | dateFormatter.dateFormat = "HH:mm" 45 | return dateFormatter 46 | }() 47 | } 48 | } 49 | 50 | // MARK: - Slot Decodable 51 | extension Schedule.Slot: Codable { 52 | init(from decoder: Decoder) throws { 53 | let values = try decoder.container(keyedBy: CodingKeys.self) 54 | 55 | id = try values.decode(UUID.self, forKey: .id) 56 | startTime = try values.decode(String.self, forKey: .startTime) 57 | duration = try values.decode(Int.self, forKey: .duration) 58 | 59 | let date = try values.decodeIfPresent(String.self, forKey: .date) ?? "" 60 | self.date = ISO8601DateFormatter().date(from: date) 61 | 62 | if let activity = try values.decodeIfPresent(Activity.self, forKey: .activity) { 63 | self.activity = activity 64 | self.presentation = nil 65 | } else if let presentation = try values.decodeIfPresent(Presentation.self, forKey: .presentation) { 66 | self.activity = nil 67 | self.presentation = presentation 68 | } else { 69 | throw(SlotError.invalidSlot) 70 | } 71 | } 72 | 73 | enum SlotError: Error { 74 | case invalidSlot 75 | } 76 | } 77 | 78 | // MARK: - Slot Equatable 79 | extension Schedule.Slot: Equatable { 80 | static func == (lhs: Schedule.Slot, rhs: Schedule.Slot) -> Bool { 81 | lhs.id == rhs.id 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SwiftLeeds/Data/Model/Speaker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Speaker.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 31/08/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Speaker: Codable, Identifiable { 11 | let id: UUID 12 | let name: String 13 | let biography: String 14 | let profileImage: String 15 | let organisation: String 16 | let twitter: String? 17 | } 18 | 19 | // MARK: - Formatting helpers 20 | extension Array where Element == Speaker { 21 | var joinedNames: String { 22 | ListFormatter.localizedString(byJoining: self.map { $0.name }) 23 | } 24 | 25 | var joinedOrganisations: String { 26 | let organisations = Set(self.map { $0.organisation }) 27 | return ListFormatter.localizedString(byJoining: organisations.map { $0 }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SwiftLeeds/Data/Model/Sponsor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sponsor.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Muralidharan Kathiresan on 26/06/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Sponsors: Decodable { 11 | let data: [Sponsor] 12 | } 13 | 14 | struct Sponsor: Decodable, Hashable, Identifiable { 15 | let id: String 16 | let name: String 17 | let subtitle: String 18 | let image: String 19 | let sponsorLevel: SponsorLevel 20 | let url: String 21 | let jobs: [Job] 22 | 23 | static let sample = Sponsor(id: "id", name: "SwiftLeeds", subtitle: "Best Conference", image: "https://swiftleeds-speakers.s3.eu-west-2.amazonaws.com/961E45E2-8667-42F6-895E-4CE5E8B954E2-skybrand.png", sponsorLevel: .platinum, url: "", jobs: [.sample]) 24 | } 25 | 26 | enum SponsorLevel: String, Decodable { 27 | case silver 28 | case platinum 29 | case gold 30 | } 31 | 32 | struct Job: Decodable, Hashable, Identifiable { 33 | let id: UUID 34 | let title: String 35 | let details: String 36 | let location: String 37 | let url: String 38 | 39 | static let sample = Job(id: UUID(), title: "Senior iOS Engineer", details: "Bringing all your Swift skills to the fore", location: "Leeds", url: "https://www.swiftleeds.co.uk") 40 | } 41 | -------------------------------------------------------------------------------- /SwiftLeeds/Extension/Calendar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Calendar.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 04/08/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Calendar { 11 | func numberOfDays(to date: Date) -> Int { 12 | let fromDate = startOfDay(for: Date.now) 13 | let toDate = startOfDay(for: date) 14 | let numberOfDays = dateComponents([.day], from: fromDate, to: toDate) 15 | 16 | return numberOfDays.day ?? 0 17 | } 18 | 19 | static var atConferenceVenue: Calendar { 20 | var calendar = Calendar.current 21 | calendar.timeZone = .init(abbreviation: "BST")! 22 | return calendar 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SwiftLeeds/Extension/Color.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 14/11/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Color { 11 | static let accent = Color("AccentColor") 12 | 13 | static let tabBarBackground = Color("TabBarBackground") 14 | static let background = Color("Background") 15 | static let listBackground = Color("ListBackground") 16 | 17 | static let cellBackground = Color("CellBackground") 18 | static let cellBorder = Color("CellBorder") 19 | static let cellForeground = Color("CellForeground") 20 | 21 | static let buyTicketGradientStart = Color("BuyTicketGradientStart") 22 | static let buyTicketGradientEnd = Color("BuyTicketGradientEnd") 23 | 24 | static let weatherGradientStart = Color("WeatherGradientStart") 25 | static let weatherGradientEnd = Color("WeatherGradientEnd") 26 | } 27 | -------------------------------------------------------------------------------- /SwiftLeeds/Extension/Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 21/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | var withoutTimeAtConferenceVenue: Date { 12 | guard let date = Calendar.atConferenceVenue.date(from: Calendar.atConferenceVenue.dateComponents([.year, .month, .day], from: self)) else { 13 | fatalError("Failed to strip time from Date") 14 | } 15 | 16 | return date 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SwiftLeeds/Extension/LinearGradient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinearGradient.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 05/07/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension LinearGradient { 12 | static let weather = LinearGradient(colors: [ 13 | .weatherGradientStart, .weatherGradientEnd 14 | ], startPoint: .bottomLeading, endPoint: .topLeading) 15 | 16 | static let announcement = LinearGradient(colors: [ 17 | .buyTicketGradientStart, .buyTicketGradientEnd 18 | ], startPoint: .bottomLeading, endPoint: .topLeading) 19 | } 20 | -------------------------------------------------------------------------------- /SwiftLeeds/Extension/String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 06/08/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | var noEmojis: String { 12 | return self.unicodeScalars 13 | .filter { $0.properties.isEmojiPresentation == false } 14 | .reduce("") { $0 + String($1) } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SwiftLeeds/Extension/View+MeasureSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+MeasureSize.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 17/07/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct SizePreferenceKey: PreferenceKey { 12 | static var defaultValue: CGSize = .zero 13 | 14 | static func reduce(value: inout CGSize, nextValue: () -> CGSize) { 15 | if nextValue() == .zero { return } 16 | value = nextValue() 17 | } 18 | } 19 | 20 | struct SizeMeasuringModifier: ViewModifier { 21 | func body(content: Content) -> some View { 22 | content.background(GeometryReader { geometry in 23 | Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size) 24 | }) 25 | } 26 | } 27 | 28 | extension View { 29 | func measureSize(perform action: @escaping (CGSize) -> Void) -> some View { 30 | self.modifier(SizeMeasuringModifier()) 31 | .onPreferenceChange(SizePreferenceKey.self, perform: action) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SwiftLeeds/Network/Endpoints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoints.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 11/08/2022. 6 | // 7 | 8 | import Foundation 9 | import NetworkKit 10 | 11 | /// All endpoints should stay in this file to avoid creating lots of little files 12 | 13 | // MARK: - Schedule Endpoint 14 | struct ScheduleEndpoint: Endpoint { 15 | typealias DataType = Schedule 16 | let path: String = "schedule" 17 | var eventID: String? 18 | 19 | var queryParameters: [URLQueryItem] { 20 | if let eventID { 21 | return [.init(name: "event", value: eventID)] 22 | } else { 23 | return [] 24 | } 25 | } 26 | } 27 | 28 | // MARK: - Local Endpoint 29 | struct LocalEndpoint: Endpoint { 30 | typealias DataType = Local 31 | let path: String = "local" 32 | } 33 | 34 | // MARK: - Sponsors Endpoint 35 | struct SponsorsEndpoint: Endpoint { 36 | typealias DataType = Sponsors 37 | let path: String = "sponsors" 38 | } 39 | -------------------------------------------------------------------------------- /SwiftLeeds/Network/HttpMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HttpMethod.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 24/09/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum HttpMethod: Equatable { 11 | case get([URLQueryItem]) 12 | case post(Data?) 13 | case head 14 | 15 | var name: String { 16 | switch self { 17 | case .get: return "GET" 18 | case .post: return "POST" 19 | case .head: return "HEAD" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SwiftLeeds/Network/Request.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 24/09/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Request { 11 | let scheme: String 12 | let host: String 13 | let path: String? 14 | let method: HttpMethod 15 | public var headers: [String: String] = [:] 16 | let eTagKey: String? 17 | let url: URL 18 | 19 | public init(scheme: String = "https", host: String, path: String, method: HttpMethod = .get([]), headers: [String: String] = [:], eTagKey: String? = nil) { 20 | self.scheme = scheme 21 | self.host = host 22 | self.path = path 23 | self.method = method 24 | self.headers = headers 25 | self.eTagKey = eTagKey 26 | 27 | var components = URLComponents() 28 | components.scheme = scheme 29 | components.host = host 30 | components.path = path 31 | 32 | guard let url = components.url else { preconditionFailure("Couldn't create a url from components") } 33 | self.url = url 34 | } 35 | 36 | var urlRequest: URLRequest { 37 | var request = URLRequest(url: url) 38 | 39 | switch method { 40 | case let .get(queryItems): 41 | var components = URLComponents(url: url, resolvingAgainstBaseURL: false) 42 | components?.queryItems = queryItems 43 | 44 | guard let url = components?.url else { preconditionFailure("Couldn't create a url from components...") } 45 | 46 | request = URLRequest(url: url) 47 | case .post(let data): 48 | request.httpBody = data 49 | case .head: 50 | break 51 | } 52 | 53 | request.httpMethod = method.name 54 | request.allHTTPHeaderFields = headers 55 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 56 | request.setValue("application/json", forHTTPHeaderField: "Accept") 57 | request.setValue("*", forHTTPHeaderField: "Accept-Encoding") 58 | 59 | if let eTagKey, let eTagValue = UserDefaults.standard.value(forKey: eTagKey) as? String { 60 | request.setValue(eTagValue, forHTTPHeaderField: "If-None-Match") 61 | } 62 | 63 | return request 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /SwiftLeeds/Network/Requests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Requests.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 24/09/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Requests { 11 | private static let host = "swiftleeds.co.uk" 12 | private static let apiPath = "/api/v1" 13 | 14 | static let schedule = Request(host: host, path: "\(apiPath)/schedule", eTagKey: "etag-schedule") 15 | static let local = Request(host: host, path: "\(apiPath)/local", eTagKey: "etag-local") 16 | static let sponsors = Request(host: host, path: "\(apiPath)/sponsors", eTagKey: "etag-sponsors") 17 | 18 | static func schedule(for eventID: UUID) -> Request { 19 | Request(host: host, path: "\(apiPath)/schedule", method: .get([.init(name: "event", value: eventID.uuidString)]), eTagKey: "etag-schedule-\(eventID.uuidString)") 20 | } 21 | 22 | static var defaultDateDecodingStratergy: JSONDecoder.DateDecodingStrategy = { 23 | let dateFormatter = DateFormatter() 24 | dateFormatter.dateFormat = "dd-MM-yyyy" 25 | return .formatted(dateFormatter) 26 | }() 27 | } 28 | -------------------------------------------------------------------------------- /SwiftLeeds/Network/URLSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSession.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 24/09/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension URLSession { 11 | static var awaitConnectivity: URLSession = { 12 | let configuration = URLSessionConfiguration.default 13 | configuration.waitsForConnectivity = true 14 | configuration.timeoutIntervalForRequest = 30 15 | configuration.timeoutIntervalForResource = 30 16 | configuration.urlCache = nil 17 | configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData 18 | return URLSession(configuration: configuration) 19 | }() 20 | 21 | func cached(_ request: Request, using decoder: JSONDecoder = .init(), 22 | dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, 23 | fileManager: FileManager = .default, filename: String? = nil) async throws -> Response { 24 | let filename = filename ?? request.url.lastPathComponent 25 | let path = fileManager.temporaryDirectory.appendingPathComponent(filename) 26 | 27 | guard let data = fileManager.contents(atPath: path.path.appending(".json")) else { throw NetworkError.cacheNotFound } 28 | 29 | let decoded = Task.detached(priority: .userInitiated) { 30 | try Task.checkCancellation() 31 | decoder.dateDecodingStrategy = dateDecodingStrategy 32 | return try decoder.decode(Response.self, from: data) 33 | } 34 | 35 | return try await decoded.value 36 | } 37 | 38 | func decode(_ request: Request, using decoder: JSONDecoder = .init(), 39 | dateDecodingStrategy: JSONDecoder.DateDecodingStrategy?, 40 | fileManager: FileManager = .default, filename: String? = nil) async throws -> Response { 41 | let filename = filename ?? request.url.lastPathComponent 42 | let path = fileManager.temporaryDirectory.appendingPathComponent("\(filename).json") 43 | 44 | let decoded = Task.detached(priority: .userInitiated) { 45 | do { 46 | let (data, response) = try await self.data(for: request.urlRequest) 47 | 48 | try Task.checkCancellation() 49 | 50 | guard let response = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } 51 | 52 | switch response.statusCode { 53 | case 200...299: break 54 | case 304: throw NetworkError.notModified 55 | default: throw NetworkError.unexpectedStatusCode(response.statusCode) 56 | } 57 | 58 | try data.write(to: path, options: .atomicWrite) 59 | 60 | if let eTagKey = request.eTagKey, let eTagValue = response.value(forHTTPHeaderField: "Etag") { 61 | UserDefaults.standard.set(eTagValue, forKey: eTagKey) 62 | } 63 | 64 | if let dateDecodingStrategy { 65 | decoder.dateDecodingStrategy = dateDecodingStrategy 66 | } 67 | 68 | return try decoder.decode(Response.self, from: data) 69 | } catch { 70 | let nsError = error as NSError 71 | 72 | if let networkIssue = NetworkError.NetworkIssue(rawValue: nsError.code) { 73 | throw NetworkError.networkIssue(networkIssue) 74 | } 75 | 76 | throw NetworkError.unexpectedError(error) 77 | } 78 | } 79 | 80 | return try await decoded.value 81 | } 82 | } 83 | 84 | // MARK: - NetworkError 85 | public enum NetworkError: Error { 86 | case cacheNotFound 87 | case notModified 88 | case unexpectedStatusCode(Int) 89 | case unexpectedError(Error) 90 | case networkIssue(NetworkIssue) 91 | 92 | public enum NetworkIssue: Int, CaseIterable { 93 | case backgroundSessionInUseByAnotherProcess = -996 94 | case timedOut = -1001 95 | case cannotFindHost = -1003 96 | case cannotConnectToHost = -1004 97 | case networkConnectionLost = -1005 98 | case notConnectedToInternet = -1009 99 | case secureConnectionFailed = -1200 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /SwiftLeeds/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftLeeds/Push/AppDelegate+Push.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+Push.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 04/09/2022. 6 | // 7 | 8 | import UIKit 9 | import UserNotifications 10 | 11 | extension AppDelegate { 12 | func requestPushAuthorization(application: UIApplication) { 13 | let notificatioNCenter = UNUserNotificationCenter.current() 14 | notificatioNCenter.requestAuthorization(options: [.badge, .sound, .alert]) { [weak self] isGranted, error in 15 | guard isGranted else { print("⛔️ not granted"); return } 16 | if let error = error { print("⛔️", error); return } 17 | 18 | notificatioNCenter.delegate = self 19 | 20 | DispatchQueue.main.async { 21 | application.registerForRemoteNotifications() 22 | } 23 | } 24 | } 25 | 26 | func sendPushRegistrationDatails(to url: URL, deviceToken: Data) { 27 | var details = TokenDetails(token: deviceToken) 28 | 29 | #if DEBUG 30 | details.debug = true 31 | print("🚀", details) 32 | #endif 33 | 34 | var request = URLRequest(url: url) 35 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 36 | request.httpMethod = "POST" 37 | request.httpBody = try? TokenDetails.encoder.encode(details) 38 | 39 | Task { 40 | do { 41 | let (_, response) = try await URLSession.shared.data(for: request) 42 | 43 | guard let statusCode = (response as? HTTPURLResponse)?.statusCode, 200..<399 ~= statusCode else { 44 | print("⛔️ Push registration failed, invalid response") 45 | return 46 | } 47 | } catch { 48 | print("⛔️ Push registration failed due to unexpected network issue:", error) 49 | } 50 | } 51 | } 52 | 53 | func handleFailedRegistration(application: UIApplication, error: Error) { 54 | print("⛔️ Push registration failed:", error) 55 | } 56 | } 57 | 58 | // MARK: - UNUserNotificationCenterDelegate 59 | extension AppDelegate: UNUserNotificationCenterDelegate { 60 | func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { 61 | [.banner, .sound, .badge] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SwiftLeeds/Push/TokenDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenDetails.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 04/09/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TokenDetails: Encodable { 11 | let token: String 12 | var debug: Bool = false 13 | 14 | static var encoder: JSONEncoder { 15 | let encoder = JSONEncoder() 16 | encoder.outputFormatting = .prettyPrinted 17 | return encoder 18 | } 19 | } 20 | 21 | extension TokenDetails { 22 | init(token: Data) { 23 | self.token = token.reduce("") { $0 + String(format: "%02x", $1) } 24 | } 25 | } 26 | 27 | extension TokenDetails: CustomStringConvertible { 28 | var description: String { 29 | do { 30 | let data = try Self.encoder.encode(self) 31 | return String(data: data, encoding: .utf8) ?? "Invalid token" 32 | } catch { 33 | return "Invalid token" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/CarriageworksTheatre.imageset/CarriageworksTheatre.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/CarriageworksTheatre.imageset/CarriageworksTheatre.jpg -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/CarriageworksTheatre.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "CarriageworksTheatre.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/Clock.imageset/Clock.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << >> 5 | endobj 6 | 7 | 2 0 obj 8 | << /Length 3 0 R >> 9 | stream 10 | /DeviceRGB CS 11 | /DeviceRGB cs 12 | q 13 | 1.000000 0.000000 -0.000000 1.000000 0.750000 -0.750000 cm 14 | 0.583333 0.583333 0.583333 scn 15 | 10.250000 7.000000 m 16 | 10.250000 4.376647 8.123353 2.250000 5.500000 2.250000 c 17 | 5.500000 0.750000 l 18 | 8.951780 0.750000 11.750000 3.548220 11.750000 7.000000 c 19 | 10.250000 7.000000 l 20 | h 21 | 5.500000 2.250000 m 22 | 2.876647 2.250000 0.750000 4.376647 0.750000 7.000000 c 23 | -0.750000 7.000000 l 24 | -0.750000 3.548220 2.048220 0.750000 5.500000 0.750000 c 25 | 5.500000 2.250000 l 26 | h 27 | 0.750000 7.000000 m 28 | 0.750000 9.623352 2.876647 11.750000 5.500000 11.750000 c 29 | 5.500000 13.250000 l 30 | 2.048220 13.250000 -0.750000 10.451779 -0.750000 7.000000 c 31 | 0.750000 7.000000 l 32 | h 33 | 5.500000 11.750000 m 34 | 8.123353 11.750000 10.250000 9.623352 10.250000 7.000000 c 35 | 11.750000 7.000000 l 36 | 11.750000 10.451779 8.951780 13.250000 5.500000 13.250000 c 37 | 5.500000 11.750000 l 38 | h 39 | f 40 | n 41 | Q 42 | q 43 | 1.000000 0.000000 -0.000000 1.000000 6.250000 3.543915 cm 44 | 0.583333 0.583333 0.583333 scn 45 | 0.750000 6.006073 m 46 | 0.750000 6.420287 0.414214 6.756073 0.000000 6.756073 c 47 | -0.414214 6.756073 -0.750000 6.420287 -0.750000 6.006073 c 48 | 0.750000 6.006073 l 49 | h 50 | 0.000000 2.706073 m 51 | -0.750000 2.706073 l 52 | -0.750000 2.421994 -0.589498 2.162297 -0.335410 2.035253 c 53 | 0.000000 2.706073 l 54 | h 55 | 1.864590 0.935253 m 56 | 2.235074 0.750011 2.685578 0.900179 2.870820 1.270663 c 57 | 3.056062 1.641147 2.905894 2.091652 2.535410 2.276894 c 58 | 1.864590 0.935253 l 59 | h 60 | -0.750000 6.006073 m 61 | -0.750000 2.706073 l 62 | 0.750000 2.706073 l 63 | 0.750000 6.006073 l 64 | -0.750000 6.006073 l 65 | h 66 | -0.335410 2.035253 m 67 | 1.864590 0.935253 l 68 | 2.535410 2.276894 l 69 | 0.335410 3.376894 l 70 | -0.335410 2.035253 l 71 | h 72 | f 73 | n 74 | Q 75 | 76 | endstream 77 | endobj 78 | 79 | 3 0 obj 80 | 1585 81 | endobj 82 | 83 | 4 0 obj 84 | << /Annots [] 85 | /Type /Page 86 | /MediaBox [ 0.000000 0.000000 12.500000 12.500000 ] 87 | /Resources 1 0 R 88 | /Contents 2 0 R 89 | /Parent 5 0 R 90 | >> 91 | endobj 92 | 93 | 5 0 obj 94 | << /Kids [ 4 0 R ] 95 | /Count 1 96 | /Type /Pages 97 | >> 98 | endobj 99 | 100 | 6 0 obj 101 | << /Pages 5 0 R 102 | /Type /Catalog 103 | >> 104 | endobj 105 | 106 | xref 107 | 0 7 108 | 0000000000 65535 f 109 | 0000000010 00000 n 110 | 0000000034 00000 n 111 | 0000001675 00000 n 112 | 0000001698 00000 n 113 | 0000001871 00000 n 114 | 0000001945 00000 n 115 | trailer 116 | << /ID [ (some) (id) ] 117 | /Root 6 0 R 118 | /Size 7 119 | >> 120 | startxref 121 | 2004 122 | %%EOF -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/Clock.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Clock.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/Icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon.pdf", 5 | "idiom" : "universal" 6 | }, 7 | { 8 | "appearances" : [ 9 | { 10 | "appearance" : "luminosity", 11 | "value" : "dark" 12 | } 13 | ], 14 | "filename" : "Icon-Dark.pdf", 15 | "idiom" : "universal" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "preserves-vector-representation" : true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/Icon.imageset/Icon-Dark.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/Icon.imageset/Icon-Dark.pdf -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/Icon.imageset/Icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/Icon.imageset/Icon.pdf -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchScreen.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "LaunchScreen-Dark.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "LaunchScreen@2x.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "LaunchScreen-Dark@2x.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "filename" : "LaunchScreen@3x.png", 37 | "idiom" : "universal", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "appearances" : [ 42 | { 43 | "appearance" : "luminosity", 44 | "value" : "dark" 45 | } 46 | ], 47 | "filename" : "LaunchScreen-Dark@3x.png", 48 | "idiom" : "universal", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen-Dark.png -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen-Dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen-Dark@2x.png -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen-Dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen-Dark@3x.png -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen.png -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen@2x.png -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/LaunchImage.imageset/LaunchScreen@3x.png -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/LeedsPlayhouse.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LeedsPlayhouse.jpeg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/LeedsPlayhouse.imageset/LeedsPlayhouse.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/LeedsPlayhouse.imageset/LeedsPlayhouse.jpeg -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/SwiftLeedsIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "SwiftLeedsIcon.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "original" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/SwiftLeedsIcon.imageset/SwiftLeedsIcon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeeds/Resources/Assets.xcassets/SwiftLeedsIcon.imageset/SwiftLeedsIcon.pdf -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/wineglass.fill.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "wineglass.fill.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Assets.xcassets/wineglass.fill.symbolset/wineglass.fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | Weight/Scale Variations 12 | Ultralight 13 | Thin 14 | Light 15 | Regular 16 | Medium 17 | Semibold 18 | Bold 19 | Heavy 20 | Black 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Design Variations 32 | Symbols are supported in up to nine weights and three scales. 33 | For optimal layout with text and other symbols, vertically align 34 | symbols with the adjacent text. 35 | 36 | 37 | 38 | 39 | 40 | Margins 41 | Leading and trailing margins on the left and right side of each symbol 42 | can be adjusted by modifying the x-location of the margin guidelines. 43 | Modifications are automatically applied proportionally to all 44 | scales and weights. 45 | 46 | 47 | 48 | Exporting 49 | Symbols should be outlined when exporting to ensure the 50 | design is preserved when submitting to Xcode. 51 | Template v.3.0 52 | Requires Xcode 13 or greater 53 | Generated from wineglass.fill 54 | Typeset at 100 points 55 | Small 56 | Medium 57 | Large 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x30", 9 | "green" : "0x3B", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "platform" : "universal", 24 | "reference" : "systemRedColor" 25 | }, 26 | "idiom" : "universal" 27 | } 28 | ], 29 | "info" : { 30 | "author" : "xcode", 31 | "version" : 1 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/Background.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xED", 9 | "green" : "0xED", 10 | "red" : "0xED" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x1E", 27 | "green" : "0x1D", 28 | "red" : "0x1C" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/CellBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFF", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x3A", 27 | "green" : "0x3A", 28 | "red" : "0x3A" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/CellBorder.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xD4", 9 | "green" : "0xD4", 10 | "red" : "0xD4" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xFF", 27 | "green" : "0xFF", 28 | "red" : "0xFF" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/CellForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x4A", 9 | "green" : "0x4A", 10 | "red" : "0x4A" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xFF", 27 | "green" : "0xFF", 28 | "red" : "0xFF" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/Gradients/BuyTicketGradientEnd.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x9A", 9 | "green" : "0x9A", 10 | "red" : "0x9A" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x7F", 27 | "green" : "0x7F", 28 | "red" : "0x7F" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/Gradients/BuyTicketGradientStart.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x5E", 9 | "green" : "0x5E", 10 | "red" : "0x5E" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x5E", 27 | "green" : "0x5E", 28 | "red" : "0x5E" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/Gradients/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/LaunchScreenBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x3A", 27 | "green" : "0x3A", 28 | "red" : "0x3A" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/ListBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFF", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x1E", 27 | "green" : "0x1D", 28 | "red" : "0x1C" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftLeeds/Resources/Colors.xcassets/TabBarBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF9", 9 | "green" : "0xF9", 10 | "red" : "0xF9" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x39", 27 | "green" : "0x39", 28 | "red" : "0x39" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftLeeds/Style/SquishyButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SquishyButtonStyle.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 01/07/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct SquishyButtonStyle: ButtonStyle { 12 | func makeBody(configuration: Self.Configuration) -> some View { 13 | configuration.label 14 | .scaleEffect(configuration.isPressed ? 0.98 : 1.0) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SwiftLeeds/SwiftLeeds.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.associated-domains 8 | 9 | appclips:swiftleeds.co.uk 10 | applinks:swiftleeds.co.uk 11 | 12 | com.apple.security.application-groups 13 | 14 | group.uk.co.swiftleeds 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/About/AboutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 25/06/2022. 6 | // 7 | 8 | import SwiftUI 9 | import ReadabilityModifier 10 | 11 | struct AboutView: View { 12 | @State private var isReportAProblemShown = false 13 | 14 | private let venueURL = URL(string: "https://swiftleeds.co.uk/#venue") 15 | private let codeOfConductURL = URL(string: "https://swiftleeds.co.uk/conduct") 16 | private let reportAProblemLink = "https://forms.gle/PJie9aRNAtzQUdUu9" 17 | 18 | var body: some View { 19 | SwiftLeedsContainer { 20 | ScrollView { 21 | content 22 | } 23 | } 24 | .edgesIgnoringSafeArea(.top) 25 | } 26 | 27 | private var content: some View { 28 | VStack(spacing: Padding.cellGap) { 29 | FancyHeaderView( 30 | title: "About", 31 | foregroundImageName: Assets.Image.swiftLeedsIcon 32 | ) 33 | 34 | VStack(spacing: Padding.cellGap) { 35 | StackedTileView(primaryText: "About", secondaryText: Strings.aboutSwiftLeeds) 36 | 37 | CommonTileButton(primaryText: "Report a problem", accessibilityHint: "Opens a web view to allow a problem to be reported", backgroundStyle: Color.cellBackground) { 38 | isReportAProblemShown = true 39 | } 40 | 41 | CommonTileButton(primaryText: "Code of conduct", accessibilityHint: "Opens a web page showing our code of conduct", backgroundStyle: Color.cellBackground) { 42 | openURL(url: codeOfConductURL) 43 | } 44 | 45 | CommonTileButton(primaryText: "Venue", accessibilityHint: "Opens a web page showing our venue information", backgroundStyle: Color.cellBackground) { 46 | openURL(url: venueURL) 47 | } 48 | } 49 | .fitToReadableContentGuide(type: .width) 50 | } 51 | .padding(.bottom, Padding.cellGap) 52 | .sheet(isPresented: $isReportAProblemShown) { 53 | WebView(url: reportAProblemLink) 54 | .ignoresSafeArea(edges: .bottom) 55 | } 56 | .navigationBarHidden(true) 57 | } 58 | 59 | private func openURL(url: URL?) { 60 | guard let url = url else { return } 61 | UIApplication.shared.open(url) 62 | } 63 | } 64 | 65 | struct AboutView_Previews: PreviewProvider { 66 | static var previews: some View { 67 | AboutView() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Common/Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helper.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 21/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Helper { 11 | static var shortDateFormatter: DateFormatter = { 12 | let dateformatter = DateFormatter() 13 | dateformatter.dateFormat = "dd-MM-yyyy" 14 | return dateformatter 15 | }() 16 | } 17 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Common/SectionHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionHeader.swift 3 | // SwiftLeeds 4 | // 5 | // Created by LUCKY AGARWAL on 23/07/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SectionHeader: View { 11 | let title: String 12 | let fontStyle: Font 13 | let foregroundColor: Color 14 | let maxWidth: CGFloat 15 | let alignment: Alignment 16 | let accesibilityAddTraits: AccessibilityTraits 17 | 18 | internal init(title: String, 19 | fontStyle: Font = .callout.weight(.semibold), 20 | foregroundColor : Color = .secondary, 21 | maxWidth: CGFloat = .infinity, 22 | alignment: Alignment = .leading, 23 | accessbilityAddTraits: AccessibilityTraits = .isHeader 24 | ) { 25 | self.title = title 26 | self.fontStyle = fontStyle 27 | self.foregroundColor = foregroundColor 28 | self.maxWidth = maxWidth 29 | self.alignment = alignment 30 | self.accesibilityAddTraits = accessbilityAddTraits 31 | } 32 | 33 | var body: some View { 34 | Text(title) 35 | .font(fontStyle) 36 | .foregroundColor(foregroundColor) 37 | .frame(maxWidth: maxWidth, alignment: alignment) 38 | .accessibilityAddTraits(accesibilityAddTraits) 39 | } 40 | } 41 | 42 | struct SectionHeader_Previews: PreviewProvider { 43 | static var previews: some View { 44 | SectionHeader(title: "SwiftLeeds") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Common/SwiftLeedsContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLeedsContainer.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 01/07/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SwiftLeedsContainer: View { 11 | private var content: () -> (Content) 12 | 13 | init(@ViewBuilder content: @escaping () -> (Content)) { 14 | self.content = content 15 | } 16 | 17 | var body: some View { 18 | ZStack { 19 | Color.background.edgesIgnoringSafeArea(.all) 20 | content() 21 | } 22 | } 23 | } 24 | 25 | struct SwiftLeedsContainer_Previews: PreviewProvider { 26 | static var previews: some View { 27 | SwiftLeedsContainer { 28 | Text(verbatim: "SwiftLeeds 22") 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Components/CommonTileButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonTileButton.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 05/07/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CommonTileButton: View { 11 | let icon: String? 12 | let primaryText: String 13 | let secondaryText: String? 14 | let subtitleText: String? 15 | let accessibilityHint: String? 16 | let showChevron: Bool 17 | let primaryColor: Color 18 | let secondaryColor: Color 19 | var backgroundStyle: BackgroundType 20 | 21 | let onTap: () -> () 22 | 23 | init( 24 | icon: String? = nil, 25 | primaryText: String, 26 | secondaryText: String? = nil, 27 | subtitleText: String? = nil, 28 | accessibilityHint: String? = nil, 29 | showChevron: Bool = false, 30 | primaryColor: Color = Color.primary, 31 | secondaryColor: Color = Color.secondary, 32 | backgroundStyle: Color = Color.cellBackground, 33 | onTap: @escaping () -> () 34 | ) where BackgroundType == Color { 35 | self.icon = icon 36 | self.primaryText = primaryText 37 | self.secondaryText = secondaryText 38 | self.subtitleText = subtitleText 39 | self.accessibilityHint = accessibilityHint 40 | self.showChevron = showChevron 41 | self.primaryColor = primaryColor 42 | self.secondaryColor = secondaryColor 43 | self.backgroundStyle = backgroundStyle 44 | self.onTap = onTap 45 | } 46 | 47 | init( 48 | icon: String? = nil, 49 | primaryText: String, 50 | secondaryText: String? = nil, 51 | subtitleText: String? = nil, 52 | accessibilityHint: String? = nil, 53 | showChevron: Bool = false, 54 | primaryColor: Color = Color.primary, 55 | secondaryColor: Color = Color.secondary, 56 | backgroundStyle: BackgroundType, 57 | onTap: @escaping () -> () 58 | ) { 59 | self.icon = icon 60 | self.primaryText = primaryText 61 | self.secondaryText = secondaryText 62 | self.subtitleText = subtitleText 63 | self.accessibilityHint = accessibilityHint 64 | self.showChevron = showChevron 65 | self.primaryColor = primaryColor 66 | self.secondaryColor = secondaryColor 67 | self.backgroundStyle = backgroundStyle 68 | self.onTap = onTap 69 | } 70 | 71 | var body: some View { 72 | Button(action: onTap) { 73 | CommonTileView( 74 | icon: icon, 75 | primaryText: primaryText, 76 | secondaryText: secondaryText, 77 | subtitleText: subtitleText, 78 | showChevron: showChevron, 79 | primaryColor: primaryColor, 80 | secondaryColor: secondaryColor, 81 | backgroundStyle: backgroundStyle 82 | ) 83 | } 84 | .buttonStyle(SquishyButtonStyle()) 85 | .accessibilityHint(accessibilityHint ?? "") 86 | .accessibilityAddTraits(.isButton) 87 | } 88 | } 89 | 90 | struct CommonTileButtton_Previews: PreviewProvider { 91 | static var previews: some View { 92 | ZStack { 93 | Color(uiColor: .systemGroupedBackground).edgesIgnoringSafeArea(.all) 94 | VStack(spacing: Padding.cellGap) { 95 | CommonTileButton( 96 | primaryText: "Primary", 97 | secondaryText: "Secondary", 98 | onTap: {} 99 | ) 100 | CommonTileButton( 101 | primaryText: "Primary", 102 | showChevron: true, 103 | onTap: {} 104 | ) 105 | CommonTileButton( 106 | primaryText: "Primary", 107 | secondaryText: "Secondary", 108 | backgroundStyle: .red, 109 | onTap: {} 110 | ) 111 | CommonTileButton( 112 | primaryText: "Primary", 113 | secondaryText: "Secondary", 114 | primaryColor: .white, 115 | secondaryColor: .white.opacity(0.8), 116 | backgroundStyle: LinearGradient.weather, 117 | onTap: {} 118 | ) 119 | } 120 | .padding() 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Components/CommonTileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonTileView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 05/07/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Generic primary secondary view 11 | struct CommonTileView: View { 12 | @Environment(\.sizeCategory) var sizeCategory 13 | 14 | let icon: String? 15 | let primaryText: String 16 | let secondaryText: String? 17 | let subtitleText: String? 18 | let showChevron: Bool 19 | let primaryColor: Color 20 | let secondaryColor: Color 21 | var backgroundStyle: BackgroundType 22 | 23 | var accessibilityTextEnabled: Bool { 24 | sizeCategory >= .accessibilityMedium 25 | } 26 | 27 | init( 28 | icon: String? = nil, 29 | primaryText: String, 30 | secondaryText: String? = nil, 31 | subtitleText: String? = nil, 32 | showChevron: Bool = false, 33 | primaryColor: Color = Color.primary, 34 | secondaryColor: Color = Color.secondary, 35 | backgroundStyle: BackgroundType 36 | ) { 37 | self.icon = icon 38 | self.primaryText = primaryText 39 | self.secondaryText = secondaryText 40 | self.subtitleText = subtitleText 41 | self.showChevron = showChevron 42 | self.primaryColor = primaryColor 43 | self.secondaryColor = secondaryColor 44 | self.backgroundStyle = backgroundStyle 45 | } 46 | 47 | init( 48 | icon: String? = nil, 49 | primaryText: String, 50 | secondaryText: String? = nil, 51 | subtitleText: String? = nil, 52 | showChevron: Bool = false, 53 | primaryColor: Color = Color.primary, 54 | secondaryColor: Color = Color.secondary, 55 | backgroundStyle: Color = Color.cellBackground 56 | ) where BackgroundType == Color { 57 | self.icon = icon 58 | self.primaryText = primaryText 59 | self.secondaryText = secondaryText 60 | self.subtitleText = subtitleText 61 | self.showChevron = showChevron 62 | self.primaryColor = primaryColor 63 | self.secondaryColor = secondaryColor 64 | self.backgroundStyle = backgroundStyle 65 | } 66 | 67 | var body: some View { 68 | sizeAwareStack(content: { 69 | VStack(alignment: .leading, spacing: 2) { 70 | if let icon { 71 | Text("\(Image(systemName: icon)) \(primaryText)") 72 | .font(.subheadline.weight(.semibold)) 73 | } else { 74 | Text(primaryText) 75 | .font(.subheadline.weight(.semibold)) 76 | } 77 | 78 | 79 | if let subtitleText { 80 | Text(subtitleText) 81 | .font(.subheadline.weight(.light)) 82 | 83 | } 84 | } 85 | .foregroundColor(primaryColor) 86 | 87 | if !accessibilityTextEnabled { 88 | Spacer() 89 | } 90 | 91 | if let secondaryText = secondaryText { 92 | Text(secondaryText) 93 | .font(.subheadline.weight(.medium)) 94 | .foregroundColor(secondaryColor) 95 | } else if showChevron { 96 | Image(systemName: "chevron.right") 97 | } 98 | }) 99 | .padding(Padding.cell) 100 | .frame(minHeight: Constants.compactCellMinimumHeight) 101 | .background( 102 | backgroundStyle, 103 | in: RoundedRectangle(cornerRadius: Constants.cellRadius) 104 | ) 105 | .accessibilityElement(children: .ignore) 106 | .accessibilityLabel( 107 | "\(primaryText), \(secondaryText ?? "")" 108 | ) 109 | } 110 | 111 | // When the text is huge, stack vertically instead to avoid compressing the leading text 112 | @ViewBuilder 113 | func sizeAwareStack(@ViewBuilder content: () -> (Content)) -> some View { 114 | if accessibilityTextEnabled { 115 | VStack { 116 | content() 117 | .frame(maxWidth: .infinity, alignment: .leading) 118 | } 119 | } else { 120 | HStack { 121 | content() 122 | } 123 | } 124 | } 125 | } 126 | 127 | struct CommonTileView_Previews: PreviewProvider { 128 | static var previews: some View { 129 | ZStack { 130 | Color(uiColor: .systemGroupedBackground).edgesIgnoringSafeArea(.all) 131 | VStack(spacing: Padding.cellGap) { 132 | CommonTileView( 133 | primaryText: "Primary", secondaryText: "Secondary", subtitleText: "More details", showChevron: true 134 | ) 135 | CommonTileView( 136 | primaryText: "Primary" 137 | ) 138 | CommonTileView( 139 | primaryText: "Primary", 140 | secondaryText: "Secondary", 141 | backgroundStyle: .red 142 | ) 143 | CommonTileView( 144 | primaryText: "Primary", 145 | secondaryText: "Secondary", 146 | primaryColor: .white, 147 | secondaryColor: .white.opacity(0.8), 148 | backgroundStyle: LinearGradient.weather 149 | ) 150 | } 151 | .padding() 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Components/FancyHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Kannan Prasad on 05/07/2022. 6 | // 7 | 8 | import SwiftUI 9 | import CachedAsyncImage 10 | 11 | struct FancyHeaderView: View { 12 | private let title: String 13 | private let foregroundImageURLs: [URL] 14 | private let foregroundImageName: String? 15 | 16 | private let foregroundImageWidth: Double = 160 17 | private let aspectRatio = 1.66 18 | 19 | @State private var foregroundGroupViewHeight: CGFloat = .zero 20 | 21 | // MARK: - Initialisers 22 | init(title: String, foregroundImageURLs: [URL] = [], foregroundImageName: String? = nil) { 23 | self.title = title 24 | self.foregroundImageURLs = foregroundImageURLs 25 | self.foregroundImageName = foregroundImageName 26 | } 27 | 28 | var body: some View { 29 | Rectangle() 30 | .foregroundColor(.clear) 31 | .edgesIgnoringSafeArea(.top) 32 | .aspectRatio(aspectRatio, contentMode: .fill) 33 | .background( 34 | createRectangleImage(for: Image(Assets.Image.leedsPlayhouse), aspectRatio: aspectRatio) 35 | .aspectRatio(contentMode: .fill) 36 | .accessibilityHidden(true) 37 | ) 38 | .overlay(foregroundGroup,alignment: .center) 39 | .padding(.bottom,foregroundGroupViewHeight/2) 40 | } 41 | 42 | private var foregroundGroup: some View { 43 | GeometryReader { geometry in 44 | VStack(spacing: Padding.cellGap) { 45 | foregroundImages 46 | .frame(width: foregroundImageWidth * Double(foregroundImageCount)) 47 | .cornerRadius(Constants.cellRadius) 48 | .shadow(color: shadowColor, radius: 8, x: 0, y: 0) 49 | Text(title) 50 | .foregroundColor(.primary) 51 | .font(.title3.weight(.bold)) 52 | .accessibilityAddTraits(.isHeader) 53 | } 54 | .frame(width: geometry.frame(in: .global).width, 55 | height: geometry.frame(in: .global).height) 56 | .offset(y: geometry.size.height/2) 57 | .onAppear { 58 | foregroundGroupViewHeight = geometry.size.height 59 | } 60 | .onChange(of: geometry.size) { newValue in 61 | foregroundGroupViewHeight = newValue.height 62 | } 63 | } 64 | } 65 | 66 | @ViewBuilder 67 | private var foregroundImages: some View { 68 | if foregroundImageURLs.isEmpty == false { 69 | HStack { 70 | ForEach(foregroundImageURLs, id: \.self) { foregroundImageURL in 71 | AsyncImage(url: foregroundImageURL) { phase in 72 | switch phase { 73 | case .empty: 74 | loadingView() 75 | case .success(let image): 76 | createRectangleImage(for: image) 77 | .accessibilityHidden(true) 78 | case .failure(_): 79 | createRectangleImage(for: Image(Assets.Image.swiftLeedsIcon)) 80 | @unknown default: 81 | loadingView() 82 | } 83 | } 84 | } 85 | } 86 | } else if let foregroundImageName = foregroundImageName { 87 | createRectangleImage(for: Image(foregroundImageName)) 88 | } else { 89 | createRectangleImage(for: Image(Assets.Image.swiftLeedsIcon)) 90 | } 91 | } 92 | 93 | private func createRectangleImage(for image: Image, aspectRatio: Double = 1.0) -> some View { 94 | return Rectangle() 95 | .foregroundColor(.clear) 96 | .aspectRatio(aspectRatio, contentMode: .fit) 97 | .background( 98 | image 99 | .resizable() 100 | .aspectRatio(contentMode: .fill) 101 | .transition(.opacity) 102 | ) 103 | } 104 | 105 | private func loadingView(aspectRatio: Double = 1.0) -> some View { 106 | return Rectangle() 107 | .foregroundColor(.secondary) 108 | .aspectRatio(aspectRatio, contentMode: .fit) 109 | .overlay( 110 | ProgressView() 111 | ) 112 | .progressViewStyle(CircularProgressViewStyle()) 113 | } 114 | 115 | private var foregroundImageCount: Int { 116 | foregroundImageURLs.count + (foregroundImageName == nil ? 0 : 1) 117 | } 118 | 119 | private var shadowColor: Color { 120 | Color.black.opacity(1/3) 121 | } 122 | } 123 | 124 | struct FancyHeaderView_Previews: PreviewProvider { 125 | static var previews: some View { 126 | Group { 127 | VStack { 128 | Text(verbatim: "Local Asset") 129 | FancyHeaderView(title: "Some Long Text here", 130 | foregroundImageName: Assets.Image.swiftLeedsIcon) 131 | Text(verbatim: "Remote Data") 132 | FancyHeaderView(title: "Swift Taylor", 133 | foregroundImageURLs: [URL(string: "https://cdn-az.allevents.in/events5/banners/458482c4fc7489448aa3d77f6e2cd5d0553fa5edd7178dbf18cf986d2172eaf2-rimg-w1200-h675-gmir.jpg?v=1655230338")!]) 134 | 135 | } 136 | ScrollView { 137 | Text(verbatim: "Local Asset") 138 | VStack { 139 | FancyHeaderView(title: "Kannan Prasad", 140 | foregroundImageName: Assets.Image.swiftLeedsIcon) 141 | } 142 | Text(verbatim: "Remote Data") 143 | FancyHeaderView(title: "Swift Taylor", 144 | foregroundImageURLs: [URL(string: "https://cdn-az.allevents.in/events5/banners/458482c4fc7489448aa3d77f6e2cd5d0553fa5edd7178dbf18cf986d2172eaf2-rimg-w1200-h675-gmir.jpg?v=1655230338")!]) 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Components/HeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 11/07/2022. 6 | // 7 | 8 | import SwiftUI 9 | import CachedAsyncImage 10 | 11 | struct HeaderView: View { 12 | private let title: String 13 | private let imageURL: URL? 14 | private let backgroundURL: URL? 15 | private let imageAssetName: String? 16 | private let backgroundImageAssetName: String? 17 | private let placeholderColor: Color 18 | private let imageBackgroundColor: Color 19 | 20 | @State private var textImageStackSize = CGSize.zero 21 | 22 | private let backgroundImageWidthToHeightRatio: CGFloat = 1.66 23 | private let frontImageHeight: CGFloat = 160 24 | private var textOffset: CGFloat { textImageStackSize.height/2 } 25 | 26 | init( 27 | title: String, 28 | imageURL: URL? = nil, 29 | backgroundURL: URL? = nil, 30 | imageAssetName: String? = nil, 31 | backgroundImageAssetName: String? = nil, 32 | placeholderColor: Color = .white, 33 | imageBackgroundColor: Color = .white 34 | ) { 35 | self.title = title 36 | self.imageURL = imageURL 37 | self.backgroundURL = backgroundURL 38 | self.imageAssetName = imageAssetName 39 | self.backgroundImageAssetName = backgroundImageAssetName 40 | self.placeholderColor = placeholderColor 41 | self.imageBackgroundColor = imageBackgroundColor 42 | } 43 | 44 | var body: some View { 45 | VStack(alignment: .center) { 46 | backgroundImage 47 | .overlay( 48 | imageAndTextStack.measureSize { size in 49 | textImageStackSize = size 50 | }, 51 | alignment: .center 52 | ) 53 | } 54 | .padding(.bottom, textOffset) 55 | .allowsHitTesting(false) 56 | } 57 | 58 | var text: some View { 59 | VStack(spacing: Padding.cellGap) { 60 | Text(title) 61 | .font(.title3.weight(.semibold)) 62 | .foregroundColor(.primary) 63 | .multilineTextAlignment(.center) 64 | .accessibilityLabel(title.noEmojis) 65 | .accessibilityHeading(.h1) 66 | } 67 | } 68 | 69 | var imageAndTextStack: some View { 70 | VStack(spacing: Padding.cellGap) { 71 | frontImage 72 | text 73 | } 74 | .offset(x: 0, y: textOffset) 75 | } 76 | 77 | private var frontImage: some View { 78 | Group { 79 | if let imageAssetName = imageAssetName { 80 | Rectangle() 81 | .foregroundColor(.clear) 82 | .aspectRatio(1.0, contentMode: .fit) 83 | .background( 84 | Image(imageAssetName) 85 | .resizable() 86 | .aspectRatio(1.0, contentMode: .fill) 87 | ) 88 | } else { 89 | remoteFrontImage 90 | } 91 | } 92 | .frame(width: frontImageHeight, height: frontImageHeight, alignment: .center) 93 | .accessibilityHidden(true) 94 | .cornerRadius(Constants.cellRadius) 95 | .shadow(color: Color.black.opacity(1/3), radius: 8, x: 0, y: 0) 96 | } 97 | 98 | private var remoteFrontImage: some View { 99 | CachedAsyncImage( 100 | url: imageURL, 101 | content: { image in 102 | Rectangle() 103 | .aspectRatio(backgroundImageWidthToHeightRatio, contentMode: .fill) 104 | .foregroundColor(.clear) 105 | .background( 106 | image 107 | .resizable() 108 | .aspectRatio(contentMode: .fill) 109 | .transition(.opacity) 110 | ) 111 | .background(imageBackgroundColor) 112 | .clipped() 113 | .transition(contentTransition) 114 | }, 115 | placeholder: { 116 | Rectangle() 117 | .foregroundColor(placeholderColor) 118 | .transition(contentTransition) 119 | .overlay(content: { 120 | ProgressView() 121 | .tint(.white) 122 | .opacity(0.5) 123 | }) 124 | } 125 | ) 126 | } 127 | 128 | private var backgroundImage: some View { 129 | Group { 130 | if let backgroundImageAssetName = backgroundImageAssetName { 131 | Rectangle() 132 | .foregroundColor(.clear) 133 | .background( 134 | Image(backgroundImageAssetName) 135 | .resizable() 136 | .aspectRatio(contentMode: .fill) 137 | ) 138 | .clipped() 139 | } else { 140 | remoteBackgroundImage 141 | } 142 | } 143 | .aspectRatio(backgroundImageWidthToHeightRatio, contentMode: .fit) 144 | .edgesIgnoringSafeArea(.top) 145 | .accessibilityHidden(true) 146 | } 147 | 148 | private var remoteBackgroundImage: some View { 149 | CachedAsyncImage( 150 | url: backgroundURL, 151 | content: { image in 152 | Rectangle() 153 | .aspectRatio(backgroundImageWidthToHeightRatio, contentMode: .fit) 154 | .foregroundColor(.clear) 155 | .background( 156 | image 157 | .resizable() 158 | .aspectRatio(contentMode: .fill) 159 | .transition(.opacity) 160 | ) 161 | .background(imageBackgroundColor) 162 | .clipped() 163 | .transition(contentTransition) 164 | }, 165 | placeholder: { 166 | Rectangle() 167 | .foregroundColor(placeholderColor) 168 | .transition(contentTransition) 169 | .overlay(content: { 170 | ProgressView() 171 | .tint(.white) 172 | .opacity(0.5) 173 | }) 174 | } 175 | ) 176 | } 177 | 178 | private var contentTransition: AnyTransition { 179 | .opacity.animation(.spring()) 180 | } 181 | } 182 | 183 | struct HeaderView_Previews: PreviewProvider { 184 | static var previews: some View { 185 | VStack { 186 | HeaderView( 187 | title: "Taylor Swift", 188 | imageURL: URL(string: "https://cdn-az.allevents.in/events5/banners/458482c4fc7489448aa3d77f6e2cd5d0553fa5edd7178dbf18cf986d2172eaf2-rimg-w1200-h675-gmir.jpg?v=1655230338"), 189 | backgroundURL: URL(string:"https://www.nycgo.com/images/itineraries/42961/soc_fb_dumbo_spots__facebook.jpg") 190 | ) 191 | Text(verbatim: "hey! :)") 192 | } 193 | } 194 | 195 | private var contentTransition: AnyTransition { 196 | .opacity.animation(.spring()) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Components/StackedTileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackedTileView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 05/07/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Used when there's lots of content to display. 11 | struct StackedTileView: View { 12 | let primaryText: String? 13 | let secondaryText: String? 14 | let primaryColor: Color 15 | let secondaryColor: Color 16 | var backgroundStyle: BackgroundType 17 | 18 | init( 19 | primaryText: String?, 20 | secondaryText: String?, 21 | primaryColor: Color = Color.primary, 22 | secondaryColor: Color = Color.secondary, 23 | backgroundStyle: BackgroundType 24 | ) { 25 | self.primaryText = primaryText 26 | self.secondaryText = secondaryText 27 | self.primaryColor = primaryColor 28 | self.secondaryColor = secondaryColor 29 | self.backgroundStyle = backgroundStyle 30 | } 31 | 32 | init( 33 | primaryText: String?, 34 | secondaryText: String?, 35 | primaryColor: Color = Color.primary, 36 | secondaryColor: Color = Color.secondary, 37 | backgroundStyle: Color = Color.cellBackground 38 | ) where BackgroundType == Color { 39 | self.primaryText = primaryText 40 | self.secondaryText = secondaryText 41 | self.primaryColor = primaryColor 42 | self.secondaryColor = secondaryColor 43 | self.backgroundStyle = backgroundStyle 44 | } 45 | 46 | var body: some View { 47 | VStack(alignment: .leading, spacing: Padding.stackGap) { 48 | if let primaryText = primaryText { 49 | Text(primaryText) 50 | .font(.headline.weight(.semibold)) 51 | .foregroundColor(primaryColor) 52 | } 53 | 54 | if let secondaryText = secondaryText { 55 | Text(.init(secondaryText)) 56 | .font(.subheadline.weight(.regular)) 57 | .foregroundColor(secondaryColor) 58 | } 59 | } 60 | .frame(maxWidth: .infinity, minHeight: Constants.compactCellMinimumHeight, alignment: .leading) 61 | .multilineTextAlignment(.leading) 62 | .padding(Padding.cell) 63 | .background( 64 | backgroundStyle, 65 | in: RoundedRectangle(cornerRadius: Constants.cellRadius) 66 | ) 67 | .accessibilityElement(children: .ignore) 68 | .accessibilityLabel(accessibilityLabel) 69 | } 70 | 71 | private var accessibilityLabel: String { 72 | [primaryText?.noEmojis, secondaryText?.noEmojis] 73 | .compactMap { $0 } 74 | .joined(separator: ", ") 75 | } 76 | } 77 | 78 | 79 | struct StackedTileView_Previews: PreviewProvider { 80 | static var previews: some View { 81 | ZStack { 82 | Color(uiColor: .systemGroupedBackground).edgesIgnoringSafeArea(.all) 83 | VStack(spacing: Padding.cellGap) { 84 | StackedTileView( 85 | primaryText: "Primary", secondaryText: "Walkin' through a crowd, the village is aglow\nKaleidoscope of loud heartbeats under coats\nEverybody here wanted somethin' more\nSearchin' for a sound we hadn't heard before" 86 | ) 87 | StackedTileView( 88 | primaryText: "Primary", 89 | secondaryText: "And it said\nWelcome to New York, it's been waitin' for you\nWelcome to New York, welcome to New York", 90 | primaryColor: .white, 91 | secondaryColor: .white, 92 | backgroundStyle: .red 93 | ) 94 | StackedTileView( 95 | primaryText: "Primary", 96 | secondaryText: "Like any great love, it keeps you guessing\nLike any real love, it's ever-changing\nLike any true love, it drives you crazy\nBut you know you wouldn't change anything, anything, anything", 97 | primaryColor: .white, 98 | secondaryColor: .white.opacity(0.8), 99 | backgroundStyle: LinearGradient(colors: [.blue, .teal], startPoint: .leading, endPoint: .trailing) 100 | ) 101 | } 102 | .padding() 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Components/WebView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 12/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import WebKit 10 | 11 | struct WebView: UIViewRepresentable { 12 | let urlString: String 13 | 14 | init(url: String) { 15 | urlString = url 16 | } 17 | 18 | func makeUIView(context: Context) -> WKWebView { 19 | let webView = WKWebView() 20 | webView.allowsBackForwardNavigationGestures = true 21 | webView.scrollView.isScrollEnabled = true 22 | return webView 23 | } 24 | 25 | func updateUIView(_ uiView: WKWebView, context: Context) { 26 | guard let url = URL(string: urlString) else { return } 27 | let request = URLRequest(url: url) 28 | uiView.load(request) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Local/BottomSheetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottomSheetView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by LUCKY AGARWAL on 25/07/22. 6 | // 7 | 8 | import SwiftUI 9 | import ReadabilityModifier 10 | 11 | struct BottomSheetView: View { 12 | @Binding var isOpen: Bool 13 | @Binding var selectedCategory: Local.LocationCategory? 14 | 15 | @GestureState private var translation: CGFloat = 0 16 | 17 | private let categories: [Local.LocationCategory] 18 | private let error: Error? 19 | 20 | private let maxHeight: CGFloat 21 | private let minHeight: CGFloat 22 | 23 | private var offsetY: CGFloat { 24 | isOpen ? 0 : maxHeight - minHeight 25 | } 26 | 27 | internal init ( 28 | isOpen: Binding, 29 | selectedCategory: Binding, 30 | categories: [Local.LocationCategory], 31 | error: Error?, 32 | maxHeight: CGFloat 33 | ){ 34 | self.minHeight = maxHeight * Constants.minHeightRatio 35 | self.maxHeight = maxHeight 36 | self.categories = categories.filter { $0.locations.isEmpty == false } 37 | self.error = error 38 | self._isOpen = isOpen 39 | self._selectedCategory = selectedCategory 40 | } 41 | 42 | var body: some View { 43 | GeometryReader { geometry in 44 | VStack { 45 | VStack(spacing: Padding.cellGap) { 46 | Spacer() 47 | SectionHeader(title: "Local", 48 | fontStyle: .title2.weight(.semibold), 49 | foregroundColor: .primary) 50 | .fitToReadableContentGuide(type: .width) 51 | ScrollView { 52 | ForEach(categories) { category in 53 | LocalCell( 54 | label: category.name, 55 | imageName: category.symbolName, 56 | foregroundColor: (category == selectedCategory ? .accent : .cellForeground), 57 | labelFontStyle: .body) { 58 | selectedCategory = category 59 | } 60 | } 61 | .padding(.bottom, Padding.screen) 62 | } 63 | .fitToReadableContentGuide(type: .width) 64 | .transition(.opacity) 65 | } 66 | .padding(Padding.screen) 67 | } 68 | .frame(width: geometry.size.width, height: self.maxHeight, alignment: .top) 69 | .background(Color.background) 70 | .cornerRadius(Constants.bottomSheetRadius) 71 | .frame(height: geometry.size.height + Padding.screen, alignment: .bottom) 72 | .offset(y: max(self.offsetY + self.translation, 0)) 73 | .animation(.interactiveSpring(), value: isOpen) 74 | .animation(.interactiveSpring(), value: translation) 75 | .gesture( 76 | DragGesture().updating(self.$translation) { value, state, _ in 77 | state = value.translation.height 78 | }.onEnded { value in 79 | let snapDistance = self.maxHeight * Constants.snapRatio 80 | guard abs(value.translation.height) > snapDistance else { 81 | return 82 | } 83 | self.isOpen = value.translation.height < 0 84 | } 85 | ) 86 | .ignoresSafeArea(edges: .bottom) 87 | } 88 | } 89 | } 90 | 91 | struct BottomSheet_Previews: PreviewProvider { 92 | static let items: [Local.LocationCategory] = [ 93 | Local.LocationCategory(id: UUID(), name: "Food", symbolName: "takeoutbag.and.cup.and.straw.fill", locations: [.init(id: UUID(), name: "Trinity Kitchen", url: URL(string: "https://trinityleeds.com/shops/trinity-kitchen")!, location: .init(latitude: 53.797378, longitude: -1.545209))]), 94 | Local.LocationCategory(id: UUID(), name: "Drinks", symbolName: "wineglass.fill", locations: [.init(id: UUID(), name: "Brew Society", url: URL(string: "https://www.brewsociety.co.uk/")!, location: .init(latitude: 53.79584058588689, longitude: -1.550339186509128))]) 95 | ] 96 | 97 | static var previews: some View { 98 | GeometryReader{ proxy in 99 | BottomSheetView( 100 | isOpen: .constant(true), 101 | selectedCategory: .constant(Self.items.first), 102 | categories: Self.items, 103 | error: nil, 104 | maxHeight: proxy.size.height * Constants.maxHeightRatio 105 | ) 106 | .background(.blue) 107 | .previewDevice(PreviewDevice(rawValue: "iPhone 13")) 108 | } 109 | .edgesIgnoringSafeArea(.all) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Local/LocalCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalCell.swift 3 | // SwiftLeeds 4 | // 5 | // Created by LUCKY AGARWAL on 25/07/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LocalCell: View { 11 | let label: String 12 | let imageName: String 13 | let foregroundColor: Color 14 | let labelFontStyle: Font 15 | let onTap: () -> () 16 | 17 | internal init(label : String, 18 | imageName: String, 19 | foregroundColor: Color = .cellForeground, 20 | labelFontStyle: Font = .headline.weight(.medium), 21 | onTap: @escaping () -> () = {} 22 | ){ 23 | self.label = label 24 | self.imageName = imageName 25 | self.foregroundColor = foregroundColor 26 | self.labelFontStyle = labelFontStyle 27 | self.onTap = onTap 28 | } 29 | 30 | var body: some View { 31 | Button(action: onTap) { 32 | HStack { 33 | Text(label) 34 | .font(labelFontStyle) 35 | Spacer() 36 | 37 | Image(uiImage: UIImage(systemName: imageName) ?? UIImage(imageLiteralResourceName: imageName)) 38 | .renderingMode(.template) 39 | .frame(width: 30) 40 | } 41 | .padding(Padding.cell) 42 | .frame(minHeight: Constants.cellMinimumHeight) 43 | .foregroundColor(foregroundColor) 44 | .background { 45 | RoundedRectangle(cornerRadius: Constants.cellRadius).fill(Color.cellBackground) 46 | } 47 | } 48 | } 49 | } 50 | 51 | struct LocalCell_Previews: PreviewProvider { 52 | static var previews: some View { 53 | VStack { 54 | LocalCell(label: "Food", imageName: "takeoutbag.and.cup.and.straw.fill") 55 | LocalCell(label: "Coffee", imageName: "cup.and.saucer.fill") 56 | LocalCell(label: "Drink", imageName: "wineglass.fill") 57 | LocalCell(label: "Best of Leeds", imageName: "mappin") 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Local/LocalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 25/06/2022. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | struct LocalView: View { 12 | @StateObject private var model = LocalViewModel() 13 | 14 | @State private var bottomSheetShown = true 15 | @State private var mapRegion: MKCoordinateRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 53.78613099154973, longitude: -1.5461652186147719), span: MKCoordinateSpan(latitudeDelta: 0.04, longitudeDelta: 0.04)) 16 | @State private var selectedLocation: Local.Location? 17 | 18 | var body: some View { 19 | ZStack { 20 | GeometryReader{ geometry in 21 | if let category = model.selectedCategory { 22 | Map(coordinateRegion: $mapRegion, showsUserLocation: true, annotationItems: model.selectedLocations) { location in 23 | MapAnnotation(coordinate: location.location.coordinate) { 24 | Image(uiImage: UIImage(systemName: category.symbolName) ?? UIImage(imageLiteralResourceName: category.symbolName)) 25 | .frame(width: 44, height: 44) 26 | .background( 27 | RoundedRectangle(cornerRadius: 6) 28 | .fill(.white) 29 | ) 30 | .onTapGesture { 31 | selectedLocation = location 32 | 33 | } 34 | } 35 | } 36 | .ignoresSafeArea() 37 | } 38 | 39 | if let location = selectedLocation { 40 | ZStack { 41 | Color.black.opacity(0.3) 42 | .ignoresSafeArea(.all) 43 | .onTapGesture { 44 | selectedLocation = nil 45 | } 46 | 47 | locationInfoView(category: model.selectedCategory!, location: location) 48 | .padding(.bottom, bottomSheetShown ? geometry.size.height * Constants.maxHeightRatio: 0) 49 | .animation(.easeInOut, value: bottomSheetShown) 50 | } 51 | } 52 | 53 | BottomSheetView( 54 | isOpen: $bottomSheetShown, 55 | selectedCategory: $model.selectedCategory, 56 | categories: model.categories, 57 | error: model.error, 58 | maxHeight: geometry.size.height * Constants.maxHeightRatio 59 | ) 60 | 61 | if model.error != nil { 62 | errorView 63 | } 64 | } 65 | } 66 | } 67 | 68 | var errorView: some View { 69 | Rectangle() 70 | .foregroundStyle(.ultraThinMaterial) 71 | .edgesIgnoringSafeArea(.all) 72 | .overlay( 73 | VStack(alignment: .center, spacing: Padding.stackGap) { 74 | Text(verbatim: "Something has gone wrong. Please try again later.") 75 | .font(.subheadline.weight(.medium)) 76 | .multilineTextAlignment(.center) 77 | Button(action: { reload() }) { 78 | Text(verbatim: "Reload") 79 | } 80 | } 81 | .padding() 82 | ) 83 | } 84 | 85 | private func reload() { 86 | Task(priority: .userInitiated) { 87 | await model.loadData() 88 | } 89 | } 90 | 91 | private func locationInfoView(category: Local.LocationCategory, location: Local.Location) -> some View { 92 | VStack(spacing: 10) { 93 | Image(uiImage: UIImage(systemName: category.symbolName) ?? UIImage(imageLiteralResourceName: category.symbolName)) 94 | .renderingMode(.template) 95 | .frame(width: 44, height: 44) 96 | 97 | Text(location.name) 98 | 99 | Button { 100 | UIApplication.shared.open(location.url) 101 | } label: { 102 | Text(verbatim: "View More") 103 | .bold() 104 | .padding(12) 105 | .frame(maxWidth: .infinity) 106 | .foregroundColor(Color.accent) 107 | .background(Color.accent.opacity(0.5)) 108 | .clipShape(RoundedRectangle(cornerRadius: 6)) 109 | } 110 | } 111 | .padding(10) 112 | .padding(.bottom, 5) 113 | .frame(width: 200) 114 | .foregroundColor(Color.cellForeground) 115 | .background(Color.cellBackground) 116 | .clipShape(RoundedRectangle(cornerRadius: 8)) 117 | } 118 | } 119 | 120 | struct LocalView_Previews: PreviewProvider { 121 | static var previews: some View { 122 | LocalView() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Local/LocalViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalViewModel.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 08/08/2022. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | class LocalViewModel: ObservableObject { 12 | @Published private(set) var categories: [Local.LocationCategory] = [] 13 | @Published private(set) var selectedLocations: [Local.Location] = [] 14 | 15 | @Published var selectedCategory: Local.LocationCategory? { 16 | didSet { selectedLocations = selectedCategory?.locations ?? [] } 17 | } 18 | 19 | private(set) var error: Error? 20 | 21 | init() { 22 | Task { 23 | await loadData() 24 | } 25 | } 26 | 27 | func loadData() async { 28 | do { 29 | let localResults = try await URLSession.awaitConnectivity.decode(Requests.local, dateDecodingStrategy: Requests.defaultDateDecodingStratergy) 30 | await updateLocal(localResults) 31 | } catch { 32 | if let cachedResponse = try? await URLSession.shared.cached(Requests.local, dateDecodingStrategy: Requests.defaultDateDecodingStratergy) { 33 | await updateLocal(cachedResponse) 34 | } else { 35 | self.error = error 36 | } 37 | } 38 | } 39 | 40 | @MainActor 41 | private func updateLocal(_ localResults: Local) async { 42 | self.categories = localResults.data 43 | self.selectedCategory = self.categories.first 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/My Conference/ActivityView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 06/08/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ActivityView: View { 11 | let activity: Activity 12 | 13 | @Environment(\.openURL) var openURL 14 | 15 | var body: some View { 16 | SwiftLeedsContainer { 17 | ScrollView { 18 | content 19 | } 20 | } 21 | .edgesIgnoringSafeArea(.top) 22 | } 23 | 24 | private var content: some View { 25 | VStack(spacing: Padding.stackGap) { 26 | FancyHeaderView( 27 | title: activity.title, 28 | foregroundImageURLs: foregroundImageURLs 29 | ) 30 | 31 | StackedTileView( 32 | primaryText: activity.subtitle, 33 | secondaryText: activity.description, 34 | secondaryColor: Color.primary 35 | ) 36 | .padding(Padding.screen) 37 | } 38 | } 39 | 40 | private var foregroundImageURLs: [URL] { 41 | if let image = activity.image?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), let url = URL(string: image) { 42 | return [url] 43 | } else { 44 | return [] 45 | } 46 | 47 | } 48 | } 49 | 50 | struct ActivityView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | ActivityView(activity: .lunch) 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/My Conference/AnnouncementCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnnouncementCell.swift.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 25/06/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AnnouncementCell: View { 11 | @Environment(\.sizeCategory) var sizeCategory 12 | 13 | let label: String 14 | let value: String 15 | let valueIcon: String 16 | let gradientColors: [Color] 17 | 18 | var body: some View { 19 | HStack { 20 | Text(label) 21 | .font(.headline.weight(.bold)) 22 | 23 | Spacer() 24 | 25 | sizeAwareStack { 26 | Image(systemName: valueIcon) 27 | .font(.title.weight(.semibold)) 28 | Text(value) 29 | .font(.subheadline.weight(.semibold)) 30 | } 31 | } 32 | .foregroundColor(.white) 33 | .padding(Padding.cell) 34 | .frame(minHeight: Constants.cellMinimumHeight) 35 | .background { 36 | RoundedRectangle(cornerRadius: Constants.cellRadius) 37 | .fill(LinearGradient(colors: gradientColors, startPoint: .topLeading, endPoint: .topTrailing)) 38 | } 39 | .accessibilityElement(children: .ignore) 40 | .accessibilityLabel("\(label). \(value)") 41 | } 42 | 43 | 44 | // When the text is huge, stack vertically instead to avoid compressing the leading text 45 | @ViewBuilder 46 | func sizeAwareStack(@ViewBuilder content: () -> (Content)) -> some View { 47 | if sizeCategory > .accessibilityLarge { 48 | VStack { 49 | content() 50 | } 51 | } else { 52 | HStack { 53 | content() 54 | } 55 | } 56 | } 57 | } 58 | 59 | struct AnnouncementCell_Previews: PreviewProvider { 60 | static var previews: some View { 61 | VStack(spacing: Padding.cellGap) { 62 | AnnouncementCell(label: "Leeds", value: "26℃", valueIcon: "cloud.sun.fill", gradientColors: [.weatherGradientStart, .weatherGradientEnd]) 63 | .previewDisplayName("Weather") 64 | 65 | AnnouncementCell(label: "Get your ticket now", value: "69 days", valueIcon: "calendar.circle", gradientColors: [.buyTicketGradientStart, .buyTicketGradientEnd]) 66 | .previewDisplayName("Buy Ticket") 67 | } 68 | .padding(20) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/My Conference/MyConferenceView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyConferenceView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 14/11/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MyConferenceView: View { 11 | @StateObject private var viewModel = MyConferenceViewModel() 12 | 13 | @State private var currentIndex: Int = 0 14 | @Namespace private var namespace 15 | 16 | var body: some View { 17 | NavigationView { 18 | VStack(spacing: 0) { 19 | Divider() 20 | 21 | if viewModel.hasLoaded == false { 22 | ZStack { 23 | Color.clear 24 | 25 | ProgressView() 26 | .progressViewStyle(.circular) 27 | .scaleEffect(2) 28 | } 29 | } else if viewModel.slots.isEmpty { 30 | empty 31 | } else { 32 | schedule 33 | } 34 | 35 | Divider() 36 | } 37 | .background(Color.listBackground) 38 | .navigationTitle("Schedule") 39 | .toolbar { 40 | if let currentEvent = viewModel.currentEvent { 41 | ToolbarItem(placement: .navigationBarTrailing) { 42 | Menu { 43 | ForEach(viewModel.events) { event in 44 | Button(action: { viewModel.updateCurrentEvent(event) }) { 45 | Text(event.name) 46 | } 47 | } 48 | } label: { 49 | HStack { 50 | Text(currentEvent.name) 51 | Image(systemName: "chevron.up.chevron.down") 52 | .font(.caption) 53 | 54 | } 55 | } 56 | .accentColor(Color("AccentColor")) 57 | } 58 | } 59 | } 60 | } 61 | .navigationViewStyle(.stack) 62 | .accentColor(.white) 63 | .task { 64 | try? await viewModel.loadSchedule() 65 | } 66 | } 67 | 68 | private var schedule: some View { 69 | VStack(spacing: 0) { 70 | ViewThatFits { 71 | scheduleHeaders 72 | 73 | ScrollView(.horizontal) { 74 | scheduleHeaders 75 | } 76 | } 77 | 78 | TabView(selection: $currentIndex) { 79 | ForEach(Array(zip(viewModel.days.indices, viewModel.days)), 80 | id: \.0) { index, key in 81 | ScheduleView(slots: viewModel.slots[key] ?? [], showSlido: viewModel.showSlido) 82 | .tag(index) 83 | } 84 | } 85 | .tabViewStyle(.page(indexDisplayMode: .never)) 86 | .edgesIgnoringSafeArea(.all) 87 | } 88 | } 89 | 90 | @ViewBuilder 91 | private var scheduleHeaders: some View { 92 | if viewModel.days.count == 3 { 93 | // Temporary solution until new API is ready to support days correctly 94 | HStack(spacing: 20) { 95 | tabBarHeader(title: "Talkshow", index: 0) 96 | tabBarHeader(title: "Day 1", index: 1) 97 | tabBarHeader(title: "Day 2", index: 2) 98 | } 99 | .padding(.horizontal) 100 | .padding(.top) 101 | } else if viewModel.days.count > 1 { 102 | HStack(spacing: 20) { 103 | ForEach(Array(zip(viewModel.days.indices, viewModel.days)), id: \.0) { index, key in 104 | tabBarHeader(title: "Day \(index + 1)", index: index) 105 | } 106 | } 107 | .padding(.horizontal) 108 | .padding(.top) 109 | } 110 | } 111 | 112 | private func tabBarHeader(title: String, index: Int) -> some View { 113 | Button { 114 | currentIndex = index 115 | } label: { 116 | VStack(spacing: 4) { 117 | Text(title) 118 | .font(.system(size: 13, weight: .light, design: .default)) 119 | 120 | Text("") 121 | .frame(height: 2) 122 | } 123 | .foregroundColor(.cellForeground) 124 | .overlay(alignment: .bottom) { 125 | if currentIndex == index { 126 | Color.cellForeground 127 | .frame(height: 2) 128 | .matchedGeometryEffect(id: "tabSelectionLine", in: namespace, properties: .frame) 129 | } else { 130 | Color.clear.frame(height: 2) 131 | } 132 | 133 | } 134 | .animation(.spring(), value: currentIndex) 135 | } 136 | .buttonStyle(.plain) 137 | } 138 | 139 | @ViewBuilder 140 | private var tickets: some View { 141 | if let numberOfDaysToConference = viewModel.numberOfDaysToConference { 142 | AnnouncementCell(label: "Get your ticket now!", 143 | value: "\(numberOfDaysToConference) days", 144 | valueIcon: "calendar.circle.fill", 145 | gradientColors: [.accent, .accent]) 146 | .previewDisplayName("Buy Ticket") 147 | } 148 | } 149 | 150 | private var empty: some View { 151 | VStack(spacing: 10) { 152 | Spacer() 153 | 154 | Image(systemName: "signpost.right.and.left") 155 | .font(.system(size: 60)) 156 | 157 | Text("Come back soon") 158 | .font(.title) 159 | 160 | Text("We're working on filling this schedule") 161 | .font(.subheadline) 162 | 163 | Spacer() 164 | } 165 | .foregroundColor(.cellForeground) 166 | .accessibilityElement(children: .ignore) 167 | .accessibilityLabel("Come back soon. We're working on filling this schedule") 168 | } 169 | } 170 | 171 | struct MyConferenceView_Previews: PreviewProvider { 172 | static var previews: some View { 173 | MyConferenceView() 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/My Conference/MyConferenceViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyConferenceViewModel.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 01/08/2022. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import SwiftUI 11 | 12 | class MyConferenceViewModel: ObservableObject { 13 | @Published private(set) var hasLoaded = false 14 | @Published private(set) var event: Schedule.Event? 15 | @Published private(set) var events: [Schedule.Event] = [] 16 | @Published private(set) var days: [String] = [] 17 | @Published private(set) var slots: [String: [Schedule.Slot]] = [:] 18 | @Published private(set) var currentEvent: Schedule.Event? 19 | 20 | func loadSchedule() async throws { 21 | do { 22 | let schedule = try await URLSession.awaitConnectivity.decode(Requests.schedule, dateDecodingStrategy: Requests.defaultDateDecodingStratergy) 23 | await updateSchedule(schedule) 24 | 25 | do { 26 | let data = try PropertyListEncoder().encode(slots) 27 | UserDefaults(suiteName: "group.uk.co.swiftleeds")?.setValue(data, forKey: "Slots") 28 | } catch { 29 | throw(error) 30 | } 31 | } catch { 32 | if let cachedResponse = try? await URLSession.shared.cached(Requests.schedule, dateDecodingStrategy: Requests.defaultDateDecodingStratergy) { 33 | await updateSchedule(cachedResponse) 34 | } else { 35 | throw(error) 36 | } 37 | } 38 | } 39 | 40 | @MainActor 41 | private func updateSchedule(_ schedule: Schedule) async { 42 | event = schedule.data.event 43 | events = schedule.data.events.sorted(by: { $0.name < $1.name }) 44 | 45 | // Set the event to the current one on first launch 46 | if currentEvent == nil { 47 | currentEvent = event 48 | } 49 | 50 | let individualDates = Set(schedule.data.slots.compactMap { $0.date?.withoutTimeAtConferenceVenue }).sorted(by: (<)) 51 | days = individualDates.map { Helper.shortDateFormatter.string(from: $0) } 52 | 53 | for date in individualDates { 54 | let key = Helper.shortDateFormatter.string(from: date) 55 | slots[key] = schedule.data.slots.filter { Calendar.current.compare(date, to: $0.date ?? Date(), toGranularity: .day) == .orderedSame } 56 | } 57 | 58 | hasLoaded = true 59 | } 60 | 61 | private func reloadSchedule() async throws { 62 | guard let currentEvent else { return } 63 | 64 | let schedule = try await URLSession.awaitConnectivity.decode(Requests.schedule(for: currentEvent.id), dateDecodingStrategy: Requests.defaultDateDecodingStratergy, filename: "schedule-\(currentEvent.id.uuidString)") 65 | await updateSchedule(schedule) 66 | } 67 | 68 | var numberOfDaysToConference: Int? { 69 | guard let days = event?.daysUntil else { return nil } 70 | 71 | // Stop showing ticket sales a week before the event 72 | if days > 7 { 73 | return days 74 | } else { 75 | return nil 76 | } 77 | } 78 | 79 | // Only show slido links on the day of the event 80 | var showSlido: Bool { 81 | guard let days = event?.daysUntil else { return false } 82 | return days <= 0 && days >= -1 83 | } 84 | 85 | static let stringDateFormatter: DateFormatter = { 86 | let dateFormatter = DateFormatter() 87 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm" 88 | return dateFormatter 89 | }() 90 | 91 | func updateCurrentEvent(_ event: Schedule.Event) { 92 | currentEvent = event 93 | 94 | Task { 95 | try? await reloadSchedule() 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/My Conference/ScheduleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScheduleView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 21/08/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ScheduleView: View { 11 | let slots: [Schedule.Slot] 12 | let showSlido: Bool 13 | 14 | var body: some View { 15 | ScrollView { 16 | VStack(spacing: Padding.cellGap) { 17 | ForEach(slots) { slot in 18 | if let activity = slot.activity { 19 | NavigationLink { 20 | ActivityView(activity: activity) 21 | } label: { 22 | TalkCell(time: slot.startTime, details: activity.title) 23 | .transition(.opacity) 24 | } 25 | } 26 | 27 | if let presentation = slot.presentation { 28 | NavigationLink { 29 | SpeakerView(presentation: presentation, 30 | showSlido: showSlido) 31 | } label: { 32 | TalkCell(time: slot.startTime, 33 | details: presentation.title, 34 | speakers: presentation.speakers) 35 | .transition(.opacity) 36 | } 37 | } 38 | } 39 | } 40 | .animation(.easeInOut, value: slots) 41 | .padding(Padding.screen) 42 | } 43 | } 44 | } 45 | 46 | struct ScheduleView_Previews: PreviewProvider { 47 | static var previews: some View { 48 | ScheduleView(slots: [], showSlido: true) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/My Conference/SpeakerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpeakerView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by LUCKY AGARWAL on 23/07/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SpeakerView: View { 11 | let presentation: Presentation 12 | let showSlido: Bool 13 | 14 | @State private var showWebSheet = false 15 | 16 | @Environment(\.openURL) var openURL 17 | 18 | var body: some View { 19 | SwiftLeedsContainer { 20 | ScrollView { 21 | content 22 | } 23 | } 24 | .edgesIgnoringSafeArea(.top) 25 | } 26 | 27 | private var content: some View { 28 | VStack(spacing: Padding.stackGap) { 29 | if presentation.speakers.isEmpty == false { 30 | FancyHeaderView( 31 | title: presentation.speakers.joinedNames, 32 | foregroundImageURLs: presentation.speakers.map { URL(string: $0.profileImage)! } 33 | ) 34 | } 35 | 36 | VStack(spacing: Padding.screen){ 37 | StackedTileView( 38 | primaryText: presentation.title, 39 | secondaryText: presentation.synopsis, 40 | secondaryColor: Color.primary 41 | ) 42 | 43 | if let videoURL = presentation.videoURL, videoURL.isEmpty == false { 44 | CommonTileView( 45 | icon: "video.fill", 46 | primaryText: "Watch video", 47 | showChevron: true, 48 | secondaryColor: Color.primary 49 | ) 50 | .accessibilityHint("Opens the video") 51 | .accessibilityAddTraits(.isButton) 52 | .onTapGesture { 53 | openURL(URL(string: videoURL)!) 54 | } 55 | } 56 | 57 | if showSlido, presentation.slidoURL?.isEmpty == false { 58 | CommonTileButton( 59 | icon: "questionmark.bubble.fill", 60 | primaryText: "Ask Questions Now", 61 | accessibilityHint: "Opens Slido to allow questions to be asked", 62 | primaryColor: .white, 63 | secondaryColor: .white.opacity(0.8), 64 | backgroundStyle: LinearGradient(gradient: Gradient(colors:[.buyTicketGradientStart, .buyTicketGradientEnd]) ,startPoint: .leading, endPoint: .trailing), 65 | onTap: { 66 | showWebSheet.toggle() 67 | } 68 | ) 69 | } 70 | 71 | ForEach(presentation.speakers) { speaker in 72 | StackedTileView( 73 | primaryText: "About\(presentation.speakers.count == 1 ? "" : ": \(speaker.name)")", 74 | secondaryText: speaker.biography, 75 | secondaryColor: Color.primary 76 | ) 77 | 78 | if let twitter = speaker.twitter, twitter.isEmpty == false { 79 | CommonTileView( 80 | primaryText: "Twitter", 81 | secondaryText: "@\(twitter)", 82 | secondaryColor: Color.primary 83 | ) 84 | .accessibilityHint("Opens twitter for this speaker") 85 | .accessibilityAddTraits(.isButton) 86 | .onTapGesture { 87 | openURL(URL(string: "https://twitter.com/\(twitter)")!) 88 | } 89 | } 90 | } 91 | } 92 | .padding(Padding.screen) 93 | } 94 | .sheet(isPresented: $showWebSheet) { 95 | WebView(url: presentation.slidoURL ?? "") 96 | .edgesIgnoringSafeArea(.bottom) 97 | } 98 | } 99 | } 100 | 101 | struct SpeakerView_Previews: PreviewProvider { 102 | static var previews: some View { 103 | SpeakerView(presentation: .donnyWalls, showSlido: true) 104 | .previewDisplayName("Donny Wals") 105 | 106 | SpeakerView(presentation: .skyBet, showSlido: true) 107 | .previewDisplayName("Sky Bet") 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/My Conference/TalkCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TalkCell.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 25/06/2022. 6 | // 7 | 8 | import SwiftUI 9 | import CachedAsyncImage 10 | 11 | struct TalkCell: View { 12 | private let time: String 13 | private let details: String 14 | private let isNext: Bool 15 | private let speakers: [Speaker] 16 | private let gradientColors: [Color]? 17 | 18 | @Environment(\.colorScheme) var colorScheme 19 | 20 | init(time: String, details: String, isNext: Bool = false, speakers: [Speaker] = [], gradientColors: [Color]? = nil) { 21 | self.time = time 22 | self.details = details 23 | self.isNext = isNext 24 | self.speakers = speakers 25 | self.gradientColors = gradientColors 26 | } 27 | 28 | var body: some View { 29 | VStack(alignment: .leading, spacing: 6) { 30 | timeLabel(time) 31 | 32 | HStack { 33 | VStack(alignment: .leading, spacing: 8) { 34 | if speakers.isEmpty == false { 35 | HStack { 36 | VStack(alignment: .leading, spacing: 4) { 37 | Text(speakers.joinedNames) 38 | .font(.headline.weight(.medium)) 39 | .multilineTextAlignment(.leading) 40 | 41 | Text(speakers.joinedOrganisations) 42 | .font(.subheadline.weight(.medium)) 43 | .opacity(0.6) 44 | } 45 | 46 | Spacer() 47 | 48 | HStack(spacing: -5) { 49 | ForEach(speakers) { speaker in 50 | CachedAsyncImage( 51 | url: URL(string: speaker.profileImage), 52 | content: { image in 53 | image 54 | .resizable() 55 | .aspectRatio(contentMode: .fill) 56 | .frame(maxWidth: 40, maxHeight: 40) 57 | .clipShape(Circle()) 58 | }, 59 | placeholder: { 60 | Circle() 61 | .fill(.white) 62 | .opacity(0.3) 63 | .frame(maxWidth: 40, maxHeight: 40) 64 | .clipShape(Circle()) 65 | } 66 | ) 67 | } 68 | } 69 | } 70 | } 71 | 72 | HStack { 73 | Text(details) 74 | .font(.body.weight(.regular)) 75 | .multilineTextAlignment(.leading) 76 | Spacer() 77 | } 78 | } 79 | .padding(.trailing, 2) 80 | 81 | Image(systemName: "chevron.right") 82 | } 83 | .padding(Padding.cell) 84 | .frame(maxWidth: .infinity) 85 | .foregroundColor(isNext ? .white : .cellForeground) 86 | .background { 87 | if let gradientColors = gradientColors { 88 | RoundedRectangle(cornerRadius: Constants.cellRadius) 89 | .fill(LinearGradient(colors: gradientColors, startPoint: .topLeading, endPoint: .topTrailing)) 90 | } else { 91 | RoundedRectangle(cornerRadius: Constants.cellRadius) 92 | .strokeBorder(Color.cellBorder) 93 | } 94 | } 95 | } 96 | .accessibilityElement(children: .ignore) 97 | .accessibilityLabel(accessibilityLabel) 98 | } 99 | 100 | private func timeLabel(_ value: String) -> some View { 101 | HStack(spacing: 7) { 102 | Image("Clock") 103 | 104 | Text(value) 105 | .foregroundColor(.cellForeground) 106 | 107 | Spacer() 108 | } 109 | .padding(.leading, 4) 110 | } 111 | 112 | private var accessibilityLabel: String { 113 | [time, speakers.joinedNames, speakers.joinedOrganisations, details.noEmojis] 114 | .filter { $0.isEmpty == false } 115 | .joined(separator: ", ") 116 | } 117 | } 118 | 119 | struct TalkCell_Previews: PreviewProvider { 120 | static var previews: some View { 121 | VStack(spacing: Padding.cellGap) { 122 | TalkCell(time: "11:00", details: Presentation.donnyWalls.title, speakers: Presentation.donnyWalls.speakers) 123 | 124 | TalkCell(time: "12:00", details: "Lunch") 125 | 126 | TalkCell(time: "1:00", details: Presentation.skyBet.title, speakers: Presentation.skyBet.speakers) 127 | } 128 | .padding(Padding.screen) 129 | .background(Color.listBackground) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Sponsors/SponsorTileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SponsorTileView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Alex Logan on 01/07/2022. 6 | // 7 | 8 | import SwiftUI 9 | import CachedAsyncImage 10 | 11 | struct SponsorTileView: View { 12 | let sponsor: Sponsor 13 | 14 | @Environment(\.openURL) private var openURL 15 | 16 | var body: some View { 17 | Button(action: { openURL(sponsor.url) }) { 18 | VStack(alignment: .leading, spacing: 0) { 19 | image 20 | .padding(16) 21 | 22 | text 23 | 24 | if sponsor.jobs.isEmpty == false { 25 | Text("JOBS") 26 | .font(.caption) 27 | .fontWeight(.thin) 28 | .padding(Padding.cell) 29 | } 30 | 31 | ForEach(sponsor.jobs) { job in 32 | VStack(spacing: 0) { 33 | Divider() 34 | 35 | CommonTileButton(primaryText: job.title, 36 | subtitleText: job.location, 37 | accessibilityHint: "Opens a web site showing more details about the job", 38 | showChevron: true, 39 | backgroundStyle: Color.cellBackground) 40 | { 41 | openURL(job.url) 42 | } 43 | .foregroundColor(.secondary) 44 | } 45 | } 46 | } 47 | } 48 | .background(Color.cellBackground, in: contentShape) 49 | .buttonStyle(SquishyButtonStyle()) 50 | } 51 | 52 | private var image: some View { 53 | CachedAsyncImage( 54 | url: URL(string: sponsor.image), 55 | content: { image in 56 | Rectangle() 57 | .aspectRatio(1.66, contentMode: .fill) 58 | .foregroundColor(.clear) 59 | .background( 60 | image 61 | .resizable() 62 | .aspectRatio(contentMode: .fit) 63 | .transition(.opacity) 64 | ) 65 | .background(Color.cellBackground) 66 | .clipped() 67 | .transition(contentTransition) 68 | }, 69 | placeholder: { 70 | Rectangle() 71 | .foregroundColor(.cellBackground) 72 | .transition(contentTransition) 73 | .overlay(content: { 74 | ProgressView() 75 | .tint(.white) 76 | .opacity(0.5) 77 | }) 78 | } 79 | ) 80 | .aspectRatio(1.66, contentMode: .fit) 81 | .accessibilityHidden(true) 82 | } 83 | 84 | private var text: some View { 85 | VStack(alignment: .leading, spacing: 4) { 86 | Text(sponsor.name) 87 | .foregroundColor(.primary) 88 | .font(.subheadline.weight(.medium)) 89 | 90 | if sponsor.subtitle.isEmpty == false { 91 | Text(sponsor.subtitle) 92 | .foregroundColor(.secondary) 93 | .font(.subheadline.weight(.regular)) 94 | } 95 | } 96 | .accessibilityElement(children: .ignore) 97 | .accessibilityLabel( 98 | "Sponsor, \(sponsor.name), \(sponsor.subtitle)" 99 | ) 100 | .padding() 101 | .frame(minHeight: 55) 102 | } 103 | 104 | private var contentShape: some Shape { 105 | RoundedRectangle(cornerRadius: Constants.cellRadius, style: .continuous) 106 | } 107 | 108 | private var contentTransition: AnyTransition { 109 | .opacity.animation(.spring()) 110 | } 111 | 112 | private func openURL(_ urlString: String) { 113 | guard let link = URL(string: urlString) else { return } 114 | openURL(link) 115 | } 116 | } 117 | 118 | struct SponsorTileView_Previews: PreviewProvider { 119 | static var previews: some View { 120 | ZStack { 121 | Color.background.edgesIgnoringSafeArea(.all) 122 | 123 | VStack { 124 | SponsorTileView( 125 | sponsor: .sample 126 | ) 127 | } 128 | .padding() 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Sponsors/SponsorsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SponsorsView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Muralidharan Kathiresan on 25/06/23. 6 | // 7 | 8 | import SwiftUI 9 | import ReadabilityModifier 10 | 11 | struct SponsorsView: View { 12 | @StateObject private var viewModel = SponsorsViewModel() 13 | 14 | var body: some View { 15 | SwiftLeedsContainer { 16 | content 17 | } 18 | .edgesIgnoringSafeArea(.top) 19 | } 20 | 21 | private var content: some View { 22 | List { 23 | ForEach(viewModel.sections) { section in 24 | switch section.type { 25 | case .platinum: 26 | Section(header: sectionHeader(for: section.type)) { 27 | ForEach(section.sponsors) { sponsor in 28 | sponsorTile(for: sponsor) 29 | .listRowBackground(Color.clear) 30 | .listRowInsets(EdgeInsets()) 31 | .padding(.bottom, Padding.cellGap ) 32 | } 33 | } 34 | case .gold, .silver: 35 | Section(header: sectionHeader(for: section.type)) { 36 | grid(for: section.sponsors) 37 | .listRowBackground(Color.clear) 38 | .listRowInsets(EdgeInsets()) 39 | } 40 | } 41 | } 42 | } 43 | .padding(.top, 50) 44 | .scrollIndicators(.hidden) 45 | .scrollContentBackground(.hidden) 46 | .fitToReadableContentGuide(type: .width) 47 | .task { 48 | try? await viewModel.loadSponsors() 49 | } 50 | } 51 | } 52 | 53 | private extension SponsorsView { 54 | func sectionHeader(for sponsorLevel: SponsorLevel) -> some View { 55 | Text("\(sponsorLevel.rawValue.capitalized) Sponsors") 56 | .font(.callout.weight(.semibold)) 57 | .foregroundColor(.secondary) 58 | .frame(maxWidth:.infinity, alignment: .leading) 59 | .accessibilityAddTraits(.isHeader) 60 | .textCase(nil) 61 | .padding(.bottom, 8) 62 | } 63 | 64 | func sponsorTile(for sponsor: Sponsor) -> some View { 65 | SponsorTileView(sponsor: sponsor) 66 | } 67 | 68 | func grid(for sponsors: [Sponsor]) -> some View { 69 | let columns = [ 70 | GridItem(.flexible()), GridItem(.flexible()) 71 | ] 72 | 73 | return LazyVGrid( 74 | columns: columns, 75 | alignment: .leading, 76 | spacing: Padding.cellGap, 77 | pinnedViews: []) { 78 | ForEach(sponsors, id: \.self) { sponsor in 79 | sponsorTile(for: sponsor) 80 | } 81 | }.foregroundColor(.clear) 82 | } 83 | } 84 | 85 | struct SponsorsView_Previews: PreviewProvider { 86 | static var previews: some View { 87 | SwiftLeedsContainer { 88 | ScrollView { 89 | SponsorsView() 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Sponsors/SponsorsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SponsorsViewModel.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Muralidharan Kathiresan on 25/06/23. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import SwiftUI 11 | 12 | final class SponsorsViewModel: ObservableObject { 13 | @Published private(set) var sections: [Section] = [Section]() 14 | 15 | struct Section: Identifiable { 16 | let type: SponsorLevel 17 | let sponsors: [Sponsor] 18 | var id : String { type.rawValue } 19 | } 20 | 21 | func loadSponsors() async throws { 22 | do { 23 | let sponsors = try await URLSession.awaitConnectivity.decode(Requests.sponsors, dateDecodingStrategy: Requests.defaultDateDecodingStratergy) 24 | await updateSponsors(sponsors) 25 | } catch { 26 | if let cachedResponse = try? await URLSession.shared.cached(Requests.sponsors, dateDecodingStrategy: Requests.defaultDateDecodingStratergy) { 27 | await updateSponsors(cachedResponse) 28 | } else { 29 | throw(error) 30 | } 31 | } 32 | } 33 | 34 | @MainActor 35 | private func updateSponsors(_ sponsors: Sponsors) async { 36 | var sections: [Section] = [Section]() 37 | let sponsors = sponsors.data 38 | 39 | let platinumSponsors = sponsors 40 | .filter {$0.sponsorLevel == .platinum} 41 | .compactMap { $0 } 42 | if !platinumSponsors.isEmpty { 43 | sections.append(Section(type: .platinum, sponsors: platinumSponsors)) 44 | } 45 | 46 | let goldSponsors = sponsors 47 | .filter {$0.sponsorLevel == .gold} 48 | .compactMap { $0 } 49 | if !goldSponsors.isEmpty { 50 | sections.append(Section(type: .gold, sponsors: goldSponsors)) 51 | } 52 | 53 | let silverSponsors = sponsors 54 | .filter {$0.sponsorLevel == .silver} 55 | .compactMap { $0 } 56 | if !silverSponsors.isEmpty { 57 | sections.append(Section(type: .silver, sponsors: silverSponsors)) 58 | } 59 | 60 | self.sections = sections 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Tab/SidebarMainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarMainView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Karim Ebrahem on 26/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SidebarMainView: View { 11 | @EnvironmentObject var appState: AppState 12 | 13 | var body: some View { 14 | NavigationSplitView { 15 | SidebarView() 16 | } detail: { 17 | switch appState.selectedTab { 18 | case .conference: 19 | MyConferenceView() 20 | case .about: 21 | AboutView() 22 | case .location: 23 | LocalView() 24 | case .sponsors: 25 | SponsorsView() 26 | } 27 | } 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Tab/SidebarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Karim Ebrahem on 26/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SidebarView: View { 11 | @EnvironmentObject var appState: AppState 12 | 13 | var body: some View { 14 | List { 15 | NavigationLink(destination: MyConferenceView().onAppear { 16 | appState.selectedTab = .conference 17 | }) { 18 | Label("My Conference", systemImage: "person.fill") 19 | } 20 | NavigationLink(destination: LocalView().onAppear { 21 | appState.selectedTab = .location 22 | }) { 23 | Label("Local", systemImage: "map.fill") 24 | } 25 | NavigationLink(destination: AboutView().onAppear { 26 | appState.selectedTab = .about 27 | }) { 28 | Label("About", systemImage: "info.circle") 29 | } 30 | NavigationLink(destination: SponsorsView().onAppear { 31 | appState.selectedTab = .sponsors 32 | }) { 33 | Label("Sponsors", systemImage: "sparkles") 34 | } 35 | } 36 | .listStyle(.sidebar) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Tab/Tabs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tabs.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Matthew Gallagher on 25/06/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Tabs: View { 11 | @Environment(\.horizontalSizeClass) private var horizontalSizeClass 12 | 13 | var body: some View { 14 | GeometryReader { ruler in 15 | if ruler.size.width < ruler.size.height || horizontalSizeClass == .compact { 16 | TabsMainView() 17 | } else { 18 | SidebarMainView() 19 | } 20 | } 21 | } 22 | } 23 | 24 | struct Tabs_Previews: PreviewProvider { 25 | static var previews: some View { 26 | Tabs() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SwiftLeeds/Views/Tab/TabsMainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabsMainView.swift 3 | // SwiftLeeds 4 | // 5 | // Created by Karim Ebrahem on 26/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | import ReadabilityModifier 10 | 11 | struct TabsMainView: View { 12 | @EnvironmentObject var appState: AppState 13 | 14 | var body: some View { 15 | TabView(selection: $appState.selectedTab) { 16 | MyConferenceView() 17 | .tabItem { 18 | Label("My Conference", systemImage: "person.fill") 19 | } 20 | .tag(TabItems.conference) 21 | 22 | LocalView() 23 | .tabItem { 24 | Label("Local", systemImage: "map.fill") 25 | } 26 | .tag(TabItems.location) 27 | 28 | AboutView() 29 | .tabItem { 30 | Label("About", systemImage: "info.circle") 31 | } 32 | .tag(TabItems.about) 33 | SponsorsView() 34 | .tabItem { 35 | Label("Sponsors", systemImage: "sparkles") 36 | } 37 | .tag(TabItems.sponsors) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SwiftLeedsAppClip/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 | -------------------------------------------------------------------------------- /SwiftLeedsAppClip/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 | -------------------------------------------------------------------------------- /SwiftLeedsAppClip/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftLeedsAppClip/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SwiftLeedsAppClip 4 | // 5 | // Created by Muralidharan Kathiresan on 03/10/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | VStack { 13 | Image(systemName: "globe") 14 | .imageScale(.large) 15 | .foregroundColor(.accentColor) 16 | Text("Hello, world App Clip!") 17 | } 18 | .padding() 19 | } 20 | } 21 | 22 | struct ContentView_Previews: PreviewProvider { 23 | static var previews: some View { 24 | ContentView() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SwiftLeedsAppClip/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppClip 6 | 7 | NSAppClipRequestEphemeralUserNotification 8 | 9 | NSAppClipRequestLocationConfirmation 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /SwiftLeedsAppClip/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftLeedsAppClip/SwiftLeedsAppClip.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.associated-domains 6 | 7 | appclips:swiftleeds.co.uk 8 | 9 | com.apple.developer.parent-application-identifiers 10 | 11 | $(AppIdentifierPrefix)uk.co.swiftleeds.SwiftLeeds 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SwiftLeedsAppClip/SwiftLeedsAppClipApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLeedsAppClipApp.swift 3 | // SwiftLeedsAppClip 4 | // 5 | // Created by Muralidharan Kathiresan on 03/10/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SwiftLeedsAppClipApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | MyConferenceView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SwiftLeedsTests/SwiftLeedsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLeedsTests.swift 3 | // SwiftLeedsTests 4 | // 5 | // Created by Matthew Gallagher on 14/11/2021. 6 | // 7 | 8 | import XCTest 9 | @testable import SwiftLeeds 10 | 11 | class SwiftLeedsTests: XCTestCase { 12 | override func setUpWithError() throws {} 13 | } 14 | -------------------------------------------------------------------------------- /SwiftLeedsUITests/SwiftLeedsUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLeedsUITests.swift 3 | // SwiftLeedsUITests 4 | // 5 | // Created by Matthew Gallagher on 14/11/2021. 6 | // 7 | 8 | import XCTest 9 | 10 | class SwiftLeedsUITests: XCTestCase { 11 | override func setUpWithError() throws { 12 | // Put setup code here. This method is called before the invocation of each test method in the class. 13 | 14 | // In UI tests it is usually best to stop immediately when a failure occurs. 15 | continueAfterFailure = false 16 | 17 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 18 | } 19 | 20 | override func tearDownWithError() throws { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | } 23 | 24 | /*func testExample() throws { 25 | let app = XCUIApplication() 26 | app.launch() 27 | }*/ 28 | } 29 | -------------------------------------------------------------------------------- /SwiftLeedsUITests/SwiftLeedsUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLeedsUITestsLaunchTests.swift 3 | // SwiftLeedsUITests 4 | // 5 | // Created by Matthew Gallagher on 14/11/2021. 6 | // 7 | 8 | import XCTest 9 | 10 | class SwiftLeedsUITestsLaunchTests: XCTestCase { 11 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 12 | true 13 | } 14 | 15 | override func setUpWithError() throws { 16 | continueAfterFailure = false 17 | } 18 | 19 | /*func testLaunch() throws { 20 | let app = XCUIApplication() 21 | app.launch() 22 | 23 | // Insert steps here to perform after app launch but before taking a screenshot, 24 | // such as logging into a test account or navigating somewhere in the app 25 | 26 | let attachment = XCTAttachment(screenshot: app.screenshot()) 27 | attachment.name = "Launch Screen" 28 | attachment.lifetime = .keepAlways 29 | add(attachment) 30 | }*/ 31 | } 32 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/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 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/SwiftLeedsWidget/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /SwiftLeedsWidget/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.widgetkit-extension 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/SwiftLeedsMediumWidgetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLeedsMediumWidgetView.swift 3 | // SwiftLeedsWidgetExtension 4 | // 5 | // Created by karim ebrahim on 09/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import WidgetKit 10 | 11 | struct SwiftLeedsMediumWidgetView: View { 12 | 13 | // MARK: - Pivate Properties 14 | 15 | private let slot: Schedule.Slot 16 | 17 | // MARK: - Init 18 | 19 | init(slot: Schedule.Slot) { 20 | self.slot = slot 21 | } 22 | 23 | // MARK: - Body View 24 | 25 | var body: some View { 26 | buildSlotView(for: slot) 27 | } 28 | } 29 | 30 | // MARK: - View Builders 31 | 32 | extension SwiftLeedsMediumWidgetView { 33 | @ViewBuilder 34 | private func buildSlotView(for slot: Schedule.Slot) -> some View { 35 | if let activity = slot.activity { 36 | slotView(time: slot.startTime, speaker: activity.title, details: activity.description!) 37 | } 38 | 39 | if let presentation = slot.presentation { 40 | let speakers = presentation.speakers.joinedNames 41 | slotView(time: slot.startTime, speaker: speakers, details: presentation.title) 42 | } 43 | } 44 | 45 | private func slotView(time: String, speaker: String = "", details: String) -> some View { 46 | ZStack { 47 | Color.background 48 | contentView(time: time, speaker: speaker, details: details) 49 | } 50 | } 51 | 52 | private func contentView(time: String, speaker: String = "", details: String) -> some View { 53 | VStack(alignment: .leading) { 54 | HStack { 55 | logoView 56 | Spacer() 57 | ZStack { 58 | RoundedRectangle(cornerRadius: 14) 59 | .fill(Color.cellForeground.opacity(0.1)) 60 | .frame(width: 100, height: 30, alignment: .center) 61 | 62 | Text(verbatim: "Up Next") 63 | .font(.system(size: 14, weight: .bold, design: .monospaced)) 64 | } 65 | } 66 | Spacer() 67 | VStack(alignment: .leading, spacing: 4) { 68 | Text(time) 69 | .font(.system(size: WidgetConstants.timeFontSize, weight: .medium)) 70 | VStack(alignment: .leading) { 71 | Text(speaker) 72 | .font(.system(size: WidgetConstants.titleFontSize, weight: .bold, design: .default)) 73 | .foregroundColor(Color.accent) 74 | Text(details) 75 | .font(.system(size: WidgetConstants.detailsFontSize, weight: .regular, design: .default)) 76 | .lineLimit(2) 77 | } 78 | } 79 | } 80 | .padding(16) 81 | .frame(maxWidth: .infinity, alignment: .leading) 82 | } 83 | 84 | private var logoView: some View { 85 | Image(Assets.Image.swiftLeedsIconWithNoBackground) 86 | .resizable() 87 | .aspectRatio(contentMode: .fill) 88 | .transition(.opacity) 89 | .frame(width: WidgetConstants.logoImageWidth, height: WidgetConstants.logoImageHeight, alignment: .center) 90 | } 91 | } 92 | 93 | // MARK: - Widget Previews 94 | 95 | struct SwiftLeedsMediumWidgetView_Previews: PreviewProvider { 96 | static var previews: some View { 97 | SwiftLeedsMediumWidgetView(slot: Schedule.Slot(id: UUID(), date: Date(), startTime: "11:00 AM", duration: 1, activity: nil, presentation: Presentation.donnyWalls)) 98 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 99 | 100 | SwiftLeedsMediumWidgetView(slot: Schedule.Slot(id: UUID(), date: Date(), startTime: "11:00 AM", duration: 1, activity: nil, presentation: Presentation.donnyWalls)) 101 | .environment(\.colorScheme, .dark) 102 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/SwiftLeedsSmallWidgetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLeedsSmallWidgetView.swift 3 | // SwiftLeedsWidgetExtension 4 | // 5 | // Created by karim ebrahim on 08/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import WidgetKit 10 | 11 | struct SwiftLeedsSmallWidgetView: View { 12 | 13 | // MARK: - Pivate Properties 14 | 15 | private let slot: Schedule.Slot 16 | 17 | // MARK: - Init 18 | 19 | init(slot: Schedule.Slot) { 20 | self.slot = slot 21 | } 22 | 23 | // MARK: - Body View 24 | 25 | var body: some View { 26 | buildSlotView(for: slot) 27 | } 28 | } 29 | 30 | // MARK: - Widget Previews 31 | 32 | extension SwiftLeedsSmallWidgetView { 33 | @ViewBuilder 34 | private func buildSlotView(for slot: Schedule.Slot) -> some View { 35 | if let activity = slot.activity { 36 | slotView(time: slot.startTime, speaker: activity.title, details: activity.description!) 37 | } 38 | 39 | if let presentation = slot.presentation { 40 | let speakers = presentation.speakers.joinedNames 41 | slotView(time: slot.startTime, speaker: speakers, details: presentation.title) 42 | } 43 | } 44 | 45 | private func slotView(time: String, speaker: String = "", details: String) -> some View { 46 | ZStack { 47 | Color.background 48 | contentView(time: time, speaker: speaker, details: details) 49 | } 50 | } 51 | 52 | private func contentView(time: String, speaker: String = "", details: String) -> some View { 53 | VStack(alignment: .leading) { 54 | logoView 55 | Spacer() 56 | VStack(alignment: .leading, spacing: 4) { 57 | Text(time) 58 | .font(.system(size: WidgetConstants.timeFontSize, weight: .medium)) 59 | VStack(alignment: .leading) { 60 | Text(speaker) 61 | .font(.system(size: WidgetConstants.titleFontSize, weight: .bold, design: .default)) 62 | .foregroundColor(Color.accent) 63 | .lineLimit(2) 64 | Text(details) 65 | .font(.system(size: WidgetConstants.detailsFontSize, weight: .regular, design: .default)) 66 | .lineLimit(2) 67 | } 68 | } 69 | } 70 | .padding(16) 71 | .frame(maxWidth: .infinity, alignment: .leading) 72 | } 73 | 74 | private var logoView: some View { 75 | Image(Assets.Image.swiftLeedsIconWithNoBackground) 76 | .resizable() 77 | .aspectRatio(contentMode: .fill) 78 | .transition(.opacity) 79 | .frame(width: WidgetConstants.logoImageWidth, height: WidgetConstants.logoImageHeight, alignment: .center) 80 | } 81 | } 82 | 83 | struct SwiftLeedsSmallWidgetView_Previews: PreviewProvider { 84 | static var previews: some View { 85 | SwiftLeedsSmallWidgetView(slot: Schedule.Slot(id: UUID(), date: Date(), startTime: "11:00 AM", duration: 1, activity: nil, presentation: Presentation.skyBet)) 86 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 87 | 88 | SwiftLeedsSmallWidgetView(slot: Schedule.Slot(id: UUID(), date: Date(), startTime: "11:00 AM", duration: 1, activity: nil, presentation: Presentation.skyBet)) 89 | .environment(\.colorScheme, .dark) 90 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/SwiftLeedsWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLeedsWidget.swift 3 | // SwiftLeedsWidget 4 | // 5 | // Created by karim ebrahim on 05/09/2022. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | @main 12 | struct SwiftLeedsWidget: Widget { 13 | let kind: String = "SwiftLeedsWidget" 14 | 15 | var body: some WidgetConfiguration { 16 | StaticConfiguration(kind: kind, provider: Provider()) { entry in 17 | SwiftLeedsWidgetEntryView(entry: entry) 18 | } 19 | .configurationDisplayName("SwiftLeeds What's up next?") 20 | .description("This widget to know what is the next talk on our stage.") 21 | .supportedFamilies([.systemSmall, .systemMedium]) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/SwiftLeedsWidgetEntryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLeedsWidgetEntryView.swift 3 | // SwiftLeedsWidgetExtension 4 | // 5 | // Created by Karim Ebrahem on 11/09/2022. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | struct SwiftLeedsWidgetEntryView : View { 12 | var entry: Provider.Entry 13 | @Environment(\.widgetFamily) var family 14 | 15 | var body: some View { 16 | if family == .systemSmall { 17 | SwiftLeedsSmallWidgetView(slot: entry.slot) 18 | } else if family == .systemMedium { 19 | SwiftLeedsMediumWidgetView(slot: entry.slot) 20 | } 21 | } 22 | } 23 | 24 | struct SwiftLeedsWidget_Previews: PreviewProvider { 25 | static var previews: some View { 26 | SwiftLeedsWidgetEntryView(entry: SwiftLeedsWidgetEntry(date: Date(), slot: Schedule.Slot(id: UUID(), date: Date(), startTime: "11:00 AM", duration: 1, activity: Activity.lunch, presentation: Presentation.donnyWalls))) 27 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 28 | 29 | SwiftLeedsWidgetEntryView(entry: SwiftLeedsWidgetEntry(date: Date(), slot: Schedule.Slot(id: UUID(), date: Date(), startTime: "11:00 AM", duration: 1, activity: Activity.lunch, presentation: Presentation.donnyWalls))) 30 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/WidgetSetup/SwiftLeedsWidgetEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftLeedsWidgetEntry.swift 3 | // SwiftLeedsWidgetExtension 4 | // 5 | // Created by Karim Ebrahem on 11/09/2022. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | struct SwiftLeedsWidgetEntry: TimelineEntry { 12 | var date: Date 13 | let slot: Schedule.Slot 14 | } 15 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/WidgetSetup/TimeineProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelineProvider.swift 3 | // SwiftLeedsWidgetExtension 4 | // 5 | // Created by Karim Ebrahem on 11/09/2022. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | struct Provider: TimelineProvider { 12 | func placeholder(in context: Context) -> SwiftLeedsWidgetEntry { 13 | SwiftLeedsWidgetEntry(date: Date(), slot: Schedule.Slot(id: UUID(), date: Date(), startTime: "11:00 AM", duration: 1, activity: nil, presentation: Presentation.donnyWalls)) 14 | } 15 | 16 | func getSnapshot(in context: Context, completion: @escaping (SwiftLeedsWidgetEntry) -> ()) { 17 | let entry = SwiftLeedsWidgetEntry(date: Date(), slot: Schedule.Slot(id: UUID(), date: Date(), startTime: "11:00 AM", duration: 1, activity: nil, presentation: Presentation.donnyWalls)) 18 | completion(entry) 19 | } 20 | 21 | func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { 22 | var entries: [SwiftLeedsWidgetEntry] = [] 23 | var slots: [Schedule.Slot] = [] 24 | 25 | do { 26 | if let data = UserDefaults(suiteName: "group.uk.co.swiftleeds")?.data(forKey: "Slots") { 27 | slots = try PropertyListDecoder().decode([Schedule.Slot].self, from: data) 28 | } 29 | 30 | for slot in slots { 31 | let date = buildDate(for: slot) 32 | if date > Date() { 33 | let entry = SwiftLeedsWidgetEntry(date: date, slot: slot) 34 | entries.append(entry) 35 | } 36 | } 37 | 38 | let nextUpdateTime = Calendar.autoupdatingCurrent.date(byAdding: .hour, value: 1, to: Calendar.autoupdatingCurrent.startOfDay(for: Date()))! 39 | let timeline = Timeline(entries: entries, policy: .after(nextUpdateTime)) 40 | completion(timeline) 41 | } catch { 42 | let nextUpdateTime = Calendar.autoupdatingCurrent.date(byAdding: .minute, value: 5, to: Calendar.autoupdatingCurrent.startOfDay(for: Date()))! 43 | let timeline = Timeline(entries: entries, policy: .after(nextUpdateTime)) 44 | completion(timeline) 45 | } 46 | } 47 | 48 | private func buildDate(for slot: Schedule.Slot) -> Date { 49 | let slotTime = slot.startTime 50 | let slotTimeComponents = slotTime.components(separatedBy: ":") 51 | let slotHour = Int(slotTimeComponents.first ?? "0") 52 | let slotMinute = Int(slotTimeComponents.last ?? "0") 53 | 54 | var dateComponents = DateComponents() 55 | dateComponents.year = 2022 56 | dateComponents.month = 10 57 | dateComponents.day = 20 58 | dateComponents.timeZone = TimeZone.current 59 | dateComponents.hour = slotHour 60 | dateComponents.minute = slotMinute 61 | let userCalendar = Calendar(identifier: .gregorian) 62 | let dateTime = userCalendar.date(from: dateComponents) ?? Date() 63 | 64 | return dateTime 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SwiftLeedsWidget/WidgetSetup/WidgetConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WidgetConstants.swift 3 | // SwiftLeedsWidgetExtension 4 | // 5 | // Created by Karim Ebrahem on 26/09/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | enum WidgetConstants { 12 | static let timeFontSize: CGFloat = 10 13 | static let titleFontSize: CGFloat = 14 14 | static let detailsFontSize: CGFloat = 14 15 | static let logoImageWidth: CGFloat = 45 16 | static let logoImageHeight: CGFloat = 45 17 | } 18 | -------------------------------------------------------------------------------- /SwiftLeedsWidgetExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.uk.co.swiftleeds 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier("uk.co.swiftleeds.SwiftLeeds") 2 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:ios) 2 | 3 | platform :ios do 4 | desc "Build SwiftLeeds scheme" 5 | lane :build do |options| 6 | build_app( 7 | project: "SwiftLeeds.xcodeproj", 8 | configuration: options[:configuration], 9 | scheme: "SwiftLeeds", 10 | clean: true, 11 | skip_archive: true, 12 | skip_codesigning: true 13 | ) 14 | end 15 | 16 | desc "Build SwiftLeeds scheme with Debug Configuration" 17 | lane :build_debug do 18 | build(configuration: 'Debug') 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## iOS 17 | 18 | ### ios build 19 | 20 | ```sh 21 | [bundle exec] fastlane ios build 22 | ``` 23 | 24 | Build SwiftLeeds scheme 25 | 26 | ### ios build_debug 27 | 28 | ```sh 29 | [bundle exec] fastlane ios build_debug 30 | ``` 31 | 32 | Build SwiftLeeds scheme with Debug Configuration 33 | 34 | ---- 35 | 36 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 37 | 38 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 39 | 40 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 41 | -------------------------------------------------------------------------------- /media/swift-leeds-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwiftLeeds/swiftleeds-ios/3961b7a7ec88cdfdc34d520864ffad08ef1c1336/media/swift-leeds-logo.png --------------------------------------------------------------------------------