├── .gitignore ├── .swn ├── .swo ├── .swp ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── iOSDevUK.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── david.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── iOSDevUK ├── AdminSection │ ├── AdminView.swift │ ├── Domains │ │ ├── AdminLocationViewModel.swift │ │ ├── AdminSessionViewModel.swift │ │ ├── AdminSpeakerViewModel.swift │ │ ├── AdminSponsorViewModel.swift │ │ ├── AdminViewModel.swift │ │ └── LoginViewModel.swift │ ├── Locations │ │ ├── AddLocation.swift │ │ └── AdminLocations.swift │ ├── LoginView.swift │ ├── Sessions │ │ ├── AddSession.swift │ │ ├── AdminSessions.swift │ │ └── SelectSpeakerView.swift │ ├── Speakers │ │ ├── AddSpeakerView.swift │ │ └── AdminSpeakers.swift │ └── Sponsors │ │ ├── AddSponsorView.swift │ │ └── AdminSponsors.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── iOSDevUK_Icon_1024.png │ ├── Colors │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── NewColors │ │ │ ├── Contents.json │ │ │ ├── SegmentTitle.colorset │ │ │ │ └── Contents.json │ │ │ ├── buttonBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── buttonTitle.colorset │ │ │ │ └── Contents.json │ │ │ ├── cardBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── cardBottom.colorset │ │ │ │ └── Contents.json │ │ │ ├── cardTop.colorset │ │ │ │ └── Contents.json │ │ │ ├── iconColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── linkButton.colorset │ │ │ │ └── Contents.json │ │ │ ├── mainTextColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── mapPin.colorset │ │ │ │ └── Contents.json │ │ │ ├── outlineColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── pickerSelected.colorset │ │ │ │ └── Contents.json │ │ │ ├── purple200.colorset │ │ │ │ └── Contents.json │ │ │ ├── purple300.colorset │ │ │ │ └── Contents.json │ │ │ ├── textBody.colorset │ │ │ │ └── Contents.json │ │ │ └── textGrey.colorset │ │ │ │ └── Contents.json │ │ ├── backgroundColor.colorset │ │ │ └── Contents.json │ │ ├── goldColor.colorset │ │ │ └── Contents.json │ │ ├── platinumColor.colorset │ │ │ └── Contents.json │ │ ├── primary.colorset │ │ │ └── Contents.json │ │ ├── secondary.colorset │ │ │ └── Contents.json │ │ └── silverColor.colorset │ │ │ └── Contents.json │ ├── Contents.json │ └── Images │ │ ├── Back.imageset │ │ ├── Angle-left.pdf │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── EV.imageset │ │ ├── Contents.json │ │ └── petrolStations.pdf │ │ ├── Shops.imageset │ │ ├── Contents.json │ │ └── shopping.pdf │ │ ├── about.imageset │ │ ├── Contents.json │ │ └── Group 95-1.pdf │ │ ├── aboutBackground.imageset │ │ ├── Contents.json │ │ ├── NantYrArian.jpg │ │ └── Sunset.png │ │ ├── appIcon.imageset │ │ ├── Contents.json │ │ └── iOSDevUK_Icon_1024.png │ │ ├── attendee.imageset │ │ ├── Contents.json │ │ └── attendee.pdf │ │ ├── background.imageset │ │ ├── Contents.json │ │ └── background.png │ │ ├── calendar.imageset │ │ ├── Contents.json │ │ └── calendar.pdf │ │ ├── car.imageset │ │ ├── Contents.json │ │ └── parking.pdf │ │ ├── conferenceImage.imageset │ │ ├── Contents.json │ │ └── conferenceImage.jpg │ │ ├── devices.imageset │ │ ├── Contents.json │ │ └── devices.png │ │ ├── emptySchedule.imageset │ │ ├── Contents.json │ │ └── emotySchedule.pdf │ │ ├── help.imageset │ │ ├── Contents.json │ │ └── file.pdf │ │ ├── home.imageset │ │ ├── Contents.json │ │ └── home.pdf │ │ ├── house.imageset │ │ ├── Contents.json │ │ └── key.pdf │ │ ├── inclusivity.imageset │ │ ├── Contents.json │ │ └── Info icons.pdf │ │ ├── info.imageset │ │ ├── Contents.json │ │ └── info.pdf │ │ ├── link.imageset │ │ ├── Contents.json │ │ └── link.pdf │ │ ├── linkedin.imageset │ │ ├── Contents.json │ │ └── linkedin.pdf │ │ ├── mapImage.imageset │ │ ├── Contents.json │ │ └── mapImage.pdf │ │ ├── mapPin.imageset │ │ ├── Contents.json │ │ └── Property 1=Location.pdf │ │ ├── phone.imageset │ │ ├── Contents.json │ │ └── Group 95-2.pdf │ │ ├── placeholder.imageset │ │ ├── Contents.json │ │ └── placeholder.png │ │ ├── pubs.imageset │ │ ├── Contents.json │ │ └── pubs.pdf │ │ ├── schedule.imageset │ │ ├── Contents.json │ │ └── schedule.pdf │ │ ├── slack.imageset │ │ ├── Contents.json │ │ └── slack.pdf │ │ ├── sponsors.imageset │ │ ├── Contents.json │ │ └── Group 95.pdf │ │ ├── ticket.imageset │ │ ├── Contents.json │ │ └── ticket.pdf │ │ ├── transport.imageset │ │ ├── Contents.json │ │ └── transport.pdf │ │ ├── twitter.imageset │ │ ├── Contents.json │ │ └── twitter.pdf │ │ ├── university.imageset │ │ ├── Contents.json │ │ └── university.pdf │ │ └── you.imageset │ │ ├── Contents.json │ │ └── you.jpg ├── DataUploading │ ├── Networking.swift │ ├── locations.json │ ├── sessions.json │ ├── speakers.json │ └── sponsors.json ├── Domains │ ├── AllSessionsViewModel.swift │ ├── BaseViewModel.swift │ ├── MapViewModel.swift │ ├── MyScheduleViewModel.swift │ ├── SpeakerDetailViewModel.swift │ └── WeatherViewModel.swift ├── Extensions │ ├── Array + Extension.swift │ ├── Bundle + Extensions.swift │ ├── Date + Extensions.swift │ ├── Double + Extensions.swift │ └── String + Extensions.swift ├── FirebaseHelpers │ ├── FCollectionReference.swift │ ├── FirebaseAuthenticationService.swift │ ├── FirebaseFileManager.swift │ └── FirebaseRepository.swift ├── Info.plist ├── MOCK │ └── DummyData.swift ├── Model │ ├── AppErrors.swift │ ├── CoreData │ │ └── iOSDevUK.xcdatamodeld │ │ │ ├── .xccurrentversion │ │ │ └── iOSDevUK.xcdatamodel │ │ │ └── contents │ ├── EventInformation.swift │ ├── InformationItem.swift │ ├── Location.swift │ ├── ScheduleType.swift │ ├── Session.swift │ ├── Speaker.swift │ ├── Sponsor.swift │ └── WeatherData.swift ├── Modifiers │ ├── BackgroundModifier.swift │ ├── ButtonModifier.swift │ └── FontModifier.swift ├── Navigation │ └── NavigationRouter.swift ├── Presentation │ ├── AttendeeView │ │ ├── AttendeeRowView.swift │ │ └── AttendeeScreen.swift │ ├── HomeView │ │ ├── ContactButtonView.swift │ │ ├── EventInfoView.swift │ │ ├── HomeScreen.swift │ │ ├── SessionsHorizontalScrollView.swift │ │ └── SpeakersHorizontalScrollView.swift │ ├── InfoView │ │ ├── AboutView.swift │ │ ├── AppInformationView.swift │ │ ├── DropDownRowView.swift │ │ ├── InclusivityView.swift │ │ ├── InfoView.swift │ │ ├── LocationHeaderView.swift │ │ └── LocationsListView.swift │ ├── MyScheduleView │ │ └── MyScheduleView.swift │ ├── SecondaryViews │ │ ├── MapView │ │ │ └── MapScreen.swift │ │ ├── Sessions │ │ │ ├── AllSessionsView.swift │ │ │ └── SessionDetailView.swift │ │ ├── SpeakerView │ │ │ ├── AllSpeakersView.swift │ │ │ └── SpeakerDetailScreen.swift │ │ └── SponsorsView │ │ │ └── SponsorsScreen.swift │ └── ViewComponents │ │ ├── AnimatedButtonView.swift │ │ ├── AppSegmentControl.swift │ │ ├── DayPickerView.swift │ │ ├── EmptySessionView.swift │ │ ├── HourlyWeatherView.swift │ │ ├── InfoRowView.swift │ │ ├── LocationInfoCardView.swift │ │ ├── MapPinShape.swift │ │ ├── MapPinView.swift │ │ ├── NavigationRowView.swift │ │ ├── RemoteImage.swift │ │ ├── SectionHeaderView.swift │ │ ├── SessionCardView.swift │ │ ├── SessionRowView.swift │ │ ├── SpeakerCardView.swift │ │ ├── SpeakerRowView.swift │ │ ├── SponsorCard.swift │ │ ├── SponsorRow.swift │ │ ├── TabBarView.swift │ │ ├── UniversityMapView.swift │ │ └── WeatherView.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Resources │ └── Fonts │ │ ├── SFPRODISPLAYBLACKITALIC.OTF │ │ ├── SFPRODISPLAYBOLD.OTF │ │ ├── SFPRODISPLAYHEAVYITALIC.OTF │ │ ├── SFPRODISPLAYLIGHTITALIC.OTF │ │ ├── SFPRODISPLAYMEDIUM.OTF │ │ ├── SFPRODISPLAYREGULAR.OTF │ │ ├── SFPRODISPLAYSEMIBOLDITALIC.OTF │ │ ├── SFPRODISPLAYTHINITALIC.OTF │ │ ├── SFPRODISPLAYULTRALIGHTITALIC.OTF │ │ ├── SFProDisplay-Light.ttf │ │ └── SFProDisplay-Semibold.ttf ├── Utils │ ├── Constants.swift │ ├── LocalStorageService.swift │ ├── LocationService.swift │ ├── MappingUtils.swift │ └── Reachability.swift ├── iOSDevUK.entitlements └── iOSDevUKApp.swift └── iOSDevUKTests ├── AllSessionsViewModelTest.swift ├── FactorySetup.swift ├── SessionDetailViewModelTest.swift ├── SessionRowViewModelTest.swift └── SpeakerDetailViewModelTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | #Fastlane 6 | fastlane/ 7 | 8 | #Firebase 9 | GoogleService-Info*.plist 10 | 11 | ## User settings 12 | xcuserdata/ 13 | 14 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 15 | *.xcscmblueprint 16 | *.xccheckout 17 | 18 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 19 | build/ 20 | DerivedData/ 21 | *.moved-aside 22 | *.pbxuser 23 | !default.pbxuser 24 | *.mode1v3 25 | !default.mode1v3 26 | *.mode2v3 27 | !default.mode2v3 28 | *.perspectivev3 29 | !default.perspectivev3 30 | 31 | ## Obj-C/Swift specific 32 | *.hmap 33 | 34 | ## App packaging 35 | *.ipa 36 | *.dSYM.zip 37 | *.dSYM 38 | 39 | ## Playgrounds 40 | timeline.xctimeline 41 | playground.xcworkspace 42 | 43 | # Swift Package Manager 44 | # 45 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 46 | # Packages/ 47 | # Package.pins 48 | # Package.resolved 49 | # *.xcodeproj 50 | # 51 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 52 | # hence it is not needed unless you have added a package configuration file to your project 53 | # .swiftpm 54 | 55 | .build/ 56 | 57 | # CocoaPods 58 | # 59 | # We recommend against adding the Pods directory to your .gitignore. However 60 | # you should judge for yourself, the pros and cons are mentioned at: 61 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 62 | # 63 | Pods/ 64 | Podfile.lock 65 | # 66 | # Add this line if you want to avoid checking in source code from the Xcode workspace 67 | *.xcworkspace 68 | 69 | # Carthage 70 | # 71 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 72 | # Carthage/Checkouts 73 | 74 | Carthage/Build/ 75 | 76 | # Accio dependency management 77 | Dependencies/ 78 | .accio/ 79 | 80 | # fastlane 81 | # 82 | # It is recommended to not store the screenshots in the git repo. 83 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 84 | # For more information about the recommended setup visit: 85 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 86 | 87 | fastlane/report.xml 88 | fastlane/Preview.html 89 | fastlane/screenshots/**/*.png 90 | fastlane/test_output 91 | 92 | # Code Injection 93 | # 94 | # After new code Injection tools there's a generated folder /iOSInjectionProject 95 | # https://github.com/johnno1962/injectionforxcode 96 | 97 | iOSInjectionProject/ 98 | 99 | ### macOS ### 100 | # General 101 | .DS_Store 102 | .AppleDouble 103 | .LSOverride 104 | 105 | # Icon must end with two \r 106 | Icon 107 | 108 | 109 | # Thumbnails 110 | ._* 111 | 112 | # Files that might appear in the root of a volume 113 | .DocumentRevisions-V100 114 | .fseventsd 115 | .Spotlight-V100 116 | .TemporaryItems 117 | .Trashes 118 | .VolumeIcon.icns 119 | .com.apple.timemachine.donotpresent 120 | 121 | # Directories potentially created on remote AFP share 122 | .AppleDB 123 | .AppleDesktop 124 | Network Trash Folder 125 | Temporary Items 126 | .apdisk 127 | 128 | ### macOS Patch ### 129 | # iCloud generated files 130 | *.icloud 131 | 132 | # Generated 133 | *.generated.swift 134 | Derived 135 | *.xcodeproj 136 | *..xcworkspace 137 | .package.resolved 138 | 139 | 140 | # Caches 141 | **/*.xcodeproj/MockingbirdCache/ 142 | 143 | # Mockingbird 144 | MockingbirdMocks 145 | Pods/MockingbirdFramework/mockingbird/bin/ 146 | 147 | -------------------------------------------------------------------------------- /.swn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/.swn -------------------------------------------------------------------------------- /.swo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/.swo -------------------------------------------------------------------------------- /.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/.swp -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | 2 | 3 | source "https://rubygems.org" 4 | 5 | 6 | gem "fastlane" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 David Kababyan 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 | # iOSDevUK 2 | This is a redevelopment of the conference app for [iOSDevUK](https://www.iosdevuk.com), in its eleventh year (2023). 3 | The app uses [SwiftUI](https://developer.apple.com/xcode/swiftui/) and [Firebase](https://firebase.google.com/). 4 | 5 | ## Setup 6 | 7 | To start using the code, the following steps are required. 8 | 9 | ### Bundle Id 10 | 11 | The Bundle Id is set to one for the conference app for its release on the AppStore. 12 | Is using this code as a basis for a different conference app, change the Bundle ID for something that is associated with your developer account. 13 | 14 | ### Firebase 15 | 16 | The application uses Firebase for the data storage and the location of the image files. 17 | 18 | * **AppInformation** - Information for the application. The collection contains one item, which has the fields for the application. 19 | * **InformationItem** - A collection of links to further information, e.g. joining instructions. 20 | * **Location** - A collection of locations, associated with a location type (e.g. supermarkets, EV charging points). 21 | * **Session** - A collection of sessions, which includes talks, workshops and social events. 22 | * **Speaker** - A collection of infomration about the speakers for the conference. 23 | * **Sponsor** - A collection of information about the sponsors for the conference. 24 | 25 | The file `GoogleService-Info.plist` is required to be inserted into the project. 26 | This is generated by Firebase when a new data store is created. The Xcode project has a placeholder for the file, but no file is included in the repository. 27 | 28 | ## Dependencies 29 | 30 | The application uses the following 3rd party libraries: 31 | 32 | * [Firebase](https://firebase.google.com/) - for access to Firebase cloud facilities, including Firestore, Storage and Auth, used under the [Firebase Apache License](https://github.com/firebase/firebase-ios-sdk/blob/master/LICENSE). 33 | * [Factory](https://github.com/hmlongco/Factory) - used for depedency injection, used [Factory's MIT license](https://github.com/hmlongco/Factory/blob/main/LICENSE). 34 | * [Kingfisher](https://github.com/onevcat/Kingfisher) - used for downloading and caching images, used under [Kingfisher's MIT license](https://github.com/onevcat/Kingfisher/blob/master/LICENSE). 35 | 36 | ## License 37 | 38 | The app uses the MIT License, specified in the [License file](LICENSE). 39 | 40 | ## Developers 41 | 42 | The re-designed app has been created by David Kababyan ([@Dave_iOSDev](https://twitter.com/Dave_iOSDev)), with some contributions from Neil Taylor ([@Digidol](https://twitter.com/digidol)) and other contributors shown at [github.com/DavidFaraday/iOSDevUK](https://github.com/DavidFaraday/iOSDevUK). 43 | -------------------------------------------------------------------------------- /iOSDevUK.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iOSDevUK.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOSDevUK.xcodeproj/xcuserdata/david.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Promises (Playground) 1.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 3 13 | 14 | Promises (Playground) 2.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 4 20 | 21 | Promises (Playground) 3.xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 5 27 | 28 | Promises (Playground) 4.xcscheme 29 | 30 | isShown 31 | 32 | orderHint 33 | 6 34 | 35 | Promises (Playground) 5.xcscheme 36 | 37 | isShown 38 | 39 | orderHint 40 | 7 41 | 42 | Promises (Playground).xcscheme 43 | 44 | isShown 45 | 46 | orderHint 47 | 2 48 | 49 | iOSDevUK.xcscheme_^#shared#^_ 50 | 51 | orderHint 52 | 0 53 | 54 | 55 | SuppressBuildableAutocreation 56 | 57 | 63F7E0B528CC942100B2E1B7 58 | 59 | primary 60 | 61 | 62 | EF17827F2941F2D800EFFB2D 63 | 64 | primary 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/AdminView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdminView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AdminView: View { 11 | @EnvironmentObject var router: NavigationRouter 12 | 13 | @StateObject private var viewModel = AdminViewModel() 14 | 15 | @ViewBuilder 16 | private func navigationBarTrailingItem() -> some View { 17 | Button(AppStrings.logOut) { 18 | Task { 19 | await viewModel.logOutUser() 20 | router.infoPath.removeLast() 21 | } 22 | } 23 | } 24 | 25 | var body: some View { 26 | Form { 27 | NavigationLink(AppStrings.speakers, value: InfoDestination.adminSpeakers) 28 | NavigationLink(AppStrings.sessions, value: InfoDestination.adminSessions) 29 | NavigationLink(AppStrings.locations, value: InfoDestination.adminLocations) 30 | NavigationLink(AppStrings.sponsors, value: InfoDestination.adminSponsors) 31 | } 32 | .task { 33 | await viewModel.uploadNewData() 34 | } 35 | .navigationTitle(AppStrings.adminArea) 36 | .toolbar { 37 | ToolbarItem(placement: .navigationBarTrailing, content: navigationBarTrailingItem) 38 | } 39 | .alert(isPresented: $viewModel.showError, content: { 40 | Alert(title: Text(AppStrings.error), message: Text(viewModel.logoutError?.localizedDescription ?? ""), dismissButton: .default(Text(AppStrings.ok))) 41 | }) 42 | } 43 | } 44 | 45 | struct AdminView_Previews: PreviewProvider { 46 | static var previews: some View { 47 | AdminView() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Domains/AdminLocationViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdminLocationViewModel.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 05/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | final class AdminLocationViewModel: ObservableObject { 11 | private var firebaseRepository: FirebaseRepositoryProtocol 12 | 13 | @Published var name = "" 14 | @Published var note = "Notes" 15 | @Published var locationType = LocationType.au 16 | @Published var imageLink = "" 17 | @Published var latitude = "" 18 | @Published var longitude = "" 19 | 20 | var location: Location? 21 | 22 | init(location: Location? = nil, firebaseRepository: FirebaseRepositoryProtocol = FirebaseRepository.shared) { 23 | self.location = location 24 | self.firebaseRepository = firebaseRepository 25 | setupUI() 26 | } 27 | 28 | private func setupUI() { 29 | guard let location = location else { return } 30 | 31 | name = location.name 32 | note = location.note ?? "" 33 | imageLink = location.imageLink ?? "" 34 | latitude = "\(location.latitude)" 35 | longitude = "\(location.longitude)" 36 | locationType = location.locationType 37 | } 38 | 39 | func save() async { 40 | let newLocation = Location(id: location?.id ?? name.removeSpaces, name: name, note: note, imageLink: imageLink, latitude: Double(latitude) ?? 0.0, longitude: Double(longitude) ?? 0.0, webLink: nil, locationType: locationType) 41 | 42 | do { 43 | try firebaseRepository.saveData(data: newLocation, to: .Location) 44 | } catch { 45 | print("Error saving location", error.localizedDescription) 46 | } 47 | } 48 | 49 | func deleteLocation(_ location: Location) { 50 | firebaseRepository.deleteDocument(with: location.id, from: .Location) 51 | } 52 | 53 | func invalidForm() -> Bool { 54 | name == "" || latitude == "" || longitude == "" || note == "" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Domains/AdminSessionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdminSessionViewModel.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 05/03/2023. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | 12 | final class AdminSessionViewModel: ObservableObject { 13 | private let firebaseRepository: FirebaseRepositoryProtocol 14 | 15 | @Published var title = "" 16 | @Published var content = "Session content" 17 | @Published var type = SessionType.talk 18 | 19 | @Published var location: Location? 20 | @Published private var locationId = "" 21 | 22 | @Published var selectedSpeakers = Set() 23 | 24 | @Published private var speakerIds: [String] = [] 25 | 26 | @Published var startDate = Date() 27 | @Published var endDate = Date() 28 | 29 | var subscriptions = Set() 30 | 31 | var session: Session? 32 | 33 | init(session: Session? = nil, firebaseRepository: FirebaseRepositoryProtocol = FirebaseRepository.shared) { 34 | self.firebaseRepository = firebaseRepository 35 | self.session = session 36 | setupUI() 37 | observerData() 38 | } 39 | 40 | 41 | private func observerData() { 42 | $location 43 | .map({ $0?.id ?? "" }) 44 | .assign(to: &$locationId) 45 | 46 | $selectedSpeakers 47 | .map({ $0.map({ $0.id }) }) 48 | .assign(to: &$speakerIds) 49 | } 50 | 51 | 52 | private func setupUI() { 53 | guard let session = session else { return } 54 | 55 | title = session.title 56 | content = session.content 57 | type = session.type 58 | locationId = session.locationId ?? "" 59 | speakerIds = session.speakerIds 60 | startDate = session.startDate 61 | endDate = session.endDate 62 | } 63 | 64 | @MainActor 65 | func save(speakers: Set) async { 66 | // selectedSpeakers = speakers 67 | // 68 | // 69 | // guard !speakerIds.isEmpty else { return } 70 | 71 | let newSession = Session(id: session?.id ?? UUID().uuidString, title: title, content: content, startDate: startDate, endDate: endDate, locationId: locationId, speakerIds: speakerIds, type: type) 72 | 73 | // print(newSession) 74 | 75 | do { 76 | try firebaseRepository.saveData(data: newSession, to: .Session) 77 | } 78 | catch { 79 | print("Error saving session", error.localizedDescription) 80 | } 81 | } 82 | 83 | 84 | func deleteSession(_ session: Session) { 85 | firebaseRepository.deleteDocument(with: session.id, from: .Session) 86 | } 87 | 88 | func invalidForm() -> Bool { 89 | title == "" || content == "" || locationId == "" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Domains/AdminSpeakerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdminSpeakerViewModel.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 15/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | import PhotosUI 10 | 11 | final class AdminSpeakerViewModel: ObservableObject { 12 | private var firebaseRepository: FirebaseRepositoryProtocol 13 | 14 | @Published var fullName = "" 15 | @Published var currentPosition = "" 16 | @Published var twitter = "" 17 | @Published var linkedIn = "" 18 | @Published var imageLink = "" 19 | @Published var selectedItem: PhotosPickerItem? 20 | @Published var selectedImageData: Data? = nil 21 | @Published var bio = "Bio" 22 | 23 | @Published var webLinkName = "" 24 | @Published var url = "" 25 | 26 | var speaker: Speaker? 27 | 28 | init(speaker: Speaker? = nil, firebaseRepository: FirebaseRepositoryProtocol = FirebaseRepository.shared) { 29 | self.speaker = speaker 30 | self.firebaseRepository = firebaseRepository 31 | setupUI() 32 | } 33 | 34 | private func setupUI() { 35 | guard let speaker = speaker else { return } 36 | 37 | fullName = speaker.name 38 | currentPosition = speaker.currentPosition ?? "" 39 | twitter = speaker.twitterId ?? "" 40 | linkedIn = speaker.linkedIn ?? "" 41 | imageLink = speaker.imageLink 42 | bio = speaker.biography 43 | } 44 | 45 | func save() async { 46 | let newSpeaker = Speaker(id: speaker?.id ?? fullName.removeSpaces, name: fullName, currentPosition: currentPosition, biography: bio, linkedIn: linkedIn, twitterId: twitter, imageLink: imageLink, webLinks: nil) 47 | 48 | do { 49 | try firebaseRepository.saveData(data: newSpeaker, to: .Speaker) 50 | } 51 | catch { 52 | print("Error saving speaker", error.localizedDescription) 53 | } 54 | 55 | //With image uploading func Will add in new version 56 | // Task { 57 | // let imageLink = await uploadAvatar() 58 | // 59 | // let newSpeaker = Speaker(id: speaker?.id ?? UUID().uuidString, name: fullName, biography: bio, linkedIn: linkedIn, twitterId: twitter, imageLink: imageLink, webLinks: nil) 60 | // do { 61 | // try FirebaseReference(.Speaker).document(newSpeaker.id).setData(from: newSpeaker) 62 | // } 63 | // catch { 64 | // print("Error saving speaker", error.localizedDescription) 65 | // } 66 | // } 67 | } 68 | 69 | 70 | // func uploadAvatar() async -> String { 71 | // guard let imageData = selectedImageData else { return "" } 72 | // 73 | // do { 74 | // return try await FirebaseFileManager.shared.uploadImage(imageData, directory: .Speakers) 75 | // } catch { 76 | // print(error.localizedDescription) 77 | // return "" 78 | // } 79 | // } 80 | 81 | func deleteSpeaker(_ speaker: Speaker) { 82 | firebaseRepository.deleteDocument(with: speaker.id, from: .Speaker) 83 | } 84 | 85 | func invalidForm() -> Bool { 86 | fullName == "" || bio == "" || imageLink == "" 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Domains/AdminSponsorViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdminSponsorViewModel.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 05/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | final class AdminSponsorViewModel: ObservableObject { 11 | private var firebaseRepository: FirebaseRepositoryProtocol 12 | 13 | @Published var name = "" 14 | @Published var url = "" 15 | @Published var urlText = "" 16 | @Published var category = SponsorCategory.Silver 17 | @Published var imageLinkDark = "" 18 | @Published var imageLinkLight = "" 19 | @Published var tagline = "About the sponsor" 20 | 21 | var sponsor: Sponsor? 22 | 23 | init( 24 | sponsor: Sponsor? = nil, 25 | firebaseRepository: FirebaseRepositoryProtocol = FirebaseRepository.shared 26 | ) { 27 | self.sponsor = sponsor 28 | self.firebaseRepository = firebaseRepository 29 | setupUI() 30 | } 31 | 32 | private func setupUI() { 33 | guard let sponsor = sponsor else { return } 34 | 35 | name = sponsor.name 36 | url = sponsor.url 37 | urlText = sponsor.urlText 38 | imageLinkDark = sponsor.imageLinkDark ?? "" 39 | imageLinkLight = sponsor.imageLinkLight ?? "" 40 | tagline = sponsor.tagline 41 | category = sponsor.sponsorCategory 42 | } 43 | 44 | func save() async { 45 | let newSponsor = Sponsor(id: sponsor?.id ?? name.removeSpaces, name: name, tagline: tagline, url: url, urlText: urlText, sponsorCategory: category, imageLinkDark: imageLinkDark, imageLinkLight: imageLinkLight) 46 | 47 | do { 48 | try firebaseRepository.saveData(data: newSponsor, to: .Sponsor) 49 | } 50 | catch { 51 | print("Error saving sponsor", error.localizedDescription) 52 | } 53 | } 54 | 55 | func deleteSponsor(_ sponsor: Sponsor) { 56 | firebaseRepository.deleteDocument(with: sponsor.id, from: .Sponsor) 57 | } 58 | 59 | func invalidForm() -> Bool { 60 | name == "" || url == "" || urlText == "" || imageLinkDark == "" || imageLinkLight == "" || tagline == "" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Domains/AdminViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdminViewModel.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 26/10/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | final class AdminViewModel: ObservableObject { 11 | private let firebaseAuth: FirebaseAuthenticationServiceProtocol 12 | 13 | @Published var logoutError: Error? 14 | @Published var showError = false 15 | 16 | @MainActor 17 | func logOutUser() async { 18 | do { 19 | try await firebaseAuth.logOutUser() 20 | } catch (let error) { 21 | logoutError = error 22 | } 23 | 24 | showError = logoutError != nil 25 | } 26 | 27 | init(firebaseAuth: FirebaseAuthenticationServiceProtocol = FirebaseAuthenticationService.shared) { 28 | self.firebaseAuth = firebaseAuth 29 | } 30 | 31 | @MainActor 32 | func uploadNewData() async { 33 | // try? await FileUploadService.shared.uploadNewData(from: "sponsors.json", to: .Sponsor, objectType: Sponsor.self) 34 | // sleep(2) 35 | // try? await FileUploadService.shared.uploadNewData(from: "speakers.json", to: .Speaker, objectType: Speaker.self) 36 | // sleep(2) 37 | // try? await FileUploadService.shared.uploadNewData(from: "locations.json", to: .Location, objectType: Location.self) 38 | // sleep(2) 39 | // try? await FileUploadService.shared.uploadNewData(from: "sessions.json", to: .Session, objectType: CustomSession.self) 40 | 41 | print("DEBUG: Uploaded new data to firestore!") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Domains/LoginViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewModel.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 14/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | final class LoginViewModel: ObservableObject { 11 | private var firebaseAuth: FirebaseAuthenticationServiceProtocol 12 | 13 | @Published var loginSuccessful = false 14 | @Published var loginError: Error? 15 | @Published var showError = false 16 | 17 | init(firebaseAuth: FirebaseAuthenticationServiceProtocol = FirebaseAuthenticationService.shared) { 18 | self.firebaseAuth = firebaseAuth 19 | } 20 | 21 | @MainActor 22 | func loginUserWith(email: String, password: String) async { 23 | do { 24 | loginSuccessful = try await firebaseAuth.loginUserWith(email: email, password: password) 25 | } catch (let error) { 26 | loginError = error 27 | showError = true 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Locations/AddLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddLocation.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AddLocation: View { 11 | @Environment(\.dismiss) var dismiss 12 | @ObservedObject var viewModel: AdminLocationViewModel 13 | 14 | @ViewBuilder 15 | private func navigationBarTrailingItem() -> some View { 16 | Button { 17 | Task { 18 | await viewModel.save() 19 | } 20 | dismiss() 21 | } label: { 22 | Text(AppStrings.save) 23 | } 24 | .disabled(viewModel.invalidForm()) 25 | } 26 | 27 | 28 | @ViewBuilder 29 | private func main() -> some View { 30 | Form { 31 | Section { 32 | TextField("Name", text: $viewModel.name) 33 | TextField("Latitude", text: $viewModel.latitude) 34 | .keyboardType(.decimalPad) 35 | TextField("Latitude", text: $viewModel.longitude) 36 | .keyboardType(.decimalPad) 37 | 38 | Picker("Type", selection: $viewModel.locationType) { 39 | ForEach(LocationType.allCases, id: \.self) { location in 40 | Text(location.name) 41 | .tag(location) 42 | } 43 | } 44 | TextEditor(text: $viewModel.note) 45 | .frame(height: AppConstants.textViewHeight) 46 | 47 | } header: { 48 | Text(AppStrings.locationDetails) 49 | } 50 | } 51 | } 52 | 53 | var body: some View { 54 | main() 55 | .navigationTitle(viewModel.location?.name ?? AppStrings.addLocation) 56 | .toolbar { 57 | ToolbarItem(placement: .navigationBarTrailing, content: navigationBarTrailingItem) 58 | } 59 | } 60 | } 61 | 62 | struct AddLocation_Previews: PreviewProvider { 63 | static var previews: some View { 64 | AddLocation(viewModel: AdminLocationViewModel()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Locations/AdminLocations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdminLocations.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AdminLocations: View { 11 | @EnvironmentObject var viewModel: BaseViewModel 12 | @StateObject private var adminLocationViewModel = AdminLocationViewModel() 13 | 14 | var categories: [String : [Location]] { 15 | .init( 16 | grouping: viewModel.locations, 17 | by: { $0.locationType.rawValue } 18 | ) 19 | } 20 | 21 | @ViewBuilder 22 | private func navigationBarTrailingItem() -> some View { 23 | NavigationLink(value: InfoDestination.adminAddLocation(nil)) { 24 | Image(systemName: ImageNames.plus) 25 | .font(.title3) 26 | } 27 | } 28 | 29 | 30 | @ViewBuilder 31 | private func main() -> some View { 32 | Form { 33 | ForEach(categories.keys.sorted(), id: \String.self) { key in 34 | 35 | Section { 36 | ForEach(categories[key] ?? [], id: \.id) { location in 37 | 38 | NavigationLink(value: InfoDestination.adminAddLocation(location)) { 39 | Text(location.name) 40 | .font(.subheadline) 41 | .lineLimit(1) 42 | .minimumScaleFactor(0.6) 43 | } 44 | } 45 | .onDelete { indexSet in 46 | guard let index = indexSet.first, let locations = categories[key] else { return } 47 | adminLocationViewModel.deleteLocation(locations[index]) 48 | } 49 | } header: { 50 | let locationType: LocationType = LocationType(rawValue: key) ?? .au 51 | SectionHeaderView(title: locationType.name) 52 | .font(.headline) 53 | } 54 | } 55 | } 56 | } 57 | 58 | var body: some View { 59 | main() 60 | .navigationTitle(AppStrings.locations) 61 | .navigationBarTitleDisplayMode(.inline) 62 | .toolbar { 63 | ToolbarItem(placement: .navigationBarTrailing, content: navigationBarTrailingItem) 64 | } 65 | } 66 | } 67 | 68 | struct AdminLocations_Previews: PreviewProvider { 69 | static var previews: some View { 70 | AdminLocations() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/LoginView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 13/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LoginView: View { 11 | @Environment(\.dismiss) var dismiss 12 | 13 | @StateObject var viewModel = LoginViewModel() 14 | 15 | @State private var email = "" 16 | @State private var password = "" 17 | 18 | @ViewBuilder 19 | private func dismissButton() -> some View { 20 | ZStack(alignment: .topTrailing) { 21 | HStack { 22 | Spacer() 23 | Button("Close") { 24 | dismiss() 25 | } 26 | .padding() 27 | } 28 | } 29 | } 30 | 31 | @ViewBuilder 32 | private func loginButton() -> some View { 33 | Button { 34 | Task { 35 | await viewModel.loginUserWith(email: email, password: password) 36 | } 37 | 38 | } label: { 39 | Text(AppStrings.login) 40 | .foregroundStyle(Color(.darkText)) 41 | .fontWeight(.semibold) 42 | .frame(maxWidth: .infinity) 43 | } 44 | .capsuleBackgroundView(color: Color(.buttonBackground)) 45 | .padding(.top, 10) 46 | .onChange(of: viewModel.loginSuccessful, perform: { newValue in 47 | if newValue { 48 | dismiss() 49 | } 50 | }) 51 | } 52 | 53 | @ViewBuilder 54 | private func loginComponent() -> some View { 55 | VStack(spacing: 10) { 56 | Text(AppStrings.login) 57 | .semiboldAppFont(size: 20) 58 | .foregroundStyle(Color(.mainText)) 59 | 60 | HStack { 61 | Image(systemName: ImageNames.envelope) 62 | .foregroundColor(.accentColor) 63 | .frame(width: 30) 64 | TextField(AppStrings.email, text: $email) 65 | .keyboardType(.emailAddress) 66 | .textFieldStyle(.roundedBorder) 67 | } 68 | 69 | HStack { 70 | Image(systemName: ImageNames.lock) 71 | .foregroundColor(.accentColor) 72 | .frame(width: 30) 73 | SecureField(AppStrings.password, text: $password) 74 | .textFieldStyle(.roundedBorder) 75 | } 76 | 77 | loginButton() 78 | } 79 | .padding() 80 | .background(.ultraThinMaterial) 81 | .overlay( 82 | RoundedRectangle(cornerRadius: 16) 83 | .stroke(Color.accentColor, lineWidth: 2) 84 | ) 85 | .padding() 86 | } 87 | 88 | var body: some View { 89 | VStack { 90 | dismissButton() 91 | loginComponent() 92 | Spacer() 93 | Spacer() 94 | } 95 | .alert(isPresented: $viewModel.showError, content: { 96 | Alert(title: Text(AppStrings.error), message: Text(viewModel.loginError?.localizedDescription ?? ""), dismissButton: .default(Text(AppStrings.ok))) 97 | }) 98 | } 99 | } 100 | 101 | struct LoginView_Previews: PreviewProvider { 102 | static var previews: some View { 103 | LoginView() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Sessions/AdminSessions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdminSessions.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AdminSessions: View { 11 | @EnvironmentObject var viewModel: BaseViewModel 12 | @StateObject private var adminSessionViewModel = AdminSessionViewModel() 13 | 14 | @ViewBuilder 15 | private func sessionRowView(session: Session) -> some View { 16 | VStack(alignment: .leading, spacing: 10) { 17 | Text(session.title) 18 | .appFont(size: 18) 19 | Text("\(session.duration)") 20 | .appFont(size: 14) 21 | } 22 | .foregroundStyle(Color(.mainText)) 23 | } 24 | 25 | @ViewBuilder 26 | private func navigationBarTrailingItem() -> some View { 27 | NavigationLink(value: InfoDestination.adminAddSession(nil)) { 28 | Image(systemName: ImageNames.plus) 29 | .font(.title3) 30 | } 31 | } 32 | 33 | @ViewBuilder 34 | private func main() -> some View { 35 | Form { 36 | ForEach(viewModel.sessions, id: \.id) { session in 37 | NavigationLink(value: InfoDestination.adminAddSession(session)) { 38 | sessionRowView(session: session) 39 | } 40 | } 41 | .onDelete { indexSet in 42 | guard let index = indexSet.first else { return } 43 | adminSessionViewModel.deleteSession(viewModel.sessions[index]) 44 | } 45 | } 46 | .listStyle(.plain) 47 | } 48 | 49 | 50 | var body: some View { 51 | main() 52 | .navigationTitle(AppStrings.sessions) 53 | .navigationBarTitleDisplayMode(.inline) 54 | .toolbar { 55 | ToolbarItem(placement: .navigationBarTrailing, content: navigationBarTrailingItem) 56 | } 57 | } 58 | } 59 | 60 | struct AdminSessions_Previews: PreviewProvider { 61 | static var previews: some View { 62 | AdminSessions() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Sessions/SelectSpeakerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectSpeakerView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 21/04/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SelectSpeakerView: View { 11 | 12 | @EnvironmentObject var viewModel: BaseViewModel 13 | @Environment(\.dismiss) var dismiss 14 | 15 | 16 | @Binding var selectedSpeakers: Set 17 | 18 | @ViewBuilder 19 | private func navigationBarTrailingItem() -> some View { 20 | Button { 21 | dismiss() 22 | } label: { 23 | Text(AppStrings.save) 24 | } 25 | } 26 | 27 | 28 | @ViewBuilder 29 | private func main() -> some View { 30 | Form { 31 | ForEach(viewModel.speakers) { speaker in 32 | Button(action: { toggleSelection(speaker) }) { 33 | HStack { 34 | Text(speaker.name) 35 | .font(.subheadline) 36 | Spacer() 37 | if selectedSpeakers.contains(where: { $0.id == speaker.id }) { 38 | Image(systemName: ImageNames.checkmark) 39 | .font(.system(size: 13, weight: .bold)) 40 | .foregroundColor(Color.accentColor) 41 | } 42 | } 43 | } 44 | .tag(speaker.id) 45 | } 46 | } 47 | } 48 | 49 | var body: some View { 50 | NavigationStack { 51 | main() 52 | .navigationTitle(AppStrings.selectSpeakers) 53 | .toolbar { 54 | ToolbarItem(placement: .navigationBarTrailing, content: navigationBarTrailingItem) 55 | } 56 | 57 | } 58 | } 59 | 60 | private func toggleSelection(_ speaker: Speaker) { 61 | if let existingIndex = selectedSpeakers.firstIndex(where: { $0.id == speaker.id }) { 62 | selectedSpeakers.remove(at: existingIndex) 63 | } else { 64 | selectedSpeakers.insert(speaker) 65 | } 66 | } 67 | 68 | } 69 | 70 | struct SelectSpeakerView_Previews: PreviewProvider { 71 | static var previews: some View { 72 | SelectSpeakerView(selectedSpeakers: .constant([])) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Speakers/AdminSpeakers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdminSpeakers.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AdminSpeakers: View { 11 | @EnvironmentObject var viewModel: BaseViewModel 12 | @StateObject private var adminSpeakerViewModel = AdminSpeakerViewModel() 13 | 14 | @ViewBuilder 15 | private func navigationBarTrailingItem() -> some View { 16 | NavigationLink(value: InfoDestination.adminAddSpeaker(nil)) { 17 | Image(systemName: ImageNames.plus) 18 | .font(.title3) 19 | } 20 | } 21 | 22 | @ViewBuilder 23 | private func main() -> some View { 24 | Form { 25 | ForEach(viewModel.speakers) { speaker in 26 | NavigationLink(value: InfoDestination.adminAddSpeaker(speaker)) { 27 | Text(speaker.name) 28 | .font(.subheadline) 29 | } 30 | } 31 | .onDelete { indexSet in 32 | guard let index = indexSet.first else { return } 33 | adminSpeakerViewModel.deleteSpeaker(viewModel.speakers[index]) 34 | } 35 | } 36 | 37 | } 38 | 39 | var body: some View { 40 | main() 41 | .navigationTitle(AppStrings.speakers) 42 | .navigationBarTitleDisplayMode(.inline) 43 | .toolbar { 44 | ToolbarItem(placement: .navigationBarTrailing, content: navigationBarTrailingItem) 45 | } 46 | } 47 | 48 | } 49 | 50 | struct AdminSpeakers_Previews: PreviewProvider { 51 | static var previews: some View { 52 | AdminSpeakers() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Sponsors/AddSponsorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddSponsorView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AddSponsorView: View { 11 | @Environment(\.dismiss) var dismiss 12 | @ObservedObject var viewModel: AdminSponsorViewModel 13 | 14 | @ViewBuilder 15 | private func navigationBarTrailingItem() -> some View { 16 | Button { 17 | Task { 18 | await viewModel.save() 19 | } 20 | dismiss() 21 | } label: { 22 | Text(AppStrings.save) 23 | } 24 | .disabled(viewModel.invalidForm()) 25 | } 26 | 27 | @ViewBuilder 28 | private func main() -> some View { 29 | Form { 30 | Section { 31 | TextField("Name", text: $viewModel.name) 32 | TextField("Url in https:// format", text: $viewModel.url) 33 | TextField("Link Text", text: $viewModel.urlText) 34 | 35 | Picker("Category", selection: $viewModel.category) { 36 | ForEach(SponsorCategory.allCases, id: \.self) { category in 37 | Text(category.rawValue) 38 | .tag(category) 39 | } 40 | } 41 | 42 | HStack { 43 | TextField("Image link dark", text: $viewModel.imageLinkDark) 44 | Spacer() 45 | if !viewModel.imageLinkDark.isEmpty { 46 | RemoteImageView(url: URL(string: viewModel.imageLinkDark)) 47 | .scaledToFit() 48 | .clipShape(RoundedRectangle(cornerRadius: 8)) 49 | .frame(width: 50) 50 | } 51 | } 52 | 53 | HStack { 54 | TextField("Image link light", text: $viewModel.imageLinkLight) 55 | Spacer() 56 | if !viewModel.imageLinkLight.isEmpty { 57 | RemoteImageView(url: URL(string: viewModel.imageLinkLight)) 58 | .scaledToFit() 59 | .clipShape(RoundedRectangle(cornerRadius: 8)) 60 | .frame(width: 50) 61 | } 62 | } 63 | 64 | TextEditor(text: $viewModel.tagline) 65 | .frame(height: AppConstants.textViewHeight) 66 | } header: { 67 | Text(AppStrings.sponsorDetails) 68 | } 69 | } 70 | } 71 | 72 | 73 | var body: some View { 74 | main() 75 | .navigationTitle(viewModel.sponsor?.name ?? AppStrings.addSponsor) 76 | .toolbar { 77 | ToolbarItem(placement: .navigationBarTrailing, content: navigationBarTrailingItem) 78 | } 79 | } 80 | } 81 | 82 | struct AddSponsor_Previews: PreviewProvider { 83 | static var previews: some View { 84 | AddSponsorView(viewModel: AdminSponsorViewModel()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /iOSDevUK/AdminSection/Sponsors/AdminSponsors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdminSponsors.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AdminSponsors: View { 11 | @EnvironmentObject var viewModel: BaseViewModel 12 | @StateObject private var adminSponsorViewModel = AdminSponsorViewModel() 13 | 14 | @ViewBuilder 15 | private func navigationBarTrailingItem() -> some View { 16 | NavigationLink(value: InfoDestination.adminAddSponsor(nil)) { 17 | Image(systemName: ImageNames.plus) 18 | .font(.title3) 19 | } 20 | } 21 | 22 | 23 | @ViewBuilder 24 | private func main() -> some View { 25 | Form { 26 | ForEach(viewModel.sponsors, id: \.id) { sponsor in 27 | NavigationLink(value: InfoDestination.adminAddSponsor(sponsor)) { 28 | 29 | HStack(spacing: 5) { 30 | ZStack { 31 | Circle() 32 | .frame(width: 20) 33 | .foregroundColor(sponsor.sponsorCategory.color) 34 | Text(sponsor.sponsorCategory.rawValue.prefix(1)) 35 | .fontWeight(.bold) 36 | .foregroundColor(.black) 37 | .font(.system(size: 13)) 38 | } 39 | 40 | Text(sponsor.name) 41 | .font(.subheadline) 42 | } 43 | } 44 | } 45 | .onDelete { indexSet in 46 | guard let index = indexSet.first else { return } 47 | adminSponsorViewModel.deleteSponsor(viewModel.sponsors[index]) 48 | } 49 | } 50 | .listStyle(.plain) 51 | } 52 | 53 | var body: some View { 54 | main() 55 | .navigationTitle(AppStrings.sponsors) 56 | .navigationBarTitleDisplayMode(.inline) 57 | .toolbar { 58 | ToolbarItem(placement: .navigationBarTrailing, content: navigationBarTrailingItem) 59 | } 60 | } 61 | } 62 | 63 | struct AdminSponsors_Previews: PreviewProvider { 64 | static var previews: some View { 65 | AdminSponsors() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "iOSDevUK_Icon_1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/AppIcon.appiconset/iOSDevUK_Icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/AppIcon.appiconset/iOSDevUK_Icon_1024.png -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEC", 9 | "green" : "0x85", 10 | "red" : "0x83" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xFF", 27 | "green" : "0xFA", 28 | "red" : "0xFA" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/SegmentTitle.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE9", 9 | "green" : "0xA1", 10 | "red" : "0x9F" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x9D", 27 | "green" : "0x97", 28 | "red" : "0x98" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/buttonBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEC", 9 | "green" : "0x85", 10 | "red" : "0x83" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xE9", 27 | "green" : "0xA1", 28 | "red" : "0x9F" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/buttonTitle.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFA", 10 | "red" : "0xFA" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x15", 27 | "green" : "0x15", 28 | "red" : "0x15" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/cardBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF8", 9 | "green" : "0xF8", 10 | "red" : "0xF8" 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" : "0x28", 27 | "green" : "0x22", 28 | "red" : "0x23" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/cardBottom.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x3A", 9 | "green" : "0x4C", 10 | "red" : "0xBB" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/cardTop.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x3E", 9 | "green" : "0x53", 10 | "red" : "0xD4" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/iconColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEC", 9 | "green" : "0x85", 10 | "red" : "0x83" 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 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/linkButton.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF8", 9 | "green" : "0xEB", 10 | "red" : "0xEB" 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" : "0x3B", 27 | "green" : "0x2F", 28 | "red" : "0x30" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/mainTextColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x15", 9 | "green" : "0x15", 10 | "red" : "0x15" 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" : "0xFA", 28 | "red" : "0xFA" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/mapPin.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFA", 10 | "red" : "0xFA" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/outlineColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEA", 9 | "green" : "0xE9", 10 | "red" : "0xE9" 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" : "0x28", 27 | "green" : "0x22", 28 | "red" : "0x23" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/pickerSelected.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x3B", 9 | "green" : "0x2F", 10 | "red" : "0x30" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xFF", 27 | "green" : "0xFA", 28 | "red" : "0xFA" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/purple200.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE9", 9 | "green" : "0xA1", 10 | "red" : "0x9F" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/purple300.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEC", 9 | "green" : "0x85", 10 | "red" : "0x83" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/textBody.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x3B", 9 | "green" : "0x2F", 10 | "red" : "0x30" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x9D", 27 | "green" : "0x97", 28 | "red" : "0x98" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/NewColors/textGrey.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x9D", 9 | "green" : "0x97", 10 | "red" : "0x98" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/backgroundColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF6", 9 | "green" : "0xF1", 10 | "red" : "0xF2" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x00", 27 | "green" : "0x00", 28 | "red" : "0x00" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/goldColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.843", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/platinumColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.886", 9 | "green" : "0.894", 10 | "red" : "0.898" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/primary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.567", 9 | "green" : "0.329", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | }, 20 | "properties" : { 21 | "localizable" : true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/secondary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.290", 9 | "green" : "0.631", 10 | "red" : "0.761" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | }, 20 | "properties" : { 21 | "localizable" : true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Colors/silverColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.796", 9 | "green" : "0.761", 10 | "red" : "0.745" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/Back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Angle-left.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/EV.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "petrolStations.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/Shops.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "shopping.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/about.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 95-1.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/about.imageset/Group 95-1.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 2.500000 0.000000 cm 14 | 0.979167 0.979730 1.000000 scn 15 | 10.833333 22.000000 m 16 | 4.166667 22.000000 l 17 | 1.869167 22.000000 0.000000 19.943916 0.000000 17.416666 c 18 | 0.000000 4.583332 l 19 | 0.000000 2.056084 1.869167 0.000000 4.166667 0.000000 c 20 | 10.833333 0.000000 l 21 | 13.130833 0.000000 15.000000 2.056084 15.000000 4.583332 c 22 | 15.000000 17.416666 l 23 | 15.000000 19.943916 13.130833 22.000000 10.833333 22.000000 c 24 | h 25 | 13.333333 4.583332 m 26 | 13.333333 3.067165 12.211666 1.833332 10.833333 1.833332 c 27 | 4.166667 1.833332 l 28 | 2.788333 1.833332 1.666667 3.067165 1.666667 4.583332 c 29 | 1.666667 17.416666 l 30 | 1.666667 18.932833 2.788333 20.166666 4.166667 20.166666 c 31 | 10.833333 20.166666 l 32 | 12.211666 20.166666 13.333333 18.932833 13.333333 17.416666 c 33 | 13.333333 4.583332 l 34 | h 35 | 11.666666 16.500000 m 36 | 11.666666 15.993083 11.294167 15.583333 10.833333 15.583333 c 37 | 4.166667 15.583333 l 38 | 3.705833 15.583333 3.333333 15.993083 3.333333 16.500000 c 39 | 3.333333 17.006916 3.705833 17.416666 4.166667 17.416666 c 40 | 10.833333 17.416666 l 41 | 11.294167 17.416666 11.666666 17.006916 11.666666 16.500000 c 42 | h 43 | 11.666666 11.916666 m 44 | 11.666666 11.409750 11.294167 11.000000 10.833333 11.000000 c 45 | 4.166667 11.000000 l 46 | 3.705833 11.000000 3.333333 11.409750 3.333333 11.916666 c 47 | 3.333333 12.423583 3.705833 12.833333 4.166667 12.833333 c 48 | 10.833333 12.833333 l 49 | 11.294167 12.833333 11.666666 12.423583 11.666666 11.916666 c 50 | h 51 | 8.333333 7.333333 m 52 | 8.333333 6.826416 7.960833 6.416666 7.500000 6.416666 c 53 | 4.166667 6.416666 l 54 | 3.705833 6.416666 3.333333 6.826416 3.333333 7.333333 c 55 | 3.333333 7.840250 3.705833 8.250000 4.166667 8.250000 c 56 | 7.500000 8.250000 l 57 | 7.960833 8.250000 8.333333 7.840250 8.333333 7.333333 c 58 | h 59 | f 60 | n 61 | Q 62 | 63 | endstream 64 | endobj 65 | 66 | 3 0 obj 67 | 1716 68 | endobj 69 | 70 | 4 0 obj 71 | << /Annots [] 72 | /Type /Page 73 | /MediaBox [ 0.000000 0.000000 20.000000 22.000000 ] 74 | /Resources 1 0 R 75 | /Contents 2 0 R 76 | /Parent 5 0 R 77 | >> 78 | endobj 79 | 80 | 5 0 obj 81 | << /Kids [ 4 0 R ] 82 | /Count 1 83 | /Type /Pages 84 | >> 85 | endobj 86 | 87 | 6 0 obj 88 | << /Pages 5 0 R 89 | /Type /Catalog 90 | >> 91 | endobj 92 | 93 | xref 94 | 0 7 95 | 0000000000 65535 f 96 | 0000000010 00000 n 97 | 0000000034 00000 n 98 | 0000001806 00000 n 99 | 0000001829 00000 n 100 | 0000002002 00000 n 101 | 0000002076 00000 n 102 | trailer 103 | << /ID [ (some) (id) ] 104 | /Root 6 0 R 105 | /Size 7 106 | >> 107 | startxref 108 | 2135 109 | %%EOF -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/aboutBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "NantYrArian.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "Sunset.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "universal", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "dark" 42 | } 43 | ], 44 | "idiom" : "universal", 45 | "scale" : "3x" 46 | } 47 | ], 48 | "info" : { 49 | "author" : "xcode", 50 | "version" : 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/aboutBackground.imageset/NantYrArian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/Images/aboutBackground.imageset/NantYrArian.jpg -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/aboutBackground.imageset/Sunset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/Images/aboutBackground.imageset/Sunset.png -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/appIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "iOSDevUK_Icon_1024.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/appIcon.imageset/iOSDevUK_Icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/Images/appIcon.imageset/iOSDevUK_Icon_1024.png -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/attendee.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "attendee.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/background.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/Images/background.imageset/background.png -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/calendar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "calendar.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/car.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "parking.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/conferenceImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "conferenceImage.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/conferenceImage.imageset/conferenceImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/Images/conferenceImage.imageset/conferenceImage.jpg -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/devices.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "devices.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/devices.imageset/devices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/Images/devices.imageset/devices.png -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/emptySchedule.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "emotySchedule.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/help.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "file.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/home.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "home.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/house.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "key.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/inclusivity.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Info icons.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/info.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "info.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/link.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "link.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/linkedin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "linkedin.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/linkedin.imageset/linkedin.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.000000 -0.013916 cm 14 | 0.623529 0.631373 0.913725 scn 15 | 11.527376 2.015236 m 16 | 9.522814 2.015236 l 17 | 9.522814 5.154486 l 18 | 9.522814 5.903080 9.509439 6.866736 8.480220 6.866736 c 19 | 7.436157 6.866736 7.276438 6.051110 7.276438 5.208985 c 20 | 7.276438 2.015454 l 21 | 5.271875 2.015454 l 22 | 5.271875 8.471018 l 23 | 7.196282 8.471018 l 24 | 7.196282 7.588799 l 25 | 7.223220 7.588799 l 26 | 7.615626 8.259705 8.345220 8.660423 9.121939 8.631611 c 27 | 11.153658 8.631611 11.528282 7.295236 11.528282 5.556673 c 28 | 11.527376 2.015236 l 29 | h 30 | 3.010000 9.353455 m 31 | 2.367532 9.353331 1.846625 9.874081 1.846500 10.516518 c 32 | 1.846375 11.158987 2.367125 11.679893 3.009563 11.680017 c 33 | 3.652032 11.680142 4.172938 11.159392 4.173063 10.516954 c 34 | 4.173188 9.874486 3.652469 9.353549 3.010000 9.353455 c 35 | h 36 | 4.012282 2.015204 m 37 | 2.005625 2.015204 l 38 | 2.005625 8.471018 l 39 | 4.012282 8.471018 l 40 | 4.012282 2.015204 l 41 | h 42 | 12.526752 13.541549 m 43 | 0.998313 13.541549 l 44 | 0.453469 13.547706 0.006656 13.111268 0.000000 12.566424 c 45 | 0.000000 0.990172 l 46 | 0.006406 0.445047 0.453188 0.008172 0.998313 0.013985 c 47 | 12.526752 0.013985 l 48 | 13.072970 0.007141 13.521470 0.443985 13.529033 0.990172 c 49 | 13.529033 12.567237 l 50 | 13.521251 13.113174 13.072689 13.549581 12.526752 13.542393 c 51 | f 52 | n 53 | Q 54 | 55 | endstream 56 | endobj 57 | 58 | 3 0 obj 59 | 1254 60 | endobj 61 | 62 | 4 0 obj 63 | << /Annots [] 64 | /Type /Page 65 | /MediaBox [ 0.000000 0.000000 13.528809 13.528564 ] 66 | /Resources 1 0 R 67 | /Contents 2 0 R 68 | /Parent 5 0 R 69 | >> 70 | endobj 71 | 72 | 5 0 obj 73 | << /Kids [ 4 0 R ] 74 | /Count 1 75 | /Type /Pages 76 | >> 77 | endobj 78 | 79 | 6 0 obj 80 | << /Pages 5 0 R 81 | /Type /Catalog 82 | >> 83 | endobj 84 | 85 | xref 86 | 0 7 87 | 0000000000 65535 f 88 | 0000000010 00000 n 89 | 0000000034 00000 n 90 | 0000001344 00000 n 91 | 0000001367 00000 n 92 | 0000001540 00000 n 93 | 0000001614 00000 n 94 | trailer 95 | << /ID [ (some) (id) ] 96 | /Root 6 0 R 97 | /Size 7 98 | >> 99 | startxref 100 | 1673 101 | %%EOF -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/mapImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mapImage.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/mapImage.imageset/mapImage.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/Images/mapImage.imageset/mapImage.pdf -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/mapPin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Property 1=Location.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/mapPin.imageset/Property 1=Location.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/Images/mapPin.imageset/Property 1=Location.pdf -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/phone.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 95-2.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/phone.imageset/Group 95-2.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 3.333496 0.000732 cm 14 | 0.979167 0.979730 1.000000 scn 15 | 9.204166 21.999313 m 16 | 9.181666 22.000229 4.151667 22.000229 4.129167 21.999313 c 17 | 1.849167 21.977312 0.000000 19.929480 0.000000 17.416895 c 18 | 0.000000 4.583561 l 19 | 0.000000 2.056311 1.869167 0.000227 4.166667 0.000227 c 20 | 9.166666 0.000227 l 21 | 11.464167 0.000227 13.333333 2.056311 13.333333 4.583561 c 22 | 13.333333 17.415979 l 23 | 13.333333 19.929480 11.484166 21.976397 9.204166 21.999313 c 24 | h 25 | 11.666666 4.583561 m 26 | 11.666666 3.067394 10.545000 1.833561 9.166666 1.833561 c 27 | 4.166667 1.833561 l 28 | 2.788333 1.833561 1.666667 3.067394 1.666667 4.583561 c 29 | 1.666667 17.415979 l 30 | 1.666667 18.747896 2.531667 19.860729 3.675833 20.112812 c 31 | 4.254166 18.839561 l 32 | 4.395000 18.528812 4.684166 18.332645 5.000000 18.332645 c 33 | 8.333333 18.332645 l 34 | 8.649166 18.332645 8.937500 18.528812 9.079166 18.839561 c 35 | 9.657499 20.112812 l 36 | 10.801666 19.861645 11.666666 18.747896 11.666666 17.415979 c 37 | 11.666666 4.583561 l 38 | h 39 | 7.500000 3.666895 m 40 | 5.833333 3.666895 l 41 | 5.373333 3.666895 5.000000 4.077560 5.000000 4.583561 c 42 | 5.000000 5.089561 5.373333 5.500227 5.833333 5.500227 c 43 | 7.500000 5.500227 l 44 | 7.960000 5.500227 8.333333 5.089561 8.333333 4.583561 c 45 | 8.333333 4.077560 7.960000 3.666895 7.500000 3.666895 c 46 | h 47 | f 48 | n 49 | Q 50 | 51 | endstream 52 | endobj 53 | 54 | 3 0 obj 55 | 1291 56 | endobj 57 | 58 | 4 0 obj 59 | << /Annots [] 60 | /Type /Page 61 | /MediaBox [ 0.000000 0.000000 20.000000 22.000000 ] 62 | /Resources 1 0 R 63 | /Contents 2 0 R 64 | /Parent 5 0 R 65 | >> 66 | endobj 67 | 68 | 5 0 obj 69 | << /Kids [ 4 0 R ] 70 | /Count 1 71 | /Type /Pages 72 | >> 73 | endobj 74 | 75 | 6 0 obj 76 | << /Pages 5 0 R 77 | /Type /Catalog 78 | >> 79 | endobj 80 | 81 | xref 82 | 0 7 83 | 0000000000 65535 f 84 | 0000000010 00000 n 85 | 0000000034 00000 n 86 | 0000001381 00000 n 87 | 0000001404 00000 n 88 | 0000001577 00000 n 89 | 0000001651 00000 n 90 | trailer 91 | << /ID [ (some) (id) ] 92 | /Root 6 0 R 93 | /Size 7 94 | >> 95 | startxref 96 | 1710 97 | %%EOF -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "placeholder.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/placeholder.imageset/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/Images/placeholder.imageset/placeholder.png -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/pubs.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pubs.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/schedule.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "schedule.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/slack.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "slack.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/slack.imageset/slack.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/Images/slack.imageset/slack.pdf -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/sponsors.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 95.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/ticket.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ticket.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/transport.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "transport.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/twitter.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "twitter.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/twitter.imageset/twitter.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.150391 0.000000 cm 14 | 0.623529 0.631373 0.913725 scn 15 | 8.152745 8.071955 m 16 | 13.252491 14.000000 l 17 | 12.044014 14.000000 l 18 | 7.615905 8.852762 l 19 | 4.079186 14.000000 l 20 | 0.000000 14.000000 l 21 | 5.348213 6.216465 l 22 | 0.000000 0.000000 l 23 | 1.208546 0.000000 l 24 | 5.884745 5.435658 l 25 | 9.619779 0.000000 l 26 | 13.698964 0.000000 l 27 | 8.152448 8.071955 l 28 | 8.152745 8.071955 l 29 | h 30 | 6.497476 6.147890 m 31 | 5.955591 6.922954 l 32 | 1.644001 13.090229 l 33 | 3.500257 13.090229 l 34 | 6.979759 8.113041 l 35 | 7.521645 7.337976 l 36 | 12.044586 0.868400 l 37 | 10.188331 0.868400 l 38 | 6.497476 6.147593 l 39 | 6.497476 6.147890 l 40 | h 41 | f 42 | n 43 | Q 44 | 45 | endstream 46 | endobj 47 | 48 | 3 0 obj 49 | 620 50 | endobj 51 | 52 | 4 0 obj 53 | << /Annots [] 54 | /Type /Page 55 | /MediaBox [ 0.000000 0.000000 14.000000 14.000000 ] 56 | /Resources 1 0 R 57 | /Contents 2 0 R 58 | /Parent 5 0 R 59 | >> 60 | endobj 61 | 62 | 5 0 obj 63 | << /Kids [ 4 0 R ] 64 | /Count 1 65 | /Type /Pages 66 | >> 67 | endobj 68 | 69 | 6 0 obj 70 | << /Pages 5 0 R 71 | /Type /Catalog 72 | >> 73 | endobj 74 | 75 | xref 76 | 0 7 77 | 0000000000 65535 f 78 | 0000000010 00000 n 79 | 0000000034 00000 n 80 | 0000000710 00000 n 81 | 0000000732 00000 n 82 | 0000000905 00000 n 83 | 0000000979 00000 n 84 | trailer 85 | << /ID [ (some) (id) ] 86 | /Root 6 0 R 87 | /Size 7 88 | >> 89 | startxref 90 | 1038 91 | %%EOF -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/university.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "university.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/you.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "you.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOSDevUK/Assets.xcassets/Images/you.imageset/you.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Assets.xcassets/Images/you.imageset/you.jpg -------------------------------------------------------------------------------- /iOSDevUK/DataUploading/Networking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Networking.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 11/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | //used only for uploading sessions because of the date/time format 11 | struct CustomSession: Codable, Identifiable { 12 | let id: String 13 | let title: String 14 | let content: String 15 | let startDate: Date? 16 | let endDate: Date? 17 | let locationId: String? 18 | let speakerIds: [String] 19 | let type: SessionType 20 | 21 | 22 | init(from decoder: Decoder) throws { 23 | let container = try decoder.container(keyedBy: CodingKeys.self) 24 | 25 | self.id = try container.decode(String.self, forKey: .id) 26 | self.title = try container.decode(String.self, forKey: .title) 27 | self.content = try container.decode(String.self, forKey: .content) 28 | self.locationId = try? container.decode(String.self, forKey: .locationId) 29 | self.speakerIds = try container.decode([String].self, forKey: .speakerIds) 30 | self.type = try container.decode(SessionType.self, forKey: .type) 31 | 32 | let startDateString = try container.decode(String.self, forKey: .startDate) 33 | let endDateString = try container.decode(String.self, forKey: .endDate) 34 | 35 | let dateFormatter = DateFormatter() 36 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 37 | 38 | if let startDate = dateFormatter.date(from: startDateString) { 39 | self.startDate = startDate 40 | } else { 41 | throw DecodingError.dataCorruptedError(forKey: .startDate, 42 | in: container, 43 | debugDescription: "Invalid date format start") 44 | } 45 | 46 | if let endDate = dateFormatter.date(from: endDateString) { 47 | self.endDate = endDate 48 | } else { 49 | throw DecodingError.dataCorruptedError(forKey: .endDate, 50 | in: container, 51 | debugDescription: "Invalid date format end") 52 | } 53 | } 54 | } 55 | 56 | 57 | //MARK: - used only to get files from JSON and save to Firebase 58 | final class FileUploadService { 59 | static let shared = FileUploadService() 60 | let firebaseRepo = FirebaseRepository.shared 61 | 62 | private init() { } 63 | 64 | func uploadNewData(from fileName: String, to collection: FCollectionReference, objectType: T.Type) async throws { 65 | 66 | let objects = Bundle.main.decode([T].self, from: fileName) 67 | 68 | for object in objects { 69 | do { 70 | try firebaseRepo.saveData(data: object, to: collection) 71 | } catch { 72 | print(error.localizedDescription) 73 | throw AppError.unknownError 74 | } 75 | } 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /iOSDevUK/DataUploading/sponsors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "urlText" : "Bumble website", 4 | "id" : "bumble", 5 | "sponsorCategory" : "Platinum", 6 | "tagline" : "Bumble Inc. is the parent company of Bumble, Badoo, Fruitz and Official. Our mission is to create a world where all relationships are healthy and equitable, through Kind Connections.", 7 | "imageLinkDark" : "https://firebasestorage.googleapis.com/v0/b/iosdevuk-data.appspot.com/o/Sponsors%2Fbumble-logo.png?alt=media", 8 | "imageLinkLight" : "https://firebasestorage.googleapis.com/v0/b/iosdevuk-data.appspot.com/o/Sponsors%2Fbumble-logo.png?alt=media", 9 | "name" : "Bumble", 10 | "url" : "https://bumble.com/" 11 | }, 12 | { 13 | "urlText" : "Glassfy website", 14 | "id" : "glassfy", 15 | "sponsorCategory" : "Gold", 16 | "tagline" : "Scale your in-app subscriptions: Glassfy SDK provides you with in-app subscription infrastructure, real-time user subscription events and out-of-the-box revenue growth tools on iOS, Android and web.", 17 | "imageLinkDark" : "https://firebasestorage.googleapis.com/v0/b/iosdevuk-data.appspot.com/o/Sponsors%2Fglassfy-logo.png?alt=media", 18 | "imageLinkLight" : "https://firebasestorage.googleapis.com/v0/b/iosdevuk-data.appspot.com/o/Sponsors%2Fglassfy-logo.png?alt=media", 19 | "name" : "Glassfy", 20 | "url" : "https://glassfy.io/" 21 | }, 22 | { 23 | "urlText" : "Marathon Labs website", 24 | "id" : "marathonlabs", 25 | "sponsorCategory" : "Gold", 26 | "tagline" : "Revolutionize your app testing experience: run any number of tests in less than 15 minutes with ZERO flakiness.", 27 | "imageLinkDark" : "https://firebasestorage.googleapis.com/v0/b/iosdevuk-data.appspot.com/o/Sponsors%2Fmarathon-logo-dark-mode.png?alt=media", 28 | "imageLinkLight" : "https://firebasestorage.googleapis.com/v0/b/iosdevuk-data.appspot.com/o/Sponsors%2Fmarathon-logo-light-mode.png?alt=media", 29 | "name" : "Marathan Labs", 30 | "url" : "https://marathonlabs.io/" 31 | } 32 | ] -------------------------------------------------------------------------------- /iOSDevUK/Domains/AllSessionsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllSessionsViewModel.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 30/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | final class AllSessionsViewModel: ObservableObject { 12 | private let firebaseRepository: FirebaseRepositoryProtocol 13 | 14 | @Published private(set) var fetchError: Error? 15 | @Published private(set) var eventInformation: EventInformation? 16 | @Published var sessions: [Session] = [] 17 | @Published var selectedDate = "" 18 | 19 | private var cancellables: Set = [] 20 | 21 | 22 | init(firebaseRepository: FirebaseRepositoryProtocol = FirebaseRepository.shared) { 23 | self.firebaseRepository = firebaseRepository 24 | } 25 | 26 | @MainActor 27 | func setCurrentDate() { 28 | guard !sessions.isEmpty else { return } 29 | 30 | //applies date preselection only during the conference 31 | if let startDate = eventInformation?.startDate, let endDate = eventInformation?.endDate { 32 | if !Date().isInRange(startDate: startDate, endDate: endDate) { 33 | return 34 | } 35 | } 36 | 37 | let currentDayMonth = Calendar.current.dateComponents([.month, .day], from: Date()) 38 | 39 | for session in sessions { 40 | let sessionDayMont = Calendar.current.dateComponents([.month, .day], from: session.startDate) 41 | 42 | if currentDayMonth == sessionDayMont { 43 | selectedDate = session.startingDay 44 | return 45 | } 46 | } 47 | 48 | selectedDate = sessions.first?.startingDay ?? "" 49 | } 50 | 51 | @MainActor 52 | func fetchEventNotification() async { 53 | guard eventInformation == nil else { return } 54 | 55 | do { 56 | self.eventInformation = try await firebaseRepository.getDocuments(from: .AppInformation)?.first 57 | } catch (let error) { 58 | fetchError = error 59 | } 60 | } 61 | 62 | func setSessions(sessions: [Session]) { 63 | self.sessions = sessions 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /iOSDevUK/Domains/MapViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapViewModel.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 25/03/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import MapKit 11 | 12 | final class MapViewModel: ObservableObject { 13 | @Published var region: MKCoordinateRegion = MKCoordinateRegion(center: MapDetails.aberystwythCoordinates, 14 | span: MapDetails.defaultSpan) 15 | 16 | @Published var locationCategory: LocationType = .au 17 | @Published var allLocations: [Location] = [] 18 | 19 | init(allLocations: [Location]) { 20 | self.allLocations = allLocations 21 | } 22 | 23 | 24 | static func openInGoogleMaps(location: Location) { 25 | 26 | let url = URL(string: "comgooglemaps://?saddr=&daddr=\(location.latitude),\(location.longitude)&directionsmode=driving") 27 | 28 | if UIApplication.shared.canOpenURL(url!) { 29 | UIApplication.shared.open(url!, options: [:], completionHandler: nil) 30 | } else { 31 | let urlBrowser = URL(string: "https://www.google.co.in/maps/dir/??saddr=&daddr=\(location.latitude),\(location.longitude)&directionsmode=driving") 32 | 33 | UIApplication.shared.open(urlBrowser!, options: [:], completionHandler: nil) 34 | } 35 | } 36 | 37 | static func openInAppleMaps(location: Location) { 38 | 39 | let mapItem = MKMapItem(placemark: MKPlacemark(coordinate: location.coordinate, addressDictionary:nil)) 40 | mapItem.name = location.name 41 | mapItem.openInMaps(launchOptions: [MKLaunchOptionsDirectionsModeKey : MKLaunchOptionsDirectionsModeWalking]) 42 | } 43 | 44 | func updateRegion() { 45 | let regionCenter = filteredAnnotations().last?.coordinate ?? MapDetails.aberystwythCoordinates 46 | region = MKCoordinateRegion(center: regionCenter, span: MapDetails.defaultSpan) 47 | } 48 | 49 | func filteredAnnotations() -> [Location] { 50 | allLocations.count > 1 ? allLocations.filter { $0.locationType == locationCategory } : allLocations 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /iOSDevUK/Domains/MyScheduleViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyScheduleViewModel.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 10/09/2022. 6 | // 7 | import Combine 8 | import SwiftUI 9 | 10 | final class MyScheduleViewModel: ObservableObject { 11 | private let localStorage: LocalStorageServiceProtocol 12 | private let firebaseRepository: FirebaseRepositoryProtocol 13 | 14 | @Published private(set) var favoriteSessionIds: [String] = [] 15 | @Published private(set) var sessions: [Session] = [] 16 | @Published private(set) var groupedSessions: [String: [Session]] = [:] 17 | @Published private(set) var fetchError: Error? 18 | 19 | private var cancellables: Set = [] 20 | 21 | init( 22 | localStorage: LocalStorageServiceProtocol = LocalStorageService.shared, 23 | firebaseRepository: FirebaseRepositoryProtocol = FirebaseRepository.shared 24 | ) { 25 | self.localStorage = localStorage 26 | self.firebaseRepository = firebaseRepository 27 | self.observerData() 28 | } 29 | 30 | private func observerData() { 31 | $sessions 32 | .sink(receiveValue: { receivedSessions in 33 | self.updateGroupedSessions(sessions: receivedSessions) 34 | }) 35 | .store(in: &cancellables) 36 | } 37 | 38 | @MainActor 39 | func listenForSessions() async { 40 | guard self.sessions.isEmpty else { return } 41 | 42 | do { 43 | try await firebaseRepository.listen(from: .Session) 44 | .sink(receiveCompletion: { completion in 45 | switch completion { 46 | case .finished: 47 | return 48 | case .failure(let error): 49 | print("Error: \(error.localizedDescription)") 50 | } 51 | }, receiveValue: { [weak self] allSessions in 52 | self?.sessions = allSessions.sorted() 53 | }) 54 | .store(in: &cancellables) 55 | } catch (let error) { 56 | fetchError = error 57 | } 58 | } 59 | 60 | 61 | func setFavSessions(favSessionIds: [String]) { 62 | self.favoriteSessionIds = favSessionIds 63 | updateGroupedSessions(sessions: self.sessions) 64 | } 65 | 66 | private func updateGroupedSessions(sessions: [Session]) { 67 | let filteredSessions = sessions.filter { self.favoriteSessionIds.contains($0.id) } 68 | 69 | self.groupedSessions = .init( 70 | grouping: filteredSessions, 71 | by: { $0.startingDay } 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /iOSDevUK/Domains/SpeakerDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpeakerDetailViewModel.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | final class SpeakerDetailViewModel: ObservableObject { 12 | private let firebaseRepository: FirebaseRepositoryProtocol 13 | 14 | @Published private(set) var fetchError: Error? 15 | 16 | @Published private(set) var sessions:[Session] = [] 17 | @Published private(set) var speaker: Speaker 18 | @Published private(set) var webLinks: [Weblink] = [] 19 | 20 | @Published var showError = false 21 | private var cancellables: Set = [] 22 | 23 | init(speaker: Speaker, firebaseRepository: FirebaseRepositoryProtocol = FirebaseRepository.shared) { 24 | self.speaker = speaker 25 | self.firebaseRepository = firebaseRepository 26 | 27 | $fetchError 28 | .dropFirst() 29 | .sink { [weak self] _ in 30 | self?.showError = true 31 | } 32 | .store(in: &cancellables) 33 | 34 | $speaker 35 | .map( { $0.webLinks ?? [] }) 36 | .assign(to: &$webLinks) 37 | } 38 | 39 | 40 | 41 | @MainActor 42 | func getSpeakerSessions() async { 43 | do { 44 | sessions = try await firebaseRepository.getDocuments(from: .Session, where: FirebaseKeys.speakerIds, arrayContains: speaker.id) ?? [] 45 | } catch (let error) { 46 | fetchError = error 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /iOSDevUK/Domains/WeatherViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherViewModel.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 26/03/2023. 6 | // 7 | 8 | import Foundation 9 | import WeatherKit 10 | import CoreLocation 11 | 12 | final class WeatherViewModel: ObservableObject { 13 | private var mappingUtils: MappingUtilsProtocol 14 | 15 | @Published private var weather: Weather? 16 | @Published private var hourlyWeatherData: Forecast? 17 | @Published private(set) var currentWeather: WeatherData? 18 | @Published private(set) var hourlyWeather: [WeatherData] = [] 19 | 20 | let weatherService: WeatherService 21 | var location: CLLocation? 22 | 23 | init( 24 | weatherService: WeatherService, 25 | mappingUtils: MappingUtilsProtocol = MappingUtils.shared 26 | ) { 27 | self.weatherService = weatherService 28 | self.mappingUtils = mappingUtils 29 | observerData() 30 | } 31 | 32 | private func observerData() { 33 | $hourlyWeatherData 34 | .map( { $0?.map(self.mappingUtils.convert) ?? [] }) 35 | .assign(to: &$hourlyWeather) 36 | 37 | $weather 38 | .map( { self.mappingUtils.convert(input: $0?.currentWeather) }) 39 | .assign(to: &$currentWeather) 40 | } 41 | 42 | @MainActor 43 | func getWeather() async { 44 | guard let location = location else { return } 45 | guard weather == nil else { return } 46 | 47 | self.weather = try? await weatherService.weather(for: location) 48 | 49 | let query = WeatherQuery.hourly(startDate: Date(), endDate: Date().tomorrow) 50 | self.hourlyWeatherData = try? await weatherService.weather(for: location, including: query) 51 | } 52 | 53 | func setLocation(location: CLLocation?) { 54 | self.location = location 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /iOSDevUK/Extensions/Array + Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array + Extension.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 12/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array { 11 | func at(_ index: Index) -> Element? { 12 | guard indices.contains(index) else { 13 | return nil 14 | } 15 | return self[index] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /iOSDevUK/Extensions/Bundle + Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Extensions.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 13/09/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | public var appName: String { getInfo("CFBundleName") } 12 | public var displayName: String { getInfo("CFBundleDisplayName") } 13 | public var language: String { getInfo("CFBundleDevelopmentRegion") } 14 | public var identifier: String { getInfo("CFBundleIdentifier") } 15 | public var copyright: String { getInfo("NSHumanReadableCopyright").replacingOccurrences(of: "\\\\n", with: "\n") } 16 | 17 | public var appBuild: String { getInfo("CFBundleVersion") } 18 | public var appVersionLong: String { getInfo("CFBundleShortVersionString") } 19 | 20 | fileprivate func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" } 21 | } 22 | 23 | 24 | extension Bundle { 25 | 26 | func data(for filename: String) -> Data? { 27 | let bundle = Bundle(for: type(of: self)) 28 | guard let url = bundle.url(forResource: filename, withExtension: nil) else { return nil } 29 | 30 | return try? Data(contentsOf: url) 31 | } 32 | } 33 | 34 | extension Bundle { 35 | func decode(_ type: T.Type, from file: String, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) -> T { 36 | guard let url = self.url(forResource: file, withExtension: nil) else { 37 | fatalError("Failed to locate \(file) in bundle.") 38 | } 39 | 40 | guard let data = try? Data(contentsOf: url) else { 41 | fatalError("Failed to load \(file) from bundle.") 42 | } 43 | 44 | let decoder = JSONDecoder() 45 | decoder.dateDecodingStrategy = dateDecodingStrategy 46 | decoder.keyDecodingStrategy = keyDecodingStrategy 47 | 48 | do { 49 | return try decoder.decode(T.self, from: data) 50 | } catch DecodingError.keyNotFound(let key, let context) { 51 | fatalError("Failed to decode \(file) from bundle due to missing key '\(key.stringValue)' not found – \(context.debugDescription)") 52 | } catch DecodingError.typeMismatch(_, let context) { 53 | fatalError("Failed to decode \(file) from bundle due to type mismatch – \(context.debugDescription)") 54 | } catch DecodingError.valueNotFound(let type, let context) { 55 | fatalError("Failed to decode \(file) from bundle due to missing \(type) value – \(context.debugDescription)") 56 | } catch DecodingError.dataCorrupted(_) { 57 | fatalError("Failed to decode \(file) from bundle because it appears to be invalid JSON") 58 | } catch { 59 | fatalError("Failed to decode \(file) from bundle: \(error.localizedDescription)") 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /iOSDevUK/Extensions/Date + Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date + Extensions.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 29/09/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | //needed to save Date to AppStorage 11 | extension Date: RawRepresentable { 12 | private static let formatter = ISO8601DateFormatter() 13 | 14 | public var rawValue: String { 15 | Date.formatter.string(from: self) 16 | } 17 | 18 | public init?(rawValue: String) { 19 | self = Date.formatter.date(from: rawValue) ?? Date() 20 | } 21 | } 22 | 23 | extension Date { 24 | 25 | func date(with year: Int, month: Int, day: Int = 1) -> Date { 26 | Calendar.current.date(from: DateComponents(year: year, month: month, day: day)) ?? Date() 27 | } 28 | 29 | var tomorrow: Date { 30 | Calendar.current.date(byAdding: .day, value: 1, to: Date())! 31 | } 32 | 33 | var longDate: String { 34 | let dateFormatter = DateFormatter() 35 | dateFormatter.dateFormat = "dd MMM yyyy" 36 | return dateFormatter.string(from: self) 37 | } 38 | 39 | var weekDayTime: String { 40 | let dateFormatter = DateFormatter() 41 | dateFormatter.dateFormat = "EE HH:mm" 42 | return dateFormatter.string(from: self) 43 | } 44 | 45 | var dateAndWeekDay: String { 46 | let dateFormatter = DateFormatter() 47 | dateFormatter.dateFormat = "d EE" 48 | return dateFormatter.string(from: self) 49 | } 50 | 51 | var dayOfTheMonth: String { 52 | let dateFormatter = DateFormatter() 53 | dateFormatter.dateFormat = "d" 54 | return dateFormatter.string(from: self) 55 | } 56 | 57 | var year: String { 58 | let dateFormatter = DateFormatter() 59 | dateFormatter.dateFormat = "yyyy" 60 | return dateFormatter.string(from: self) 61 | } 62 | 63 | var dayAndMonth: String { 64 | let dateFormatter = DateFormatter() 65 | dateFormatter.dateFormat = "d MMMM" 66 | return dateFormatter.string(from: self) 67 | } 68 | 69 | var weekDay: String { 70 | let dateFormatter = DateFormatter() 71 | dateFormatter.dateFormat = "EE" 72 | return dateFormatter.string(from: self) 73 | } 74 | 75 | var time: String { 76 | let dateFormatter = DateFormatter() 77 | dateFormatter.dateFormat = "HH:mm" 78 | return dateFormatter.string(from: self) 79 | } 80 | 81 | func isInRange(startDate: Date, endDate: Date) -> Bool { 82 | self >= startDate && self <= endDate 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /iOSDevUK/Extensions/Double + Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double + Extensions.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 26/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Double { 11 | 12 | var toCelsius: String { 13 | let formatter = MeasurementFormatter() 14 | formatter.unitOptions = .providedUnit 15 | formatter.locale = Locale(identifier: "en_GB") 16 | 17 | return formatter.string(from: Measurement(value: self, unit: UnitTemperature.celsius)) 18 | } 19 | 20 | var toFahrenheit: String { 21 | let formatter = MeasurementFormatter() 22 | formatter.unitOptions = .providedUnit 23 | formatter.locale = Locale(identifier: "en_US") 24 | return formatter.string(from: Measurement(value: self, unit: UnitTemperature.fahrenheit)) 25 | } 26 | 27 | func roundNearest() -> Double { 28 | (self * 2).rounded() / 2 29 | } 30 | 31 | func roundDown() -> String { 32 | self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(self) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iOSDevUK/Extensions/String + Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String + Extensions.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 30/10/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | var removeDigits: String { 12 | components(separatedBy: CharacterSet.decimalDigits).joined() 13 | } 14 | var removeChars: String { 15 | components(separatedBy: CharacterSet.letters).joined() 16 | } 17 | 18 | var removeSpaces: String { 19 | self.replacingOccurrences(of: " ", with: "") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iOSDevUK/FirebaseHelpers/FCollectionReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FCollectionReference.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 10/09/2022. 6 | // 7 | 8 | import Firebase 9 | import FirebaseFirestoreSwift 10 | 11 | 12 | enum FCollectionReference: String { 13 | case Speaker 14 | case Session 15 | case Sponsor 16 | case Location 17 | case InformationItem 18 | case AppInformation 19 | case TestData //used only for testing purposes 20 | } 21 | 22 | 23 | func FirebaseReference(_ collectionReference: FCollectionReference) -> CollectionReference { 24 | return Firestore.firestore().collection(collectionReference.rawValue) 25 | } 26 | -------------------------------------------------------------------------------- /iOSDevUK/FirebaseHelpers/FirebaseAuthenticationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirebaseAuthentication.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 14/01/2023. 6 | // 7 | 8 | import Foundation 9 | import Firebase 10 | 11 | protocol FirebaseAuthenticationServiceProtocol { 12 | func hasCurrentUser() -> Bool 13 | func loginUserWith(email: String, password: String) async throws -> Bool 14 | func logOutUser() async throws 15 | } 16 | 17 | class FirebaseAuthenticationService: FirebaseAuthenticationServiceProtocol { 18 | 19 | static let shared = FirebaseAuthenticationService() 20 | 21 | private init() {} 22 | 23 | func hasCurrentUser() -> Bool { 24 | Auth.auth().currentUser != nil 25 | } 26 | 27 | func loginUserWith(email: String, password: String) async throws -> Bool { 28 | 29 | try await withCheckedThrowingContinuation { continuation in 30 | 31 | Auth.auth().signIn(withEmail: email, password: password) { authResult, error in 32 | if let error = error { 33 | continuation.resume(throwing: error) 34 | return 35 | } 36 | continuation.resume(returning: true) 37 | } 38 | } 39 | } 40 | 41 | func logOutUser() async throws { 42 | do { 43 | try Auth.auth().signOut() 44 | } catch { 45 | throw error 46 | } 47 | } 48 | } 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /iOSDevUK/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIAppFonts 6 | 7 | SFPRODISPLAYBLACKITALIC.OTF 8 | SFPRODISPLAYBOLD.OTF 9 | SFPRODISPLAYHEAVYITALIC.OTF 10 | SFPRODISPLAYLIGHTITALIC.OTF 11 | SFPRODISPLAYMEDIUM.OTF 12 | SFPRODISPLAYREGULAR.OTF 13 | SFPRODISPLAYSEMIBOLDITALIC.OTF 14 | SFPRODISPLAYTHINITALIC.OTF 15 | SFPRODISPLAYULTRALIGHTITALIC.OTF 16 | SFProDisplay-Semibold.ttf 17 | SFProDisplay-Light.ttf 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /iOSDevUK/Model/AppErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppErrors.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 27/11/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | enum AppError: Error { 11 | case badSnapshot 12 | case unknownError 13 | } 14 | 15 | extension AppError: LocalizedError { 16 | public var errorDescription: String? { 17 | switch self { 18 | case .badSnapshot: 19 | return NSLocalizedString("Please check your internet connection and try again.", comment: "App error") 20 | case .unknownError: 21 | return NSLocalizedString("Please try again later.", comment: "App error") 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /iOSDevUK/Model/CoreData/iOSDevUK.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /iOSDevUK/Model/CoreData/iOSDevUK.xcdatamodeld/iOSDevUK.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /iOSDevUK/Model/EventInformation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventInformation.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 25/11/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct EventInformation: Codable { 11 | let about: String 12 | let notification: String 13 | let startDate: Date 14 | let endDate: Date 15 | let inclusivityText: String 16 | } 17 | -------------------------------------------------------------------------------- /iOSDevUK/Model/InformationItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InformationItem.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 06/10/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct InformationItem: Codable, Identifiable, Comparable { 11 | 12 | let id: String 13 | let name: String 14 | let subtitle: String 15 | let link: String 16 | let imageName: String? 17 | 18 | var url: URL? { 19 | URL(string: link) 20 | } 21 | 22 | static func < (lhs: InformationItem, rhs: InformationItem) -> Bool { 23 | lhs.name < rhs.name 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /iOSDevUK/Model/Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Location.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 10/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | enum LocationType: String, Codable, CaseIterable { 12 | case au, transport, ev, pubs, sm 13 | 14 | var name: String { 15 | switch self { 16 | case .au: 17 | return "Aberystwyth University" 18 | case .transport: 19 | return "Transport" 20 | case .ev: 21 | return "Electric Vehicle" 22 | case .pubs: 23 | return "Pubs" 24 | case .sm: 25 | return "Supermarket" 26 | } 27 | } 28 | 29 | var shortName: String { 30 | switch self { 31 | case .au: 32 | return "University" 33 | case .transport: 34 | return "Transport" 35 | case .ev: 36 | return "EV" 37 | case .pubs: 38 | return "Pubs" 39 | case .sm: 40 | return "Shops" 41 | } 42 | } 43 | } 44 | 45 | struct Location: Codable, Identifiable, Hashable, Comparable { 46 | let id: String 47 | let name: String 48 | let note: String? 49 | let imageLink: String? 50 | let latitude: Double 51 | let longitude: Double 52 | let webLink: Weblink? 53 | let locationType: LocationType 54 | 55 | var coordinate: CLLocationCoordinate2D { 56 | CLLocation(latitude: latitude, longitude: longitude).coordinate 57 | } 58 | 59 | static func ==(lhs: Location, rhs: Location) -> Bool { 60 | lhs.id == rhs.id 61 | } 62 | 63 | static func <(lhs: Location, rhs: Location) -> Bool { 64 | lhs.name < rhs.name 65 | } 66 | 67 | var imageUrl: URL? { 68 | URL(string: imageLink ?? "") 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /iOSDevUK/Model/ScheduleType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScheduleType.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/03/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ScheduleType: String, CaseIterable { 11 | case mySchedule = "My Schedule" 12 | case allSessions = "All Sessions" 13 | } 14 | -------------------------------------------------------------------------------- /iOSDevUK/Model/Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Session.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 21/09/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SessionType: String, Codable, CaseIterable { 11 | case talk, workshop, lightningTalk, social, lunch, coffeeBiscuits, dinner, registration 12 | 13 | var name: String { 14 | switch self { 15 | case .talk: 16 | return "Talk" 17 | case .workshop: 18 | return "Workshop" 19 | case .lightningTalk: 20 | return "Lightning Talk" 21 | case .social: 22 | return "Social" 23 | case .lunch: 24 | return "Lunch" 25 | case .coffeeBiscuits: 26 | return "Coffee Biscuits" 27 | case .dinner: 28 | return "Dinner" 29 | case .registration: 30 | return "Registration" 31 | } 32 | } 33 | } 34 | 35 | struct Session: Codable, Identifiable, Hashable, Comparable { 36 | 37 | let id: String 38 | let title: String 39 | let content: String 40 | let startDate: Date 41 | let endDate: Date 42 | let locationId: String? 43 | let speakerIds: [String] 44 | let type: SessionType 45 | 46 | var startingDay: String { 47 | startDate.dateAndWeekDay 48 | } 49 | 50 | var duration: String { 51 | "\(startDate.weekDayTime) - \(endDate.time)" 52 | } 53 | 54 | static func < (lhs: Session, rhs: Session) -> Bool { 55 | lhs.startDate < rhs.startDate 56 | } 57 | 58 | func hash(into hasher: inout Hasher) { 59 | hasher.combine(title) 60 | hasher.combine(content) 61 | hasher.combine(startDate) 62 | hasher.combine(endDate) 63 | hasher.combine(locationId) 64 | hasher.combine(speakerIds) 65 | hasher.combine(type) 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /iOSDevUK/Model/Speaker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Speaker.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 10/09/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Weblink: Codable, Hashable { 11 | let name: String 12 | let recordName: String 13 | let url: String 14 | 15 | var webUrl: URL? { 16 | URL(string: url) 17 | } 18 | 19 | } 20 | 21 | struct Speaker: Codable, Identifiable, Hashable, Comparable { 22 | 23 | let id: String 24 | let name: String 25 | let currentPosition: String? 26 | let biography: String 27 | let linkedIn: String? 28 | let twitterId: String? 29 | let imageLink: String 30 | let webLinks: [Weblink]? 31 | 32 | var imageUrl: URL? { 33 | URL(string: imageLink) 34 | } 35 | 36 | static func < (lhs: Speaker, rhs: Speaker) -> Bool { 37 | lhs.name < rhs.name 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /iOSDevUK/Model/Sponsor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sponsor.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 21/09/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | enum SponsorCategory: String, Codable, Comparable, Hashable, CaseIterable { 12 | case Platinum, Gold, Silver 13 | 14 | var sortOrder: Int { 15 | switch self { 16 | case .Platinum: 17 | return 0 18 | case .Gold: 19 | return 1 20 | case .Silver: 21 | return 2 22 | } 23 | } 24 | 25 | var color: Color { 26 | switch self { 27 | case .Platinum: 28 | return Color(.platinum) 29 | case .Gold: 30 | return Color(.gold) 31 | case .Silver: 32 | return Color(.silver) 33 | } 34 | } 35 | 36 | static func < (lhs: SponsorCategory, rhs: SponsorCategory) -> Bool { 37 | lhs.sortOrder < rhs.sortOrder 38 | } 39 | 40 | } 41 | 42 | struct Sponsor: Codable, Identifiable, Equatable, Hashable, Comparable { 43 | 44 | let id: String 45 | let name: String 46 | let tagline: String 47 | let url: String 48 | let urlText: String 49 | let sponsorCategory: SponsorCategory 50 | let imageLinkDark: String? 51 | let imageLinkLight: String? 52 | 53 | var imageUrlDark: URL? { 54 | URL(string: imageLinkDark ?? "") 55 | } 56 | var imageUrlLight: URL? { 57 | URL(string: imageLinkLight ?? "") 58 | } 59 | 60 | var webUrl: URL? { 61 | URL(string: url) 62 | } 63 | 64 | static func < (lhs: Sponsor, rhs: Sponsor) -> Bool { 65 | lhs.sponsorCategory < rhs.sponsorCategory 66 | } 67 | 68 | static func ==(lhs: Sponsor, rhs: Sponsor) -> Bool { 69 | lhs.id == rhs.id 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /iOSDevUK/Model/WeatherData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeatherData.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 01/04/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct WeatherData: Identifiable { 11 | var id = UUID().uuidString 12 | let lastCheck = Date() 13 | let tempDate: Date 14 | let condition: String 15 | let symbolName: String 16 | 17 | let currentTempC: Double 18 | let feelsLikeC: Double 19 | 20 | var currentTempF: Double { 21 | (currentTempC * 9/5) + 32 22 | } 23 | var feelsLikeF: Double { 24 | (currentTempC * 9/5) + 32 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /iOSDevUK/Modifiers/BackgroundModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundModifier.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 15/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CapsuleBackgroundModifier: ViewModifier { 11 | 12 | let height: CGFloat 13 | let color: Color 14 | 15 | func body(content: Content) -> some View { 16 | content 17 | .padding(.vertical, 10) 18 | .padding(.horizontal, 16) 19 | .background { 20 | Capsule() 21 | .foregroundStyle(color) 22 | .frame(height: height) 23 | } 24 | } 25 | } 26 | 27 | extension View { 28 | func capsuleBackgroundView(height: CGFloat = 35, color: Color = Color(.linkButton)) -> some View { 29 | self.modifier(CapsuleBackgroundModifier(height: height, color: color)) 30 | } 31 | } 32 | 33 | 34 | struct RoundBackgroundModifier: ViewModifier { 35 | let color: Color 36 | 37 | func body(content: Content) -> some View { 38 | content 39 | .padding(16) 40 | .background { 41 | RoundedRectangle(cornerRadius: 16) 42 | .foregroundStyle(color) 43 | } 44 | } 45 | } 46 | 47 | extension View { 48 | func roundBackgroundView(color: Color) -> some View { 49 | self.modifier(RoundBackgroundModifier(color: color)) 50 | } 51 | } 52 | 53 | 54 | struct CapsuleOutlineModifier: ViewModifier { 55 | 56 | let height: CGFloat 57 | let color: Color 58 | 59 | func body(content: Content) -> some View { 60 | content 61 | .padding(.vertical, 10) 62 | .padding(.horizontal, 16) 63 | .background { 64 | Capsule() 65 | .stroke(color, lineWidth: 2) 66 | .frame(height: height) 67 | } 68 | } 69 | } 70 | 71 | extension View { 72 | func capsuleOutlineView(height: CGFloat = 40, color: Color = Color(.outline)) -> some View { 73 | self.modifier(CapsuleOutlineModifier(height: height, color: color)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /iOSDevUK/Modifiers/ButtonModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonModifier.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 16/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppButtonStyle: ButtonStyle { 11 | let color: Color 12 | let fontSize: CGFloat 13 | let height: CGFloat 14 | 15 | init(color: Color, fontSize: CGFloat = 15, height: CGFloat = 16) { 16 | self.color = color 17 | self.fontSize = fontSize 18 | self.height = height 19 | } 20 | 21 | 22 | 23 | func makeBody(configuration: Configuration) -> some View { 24 | configuration.label 25 | .boldAppFont(size: fontSize) 26 | .foregroundStyle(Color(.buttonTitle)) 27 | .padding(.horizontal, 30) 28 | .padding(.vertical, height) 29 | .frame(maxWidth: .infinity) 30 | .background( 31 | Capsule() 32 | .fill(color) 33 | ) 34 | } 35 | } 36 | 37 | extension ButtonStyle where Self == AppButtonStyle { 38 | static func primary(with color: Color) -> AppButtonStyle { 39 | return AppButtonStyle(color: color) 40 | } 41 | 42 | static var appPrimary: AppButtonStyle { 43 | return primary(with: Color(.buttonBackground)) 44 | } 45 | 46 | static func customButton(fontSize: CGFloat = 24, height: CGFloat = 15) -> AppButtonStyle { 47 | return AppButtonStyle(color: Color(.buttonBackground), fontSize: fontSize, height: height) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /iOSDevUK/Modifiers/FontModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppFontModifier.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 15/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppFontModifier: ViewModifier { 11 | enum Weight { 12 | case light 13 | case regular 14 | case semibold 15 | case bold 16 | } 17 | 18 | let size: CGFloat 19 | let weight: Weight 20 | 21 | init(weight: Weight, size: CGFloat) { 22 | self.size = size 23 | self.weight = weight 24 | } 25 | 26 | func body(content: Content) -> some View { 27 | switch weight { 28 | case .regular: 29 | content.font(.custom("SFProDisplay-Regular", size: size)) 30 | case .bold: 31 | content.font(.custom("SFProDisplay-Bold", size: size)) 32 | case .light: 33 | content.font(.custom("SFProDisplay-Light", size: size)) 34 | case .semibold: 35 | content.font(.custom("SFProDisplay-Semibold", size: size)) 36 | 37 | } 38 | } 39 | } 40 | 41 | extension View { 42 | func appFont(weight: AppFontModifier.Weight = .regular, size: CGFloat) -> some View { 43 | modifier(AppFontModifier(weight: weight, size: size)) 44 | } 45 | 46 | func boldAppFont(size: CGFloat) -> some View { 47 | appFont(weight: .bold, size: size) 48 | } 49 | 50 | func semiboldAppFont(size: CGFloat) -> some View { 51 | appFont(weight: .semibold, size: size) 52 | } 53 | 54 | func lightAppFont(size: CGFloat) -> some View { 55 | appFont(weight: .light, size: size) 56 | } 57 | } 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /iOSDevUK/Navigation/NavigationRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationRouter.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/11/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | final class NavigationRouter: ObservableObject { 11 | @Published var homePath: [Destination] = [] 12 | @Published var schedulePath: [Destination] = [] 13 | @Published var attendeePath: [Destination] = [] 14 | @Published var infoPath: [InfoDestination] = [] 15 | } 16 | 17 | 18 | enum Destination: Hashable { 19 | case speaker(Speaker) 20 | case speakers([Speaker]) 21 | case session(Session) 22 | case sessions([Session]) 23 | case sponsor 24 | case locations(locations: [Location]) 25 | } 26 | 27 | enum InfoDestination: Hashable { 28 | case inclusivity 29 | case sponsors 30 | case aboutApp 31 | case appInformation 32 | case locationList 33 | case locations(locations: [Location]) 34 | case admin 35 | case adminSpeakers 36 | case adminAddSpeaker(Speaker?) 37 | case adminSessions 38 | case adminAddSession(Session?) 39 | case adminLocations 40 | case adminAddLocation(Location?) 41 | case adminSponsors 42 | case adminAddSponsor(Sponsor?) 43 | } 44 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/AttendeeView/AttendeeRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttendeeRowView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 17/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AttendeeRowView: View { 11 | let subtitle: String? 12 | let title: String 13 | let description: String 14 | let image: Image 15 | 16 | init(subtitle: String? = nil, title: String, description: String, image: Image) { 17 | self.subtitle = subtitle 18 | self.title = title 19 | self.description = description 20 | self.image = image 21 | } 22 | 23 | var body: some View { 24 | HStack { 25 | VStack(alignment: .leading, spacing: 10) { 26 | if let subtitle { 27 | Text(subtitle) 28 | .appFont(size: 12) 29 | .foregroundStyle(Color(.purple200)) 30 | } 31 | 32 | Text(title) 33 | .semiboldAppFont(size: 22) 34 | .foregroundStyle(Color(.mainText)) 35 | 36 | Text(description) 37 | .multilineTextAlignment(.leading) 38 | .appFont(size: 14) 39 | .foregroundStyle(Color(.textBody)) 40 | } 41 | 42 | Spacer() 43 | 44 | image 45 | } 46 | .padding(10) 47 | .roundBackgroundView(color: Color(.cardBackground)) 48 | } 49 | } 50 | 51 | #Preview { 52 | AttendeeRowView(subtitle: nil, title: "", description: "", image: Image(systemName: "circle")) 53 | } 54 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/AttendeeView/AttendeeScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttendeeView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 10/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AttendeeScreen: View { 11 | @EnvironmentObject var viewModel: BaseViewModel 12 | @EnvironmentObject var router: NavigationRouter 13 | 14 | @ViewBuilder 15 | private func main() -> some View { 16 | ScrollView { 17 | LazyVStack(alignment: .leading, spacing: 15) { 18 | ForEach(viewModel.infoItems) { item in 19 | if let url = item.url { 20 | Link(destination: url) { 21 | AttendeeRowView( 22 | subtitle: item.imageName == "ticket" ? "LINK" : "PDF", 23 | title: item.name, 24 | description: item.subtitle, 25 | image: Image(item.imageName ?? "") 26 | ) 27 | } 28 | } else { 29 | AttendeeRowView( 30 | title: item.name, 31 | description: item.subtitle, 32 | image: Image(item.imageName ?? "") 33 | ) 34 | } 35 | } 36 | } 37 | .padding([.horizontal, .top], 16) 38 | } 39 | } 40 | 41 | var body: some View { 42 | NavigationStack(path: $router.attendeePath) { 43 | main() 44 | .navigationTitle("Attendee Information") 45 | .navigationDestination(for: Destination.self) { destination in 46 | switch destination { 47 | case .session(let session): 48 | SessionDetailView(session: session) 49 | case .sessions(let sessions): 50 | AllSessionsView(sessions: sessions) 51 | case .speaker(let speaker): 52 | SpeakerDetailScreen(speaker: speaker) 53 | case .speakers(let speakers): 54 | AllSpeakersView(speakers: speakers) 55 | case .sponsor: 56 | SponsorsScreen() 57 | case .locations(let locations): 58 | MapScreen(allLocations: locations) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | struct AttendeeView_Previews: PreviewProvider { 66 | static var previews: some View { 67 | AttendeeScreen() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/HomeView/ContactButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactButtonView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 18/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContactButtonView: View { 11 | let imageName: String 12 | let title: String 13 | 14 | var body: some View { 15 | HStack { 16 | Image(imageName) 17 | .resizable() 18 | .aspectRatio(contentMode: .fit) 19 | .frame(width: 22) 20 | .foregroundStyle(Color(.mainText)) 21 | 22 | Spacer() 23 | 24 | Text(title) 25 | .semiboldAppFont(size: 14) 26 | .foregroundStyle(Color(.mainText)) 27 | 28 | Spacer() 29 | } 30 | .padding(.vertical, 2) 31 | .capsuleOutlineView() 32 | } 33 | } 34 | 35 | #Preview { 36 | ContactButtonView(imageName: "", title: "") 37 | } 38 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/HomeView/EventInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventInfoView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 18/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EventInfoView: View { 11 | 12 | let eventDate: String? 13 | let notificationBody: String? 14 | 15 | var body: some View { 16 | VStack(alignment: .leading, spacing: 20) { 17 | Text("Details") 18 | .foregroundStyle(Color(.mainText)) 19 | .boldAppFont(size: 20) 20 | 21 | UniversityMapView(eventDate: eventDate) 22 | 23 | if let notificationBody { 24 | Text(notificationBody) 25 | .appFont(size: 16) 26 | .foregroundStyle(Color(.mainText)) 27 | } 28 | } 29 | } 30 | } 31 | 32 | #Preview { 33 | EventInfoView(eventDate: "", notificationBody: "") 34 | } 35 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/HomeView/SessionsHorizontalScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionsHorizontalScrollView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 09/04/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SessionsHorizontalScrollView: View { 11 | let sessions: [Session] 12 | let geometry: GeometryProxy 13 | 14 | var body: some View { 15 | 16 | VStack(alignment: .leading) { 17 | HStack { 18 | Text(AppStrings.sessions) 19 | .foregroundStyle(Color(.mainText)) 20 | .boldAppFont(size: 20) 21 | 22 | Spacer() 23 | NavigationLink(AppStrings.viewAll, value: Destination.sessions(sessions)) 24 | .foregroundStyle(Color(.textGrey)) 25 | } 26 | .padding(.horizontal) 27 | 28 | if #available(iOS 17.0, *) { 29 | ScrollView(.horizontal) { 30 | LazyHStack(spacing: 10) { 31 | ForEach(sessions, id: \.self) { session in 32 | 33 | NavigationLink(value: Destination.session(session)) { 34 | SessionCardView(session: session, geometry: geometry) 35 | .id(session) 36 | } 37 | } 38 | } 39 | .scrollTargetLayout() 40 | } 41 | .scrollTargetBehavior(.viewAligned) 42 | .safeAreaPadding(.horizontal, 20) 43 | .scrollIndicators(.hidden) 44 | } else { 45 | ScrollView(.horizontal) { 46 | LazyHStack(spacing: 10) { 47 | ForEach(sessions, id: \.self) { session in 48 | 49 | NavigationLink(value: Destination.session(session)) { 50 | SessionCardView(session: session, geometry: geometry) 51 | .id(session) 52 | } 53 | } 54 | } 55 | .padding(.leading, 16) 56 | } 57 | .scrollIndicators(.hidden) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/HomeView/SpeakersHorizontalScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpeakersHorizontalScrollView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 15/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SpeakersHorizontalScrollView: View { 11 | let speakers: [Speaker] 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 20) { 15 | HStack { 16 | Text(AppStrings.speakers) 17 | .foregroundStyle(Color(.mainText)) 18 | .boldAppFont(size: 20) 19 | 20 | Spacer() 21 | 22 | NavigationLink(AppStrings.viewAll, value: Destination.speakers(speakers.sorted())) 23 | .foregroundStyle(Color(.textGrey)) 24 | } 25 | .padding(.horizontal) 26 | 27 | 28 | if #available(iOS 17.0, *) { 29 | ScrollView(.horizontal) { 30 | LazyHStack(spacing: 5) { 31 | ForEach(speakers) { speaker in 32 | NavigationLink(value: Destination.speaker(speaker)) { 33 | SpeakerCardView(speaker: speaker) 34 | .frame(width: 110) 35 | } 36 | } 37 | } 38 | .scrollTargetLayout() 39 | } 40 | .scrollTargetBehavior(.viewAligned) 41 | .safeAreaPadding(.horizontal, 10) 42 | .scrollIndicators(.hidden) 43 | } else { 44 | ScrollView(.horizontal) { 45 | LazyHStack(spacing: 10) { 46 | ForEach(speakers) { speaker in 47 | NavigationLink(value: Destination.speaker(speaker)) { 48 | SpeakerCardView(speaker: speaker) 49 | .frame(width: 110) 50 | } 51 | } 52 | } 53 | .padding(.leading, 16) 54 | } 55 | .scrollIndicators(.hidden) 56 | } 57 | } 58 | } 59 | } 60 | 61 | #Preview { 62 | SpeakersHorizontalScrollView(speakers: DummyData.speakers) 63 | } 64 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/InfoView/AboutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 13/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AboutView: View { 11 | @EnvironmentObject var viewModel: BaseViewModel 12 | @Environment(\.presentationMode) var presentationMode 13 | 14 | @ViewBuilder 15 | private func navigationBarLeadingItem() -> some View { 16 | Button { presentationMode.wrappedValue.dismiss() } 17 | label: { Image(.back) } 18 | .tint(Color(.mainText)) 19 | } 20 | 21 | var body: some View { 22 | ScrollView { 23 | Image(.aboutBackground) 24 | .resizable() 25 | .aspectRatio(contentMode: .fit) 26 | 27 | VStack(alignment: .leading, spacing: 20) { 28 | 29 | Text("iOSDevUK is the UK conference for iOS developers. It takes place in Aberystwyth, on the mid-Wales coast, from \(viewModel.eventInformation?.startDate.dayOfTheMonth ?? "") to the \(viewModel.eventInformation?.endDate.dayAndMonth ?? "").") 30 | 31 | Text("• Great talks\n• Great get-togethers\n• Optional workshops") .multilineTextAlignment(.leading) 32 | 33 | Text("The conference is organised by Aberystwyth University and is now in its eleventh year. iOS, iPhone, iPad, Apple Watch, watchOS, Apple TV and tvOS are trademarks of Apple Inc. For the avoidance of doubt, Apple Inc. has no association with this conference.") 34 | } 35 | .padding(20) 36 | } 37 | .appFont(size: 16) 38 | .foregroundStyle(Color(.textBody)) 39 | .edgesIgnoringSafeArea(.top) 40 | .navigationBarBackButtonHidden() 41 | .toolbar { 42 | ToolbarItem(placement: .topBarLeading, content: navigationBarLeadingItem) 43 | } 44 | 45 | } 46 | } 47 | 48 | struct AboutView_Previews: PreviewProvider { 49 | static var previews: some View { 50 | AboutView() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/InfoView/AppInformationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppInformationView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 13/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppInformationView: View { 11 | @EnvironmentObject var viewModel: BaseViewModel 12 | @Environment(\.presentationMode) var presentationMode 13 | 14 | @ViewBuilder 15 | private func navigationBarLeadingItem() -> some View { 16 | Button { presentationMode.wrappedValue.dismiss() } 17 | label: { Image(.back) } 18 | .tint(Color(.mainText)) 19 | } 20 | 21 | var body: some View { 22 | 23 | ScrollView { 24 | VStack(spacing: 20) { 25 | 26 | Text("The app has been developed by David Kababyan ([@Dave_iOSDev](https://twitter.com/Dave_iOSDev)), with contributions from Neil Taylor ([@digidol](https://twitter.com/digidol)).\n\nApp data wrangling by Chris Price ([@iOSDevUK](https://twitter.com/iOSDevUK)).") 27 | 28 | Text("Designed by Oksana Korotun ([@LinkedIn](https://linkedin.com/in/oksana-korotun/)).") 29 | 30 | Text("Thanks to John Gilbey ([@John_Gilbey](https://twitter.com/John_Gilbey)) for his picture of conference attendees that is used in this app.") 31 | 32 | Text("Other images by Neil & Chris.") 33 | 34 | VStack(spacing: 10) { 35 | if let twitterUrl = URL(string: ContactAccounts.developer) { 36 | Link("Contact the developer", destination: twitterUrl) 37 | } 38 | 39 | if let linkedInUrl = URL(string: ContactAccounts.designer) { 40 | Link("Contact the designer", destination: linkedInUrl) 41 | } 42 | 43 | if let twitterUrl = URL(string: ContactAccounts.digidol) { 44 | Link("Contact @digidol", destination: twitterUrl) 45 | } 46 | } 47 | .tint(Color(.purple300)) 48 | 49 | Spacer() 50 | } 51 | .appFont(size: 16) 52 | .multilineTextAlignment(.center) 53 | .padding(.top, 25) 54 | .padding() 55 | .navigationTitle(AppStrings.appInfo) 56 | .navigationBarBackButtonHidden() 57 | .toolbar { 58 | ToolbarItem(placement: .topBarLeading, content: navigationBarLeadingItem) 59 | } 60 | 61 | } 62 | } 63 | } 64 | 65 | struct AppInformationView_Previews: PreviewProvider { 66 | static var previews: some View { 67 | Group { 68 | NavigationView { 69 | AppInformationView() 70 | .preferredColorScheme(.dark) 71 | } 72 | NavigationView { 73 | AppInformationView() 74 | .preferredColorScheme(.light) 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/InfoView/DropDownRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DropDownRowView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 01/04/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DropDownRowView: View { 11 | let title: String 12 | let imageName: String 13 | let locations: [Location] 14 | @State private var isExpanded = false 15 | 16 | var body: some View { 17 | VStack(alignment: .leading) { 18 | LocationHeaderView( 19 | text: title, 20 | imageName: imageName, 21 | isExpanded: isExpanded 22 | ) { 23 | withAnimation { 24 | isExpanded.toggle() 25 | } 26 | } 27 | 28 | if isExpanded { 29 | ForEach(locations) { location in 30 | NavigationLink(value: InfoDestination.locations(locations: [location])) { 31 | Text(location.name) 32 | .semiboldAppFont(size: 16) 33 | .lineLimit(1) 34 | .minimumScaleFactor(0.6) 35 | } 36 | .tint(Color(.buttonBackground)) 37 | .frame(height: 44) 38 | .padding(.horizontal, 10) 39 | } 40 | } 41 | } 42 | .roundBackgroundView(color: Color(.cardBackground)) 43 | } 44 | } 45 | 46 | #Preview { 47 | DropDownRowView(title: "Aber uni", imageName: "", locations: [DummyData.location, DummyData.location]) 48 | } 49 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/InfoView/InclusivityView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InclusivityView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 11/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct InclusivityView: View { 11 | @EnvironmentObject var viewModel: BaseViewModel 12 | @Environment(\.presentationMode) var presentationMode 13 | 14 | 15 | @ViewBuilder 16 | private func navigationBarLeadingItem() -> some View { 17 | Button { presentationMode.wrappedValue.dismiss() } 18 | label: { Image(.back) } 19 | .tint(Color(.mainText)) 20 | } 21 | 22 | var body: some View { 23 | ScrollView { 24 | Image(.conference) 25 | .resizable() 26 | .aspectRatio(contentMode: .fit) 27 | 28 | Text(AppStrings.inclusivityPolicy) 29 | .semiboldAppFont(size: 24) 30 | .foregroundStyle(Color(.mainText)) 31 | .minimumScaleFactor(0.7) 32 | 33 | Spacer() 34 | 35 | if let inclusivityText = viewModel.eventInformation?.inclusivityText { 36 | Text(inclusivityText) 37 | .appFont(size: 16) 38 | .foregroundStyle(Color(.textBody)) 39 | .lineLimit(nil) 40 | .padding() 41 | } 42 | } 43 | .edgesIgnoringSafeArea(.top) 44 | .navigationBarBackButtonHidden() 45 | .toolbar { 46 | ToolbarItem(placement: .topBarLeading, content: navigationBarLeadingItem) 47 | } 48 | 49 | } 50 | } 51 | 52 | struct InclusivityViewView_Previews: PreviewProvider { 53 | static var previews: some View { 54 | InclusivityView() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/InfoView/LocationHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationHeaderView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 01/04/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LocationHeaderView: View { 11 | let text: String 12 | let imageName: String 13 | let isExpanded: Bool 14 | var action: () -> Void 15 | 16 | init(text: String, imageName: String, isExpanded: Bool, action: @escaping () -> Void) { 17 | self.text = text 18 | self.imageName = imageName 19 | self.isExpanded = isExpanded 20 | self.action = action 21 | } 22 | 23 | var body: some View { 24 | 25 | HStack(spacing: 10) { 26 | Image(imageName) 27 | .resizable() 28 | .frame(width: 20, height: 20) 29 | .foregroundStyle(Color(.icon)) 30 | .roundBackgroundView(color: Color(.linkButton)) 31 | 32 | Text(text) 33 | .boldAppFont(size: 16) 34 | .foregroundStyle(Color(.mainText)) 35 | 36 | Spacer() 37 | 38 | Image(systemName: isExpanded ? ImageNames.chevronUp : ImageNames.chevronDown) 39 | } 40 | .contentShape(Rectangle()) //needed to recognise tap on empty space 41 | .onTapGesture { 42 | action() 43 | } 44 | } 45 | } 46 | 47 | #Preview { 48 | LocationHeaderView(text: "", imageName: "", isExpanded: false) { 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/InfoView/LocationsListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationsListView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 25/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LocationsListView: View { 11 | @EnvironmentObject var viewModel: BaseViewModel 12 | @Environment(\.presentationMode) var presentationMode 13 | 14 | @State var expanded: Bool = true 15 | var groupedLocations: [String : [Location]] { 16 | .init( 17 | grouping: viewModel.locations, 18 | by: { $0.locationType.rawValue } 19 | ) 20 | } 21 | 22 | @ViewBuilder 23 | private func navigationBarLeadingItem() -> some View { 24 | Button { presentationMode.wrappedValue.dismiss() } 25 | label: { Image(.back) } 26 | .tint(Color(.mainText)) 27 | } 28 | 29 | @ViewBuilder 30 | private func main() -> some View { 31 | VStack(alignment: .leading, spacing: 10) { 32 | NavigationLink(value: InfoDestination.locations(locations: viewModel.locations)) { 33 | UniversityMapView() 34 | .padding(.horizontal, 16) 35 | } 36 | .buttonStyle(.plain) 37 | 38 | ScrollView { 39 | ForEach(groupedLocations.keys.sorted(), id: \String.self) { key in 40 | let locationType: LocationType = LocationType(rawValue: key) ?? .au 41 | 42 | DropDownRowView( 43 | title: locationType.name, 44 | imageName: locationType.shortName.lowercased(), 45 | locations: groupedLocations[key] ?? [] 46 | ) 47 | .padding(.horizontal, 16) 48 | } 49 | } 50 | } 51 | } 52 | 53 | 54 | var body: some View { 55 | main() 56 | .navigationBarTitle(AppStrings.locations, displayMode: .inline) 57 | .navigationBarBackButtonHidden() 58 | .toolbar { 59 | ToolbarItem(placement: .topBarLeading, content: navigationBarLeadingItem) 60 | } 61 | } 62 | } 63 | 64 | struct LocationsListView_Previews: PreviewProvider { 65 | static var previews: some View { 66 | LocationsListView() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/SecondaryViews/Sessions/AllSessionsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllSessionsView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 21/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AllSessionsView: View { 11 | 12 | @StateObject private var viewModel = AllSessionsViewModel() 13 | @EnvironmentObject var baseViewModel: BaseViewModel 14 | 15 | @Environment(\.presentationMode) var presentationMode 16 | var showBackButton: Bool = true 17 | let sessions: [Session] 18 | private var groupedSessions: [String : [Session]] { 19 | .init( 20 | grouping: baseViewModel.sessions, 21 | by: { $0.startingDay } 22 | ) 23 | } 24 | 25 | 26 | @ViewBuilder 27 | private func navigationBarLeadingItem() -> some View { 28 | Button { presentationMode.wrappedValue.dismiss() } 29 | label: { Image(.back) } 30 | .tint(Color(.mainText)) 31 | } 32 | 33 | var body: some View { 34 | ScrollView { 35 | LazyVStack(alignment: .leading, spacing: 10) { 36 | ForEach(groupedSessions[viewModel.selectedDate]?.sorted() ?? [], id: \.id) { session in 37 | 38 | NavigationLink(value: Destination.session(session)) { 39 | SessionRowView( 40 | session: session, 41 | showSpeakers: true 42 | ) { 43 | baseViewModel.updateFavoriteSession(sessionId: session.id) 44 | } 45 | .id(session) 46 | } 47 | } 48 | } 49 | .padding(.horizontal, 16) 50 | } 51 | .task { await viewModel.fetchEventNotification() } 52 | .task { baseViewModel.loadFavSessions() } 53 | .task { viewModel.setCurrentDate() } 54 | .onAppear { viewModel.setSessions(sessions: sessions) } 55 | .safeAreaInset(edge: .top) { 56 | Picker("", selection: $viewModel.selectedDate.animation()) { 57 | ForEach(groupedSessions.keys.sorted(), id: \String.self) { weekDay in 58 | Text(weekDay.removeDigits) 59 | } 60 | } 61 | .pickerStyle(.segmented) 62 | .padding(10) 63 | .background(Color(.background)) 64 | } 65 | .navigationTitle(showBackButton ? "All Sessions" : "") 66 | .navigationBarTitleDisplayMode(.inline) 67 | .navigationBarBackButtonHidden() 68 | .toolbar { 69 | if showBackButton { 70 | ToolbarItem(placement: .topBarLeading, content: navigationBarLeadingItem) 71 | } 72 | } 73 | } 74 | } 75 | 76 | struct SessionsView_Previews: PreviewProvider { 77 | static var previews: some View { 78 | NavigationView { 79 | AllSessionsView(sessions: DummyData.sessions) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/SecondaryViews/Sessions/SessionDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionDetailView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 03/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SessionDetailView: View { 11 | @EnvironmentObject var baseViewModel: BaseViewModel 12 | @Environment(\.presentationMode) var presentationMode 13 | 14 | let session: Session 15 | 16 | @ViewBuilder 17 | private func navigationBarLeadingItem() -> some View { 18 | Button { presentationMode.wrappedValue.dismiss() } 19 | label: { Image(.back) } 20 | .tint(Color(.mainText)) 21 | } 22 | 23 | @ViewBuilder 24 | private func main() -> some View { 25 | 26 | ScrollView { 27 | VStack(alignment: .leading, spacing: 10) { 28 | 29 | Text(session.title) 30 | .boldAppFont(size: 24) 31 | .foregroundStyle(Color(.mainText)) 32 | .padding(.top, 10) 33 | 34 | Text(session.content) 35 | .foregroundStyle(Color(.textBody)) 36 | .appFont(size: 16) 37 | .multilineTextAlignment(.leading) 38 | .padding(.vertical, 10) 39 | 40 | 41 | InfoRowView(text: session.duration, imageName: ImageNames.calendar) 42 | 43 | 44 | if let location = baseViewModel.getLocation(with: session.locationId) { 45 | NavigationLink(value: Destination.locations(locations: [location])) { 46 | InfoRowView(text: location.name, imageName: ImageNames.location) 47 | } 48 | } 49 | 50 | if !baseViewModel.getSpeakers(with: session.speakerIds).isEmpty { 51 | ForEach(baseViewModel.getSpeakers(with: session.speakerIds)) { speaker in 52 | NavigationLink(value: Destination.speaker(speaker)) { 53 | SpeakerRowView(speaker: speaker) 54 | } 55 | } 56 | } 57 | } 58 | .padding(.horizontal, 16) 59 | } 60 | .scrollIndicators(.hidden) 61 | .safeAreaInset(edge: .bottom) { 62 | AnimatedButtonView( 63 | title: baseViewModel.isFavorite(session.id) ? "Remove from schedule" : "Add to schedule", 64 | color: Color(.buttonBackground), 65 | action: { 66 | baseViewModel.updateFavoriteSession(sessionId: session.id) 67 | } 68 | ) 69 | .frame(height: 50) 70 | .padding([.bottom, .horizontal], 16) 71 | } 72 | } 73 | 74 | var body: some View { 75 | main() 76 | .navigationTitle("Event Info") 77 | .navigationBarTitleDisplayMode(.inline) 78 | .navigationBarBackButtonHidden() 79 | .toolbar { 80 | ToolbarItem(placement: .topBarLeading, content: navigationBarLeadingItem) 81 | } 82 | 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/SecondaryViews/SpeakerView/AllSpeakersView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllSpeakersView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 10/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AllSpeakersView: View { 11 | @Environment(\.presentationMode) var presentationMode 12 | 13 | let speakers: [Speaker] 14 | 15 | @ViewBuilder 16 | private func navigationBarLeadingItem() -> some View { 17 | Button { presentationMode.wrappedValue.dismiss() } 18 | label: { Image(.back) } 19 | .tint(Color(.mainText)) 20 | } 21 | 22 | 23 | var body: some View { 24 | 25 | ScrollView { 26 | LazyVStack { 27 | ForEach(speakers) { speaker in 28 | NavigationLink(value: Destination.speaker(speaker)) { 29 | SpeakerRowView(speaker: speaker) 30 | .padding(.bottom, 5) 31 | } 32 | } 33 | } 34 | .padding([.top, .horizontal], 16) 35 | } 36 | .scrollIndicators(.hidden) 37 | .navigationTitle(AppStrings.speakers) 38 | .navigationBarTitleDisplayMode(.large) 39 | .navigationBarBackButtonHidden() 40 | .toolbar { 41 | ToolbarItem(placement: .topBarLeading, content: navigationBarLeadingItem) 42 | } 43 | } 44 | } 45 | 46 | struct SpeakersView_Previews: PreviewProvider { 47 | static var previews: some View { 48 | NavigationView { 49 | AllSpeakersView(speakers: DummyData.speakers) 50 | } 51 | .previewDevice(PreviewDevice(rawValue: "iPhone 14 pro")) 52 | .previewDisplayName("iPhone 14") 53 | 54 | AllSpeakersView(speakers: DummyData.speakers) 55 | .previewDevice(PreviewDevice(rawValue: "iPad mini (6th generation)")) 56 | .previewDisplayName("iPad mini") 57 | 58 | AllSpeakersView(speakers: DummyData.speakers) 59 | .previewDevice(PreviewDevice(rawValue: "iPad Pro (12.9-inch) (6th generation)")) 60 | .previewDisplayName("iPad pro 11") 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/SecondaryViews/SponsorsView/SponsorsScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SponsorsView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 27/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SponsorsScreen: View { 11 | @EnvironmentObject var viewModel: BaseViewModel 12 | @Environment(\.presentationMode) var presentationMode 13 | 14 | @ViewBuilder 15 | private func navigationBarLeadingItem() -> some View { 16 | Button { presentationMode.wrappedValue.dismiss() } 17 | label: { Image(.back) } 18 | .tint(Color(.mainText)) 19 | } 20 | 21 | var body: some View { 22 | ScrollView { 23 | LazyVStack(alignment: .leading, spacing: 20) { 24 | ForEach(viewModel.sponsors) { sponsor in 25 | SponsorRow(sponsor: sponsor) 26 | } 27 | } 28 | .padding([.top, .horizontal], 16) 29 | } 30 | .navigationTitle(AppStrings.sponsors) 31 | .navigationBarTitleDisplayMode(.inline) 32 | .navigationBarBackButtonHidden() 33 | .toolbar { 34 | ToolbarItem(placement: .topBarLeading, content: navigationBarLeadingItem) 35 | } 36 | 37 | } 38 | } 39 | 40 | struct SponsorsView_Previews: PreviewProvider { 41 | static var previews: some View { 42 | SponsorsScreen() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/AnimatedButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatedButtonView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 18/04/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AnimatedButtonView: View { 11 | @State var isAnimating = false 12 | @State var doneAnimation = false 13 | @State var submitScale: CGFloat = 1 14 | 15 | let animationDuration: TimeInterval = 0.20 16 | 17 | let title: String 18 | let color: Color 19 | let shouldAnimate: Bool 20 | let buttonAction: () -> Void 21 | 22 | init(title: String, color: Color, shouldAnimate: Bool = true, action: @escaping () -> Void) { 23 | self.title = title 24 | self.color = color 25 | self.shouldAnimate = shouldAnimate 26 | self.buttonAction = action 27 | } 28 | 29 | 30 | var body: some View { 31 | ZStack { 32 | Capsule() 33 | .fill(color) 34 | .frame(maxWidth: self.isAnimating ? (52) : .infinity) 35 | .scaleEffect(submitScale, anchor: .center) 36 | 37 | Tick(scaleFactor: 0.2) 38 | .trim(from: 0, to: self.doneAnimation ? 1 : 0) 39 | .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round)) 40 | .foregroundColor(Color(.buttonTitle)) 41 | .frame(width: 16) 42 | .offset(x: -4, y: 4) 43 | .animation(.easeOut(duration: 0.35), value: self.doneAnimation) 44 | 45 | Text(title) 46 | .semiboldAppFont(size: 18) 47 | .foregroundColor(Color(.buttonTitle)) 48 | .opacity(self.isAnimating ? 0 : 1) 49 | .animation(.easeOut(duration: animationDuration), value: self.isAnimating) 50 | .scaleEffect(self.isAnimating ? 0.7 : 1) 51 | .animation(.easeOut(duration: animationDuration), value: self.isAnimating) 52 | .padding(10) 53 | } 54 | .frame(maxWidth: .infinity) 55 | .onTapGesture { 56 | if shouldAnimate && !self.isAnimating { 57 | toggleIsAnimating() 58 | resetSubmit() 59 | self.doneAnimation = true 60 | } 61 | buttonAction() 62 | } 63 | } 64 | 65 | func resetSubmit() { 66 | Timer.scheduledTimer(withTimeInterval: animationDuration * 4.5, repeats: false) { _ in 67 | self.doneAnimation.toggle() 68 | } 69 | Timer.scheduledTimer(withTimeInterval: animationDuration * 5.5, repeats: false) { _ in 70 | toggleIsAnimating() 71 | } 72 | } 73 | 74 | func toggleIsAnimating() { 75 | withAnimation(Animation.spring(response: animationDuration * 1.25, dampingFraction: 0.9, blendDuration: 1)) { 76 | self.isAnimating.toggle() 77 | } 78 | } 79 | } 80 | 81 | 82 | 83 | 84 | struct Tick: Shape { 85 | let scaleFactor: CGFloat 86 | 87 | func path(in rect: CGRect) -> Path { 88 | let cX = rect.midX + 4 89 | let cY = rect.midY - 3 90 | 91 | var path = Path() 92 | path.move(to: CGPoint(x: rect.midX, y: rect.minY)) 93 | path.move(to: CGPoint(x: cX - (42 * scaleFactor), y: cY - (4 * scaleFactor))) 94 | path.addLine(to: CGPoint(x: cX - (scaleFactor * 18), y: cY + (scaleFactor * 28))) 95 | path.addLine(to: CGPoint(x: cX + (scaleFactor * 40), y: cY - (scaleFactor * 36))) 96 | return path 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/AppSegmentControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppSegmentControl.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | import SwiftUI 11 | 12 | struct AppSegmentedControl: View where T.RawValue == String { 13 | 14 | @Binding var selection: Int 15 | 16 | var options: [T] { 17 | return T.allCases as! [T] 18 | } 19 | 20 | 21 | var body: some View { 22 | HStack(spacing: 2) { 23 | ForEach(options.indices, id:\.self) { index in 24 | 25 | Rectangle() 26 | .foregroundStyle(Color(.buttonBackground)) 27 | .cornerRadius(20) 28 | .padding(2) 29 | .opacity(selection == index ? 1 : 0.1) 30 | .onTapGesture { 31 | withAnimation(.interactiveSpring()) { 32 | selection = index 33 | } 34 | } 35 | .overlay( 36 | Text(options[index].rawValue.capitalized) 37 | .semiboldAppFont(size: 18) 38 | .foregroundStyle(selection == index ? Color(.buttonTitle) : Color(.segmentTitle)) 39 | ) 40 | } 41 | } 42 | .frame(height: 40) 43 | .cornerRadius(20) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/DayPickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DayPickerView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | //struct DayPickerView: View { 11 | // 12 | // let days: [String] 13 | // @Binding var selection: String 14 | // 15 | // var body: some View { 16 | // ScrollView(.horizontal) { 17 | // HStack { 18 | // ForEach(days, id: \.self) { day in 19 | // VStack(spacing: 20) { 20 | // Text(day.removeChars) 21 | // .semiboldAppFont(size: 20) 22 | // Text(day.removeDigits.uppercased()) 23 | // .appFont(size: 14) 24 | // } 25 | // .frame(width: 54, height: 56) 26 | // .foregroundStyle(selection == day ? Color(.buttonTitle) : Color(.mainText)) 27 | // .roundBackgroundView(color: selection == day ? Color(.pickerSelected) : Color(.cardBackground)) 28 | // .onTapGesture { 29 | // selection = day 30 | // } 31 | // } 32 | // } 33 | // .padding(.leading) 34 | // } 35 | // } 36 | //} 37 | 38 | //#Preview { 39 | // DayPickerView() 40 | //} 41 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/EmptySessionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptySessionView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 05/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EmptyContentView: View { 11 | let image: Image 12 | let title: String 13 | let description: String? 14 | let buttonTitle: String? 15 | var action: (() -> ())? 16 | 17 | init( 18 | image: Image, 19 | title: String, 20 | description: String? = nil, 21 | buttonTitle: String? = nil, 22 | action: (() -> Void)? = nil 23 | ) { 24 | self.image = image 25 | self.title = title 26 | self.description = description 27 | self.buttonTitle = buttonTitle 28 | self.action = action 29 | } 30 | 31 | var body: some View { 32 | 33 | if #available(iOS 17.0, *) { 34 | ContentUnavailableView { 35 | VStack(spacing: 10) { 36 | image 37 | .resizable() 38 | .frame(width: 80, height: 80) 39 | .aspectRatio(contentMode: .fill) 40 | .foregroundStyle(Color(.linkButton)) 41 | 42 | Text(title) 43 | .boldAppFont(size: 20) 44 | .foregroundStyle(Color(.mainText)) 45 | } 46 | } description: { 47 | if let description { 48 | Text(description) 49 | .multilineTextAlignment(.center) 50 | .foregroundStyle(Color(.mainText)) 51 | .appFont(size: 16) 52 | } 53 | } actions: { 54 | if let action = action, let buttonTitle { 55 | Button(action: action, label: { 56 | Text(buttonTitle) 57 | }) 58 | .buttonStyle(.appPrimary) 59 | } 60 | } 61 | } else { 62 | VStack(spacing: 12) { 63 | Spacer() 64 | 65 | image 66 | .resizable() 67 | .frame(width: 80, height: 80) 68 | .foregroundStyle(Color(.linkButton)) 69 | 70 | Text(title) 71 | .semiboldAppFont(size: 20) 72 | .foregroundStyle(Color(.mainText)) 73 | 74 | if let description { 75 | Text(description) 76 | .multilineTextAlignment(.center) 77 | .foregroundStyle(Color(.mainText)) 78 | .appFont(size: 16) 79 | } 80 | 81 | if let action = action, let buttonTitle { 82 | Spacer() 83 | 84 | Button(action: action, label: { 85 | Text(buttonTitle) 86 | }) 87 | .buttonStyle(.appPrimary) 88 | .padding([.horizontal, .bottom], 16) 89 | } 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/HourlyWeatherView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HourlyWeatherView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 02/04/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HourlyWeatherView: View { 11 | 12 | private let weatherData: WeatherData 13 | private let isCelsius: Bool 14 | 15 | init(weatherData: WeatherData, isCelsius: Bool) { 16 | self.weatherData = weatherData 17 | self.isCelsius = isCelsius 18 | } 19 | 20 | var body: some View { 21 | VStack(spacing: 5) { 22 | Text(weatherData.tempDate.formatted(date: .omitted, time: .shortened)) 23 | .appFont(size: 16) 24 | 25 | HStack(spacing: 5) { 26 | Image(systemName: weatherData.symbolName) 27 | .symbolRenderingMode(.palette) 28 | .foregroundStyle(Color.primary, Color(.purple200)) 29 | 30 | Text(isCelsius ? weatherData.feelsLikeC.roundNearest().toCelsius : weatherData.feelsLikeF.roundNearest().toFahrenheit) 31 | } 32 | } 33 | .appFont(size: 14) 34 | .foregroundStyle(Color(.mainText)) 35 | } 36 | } 37 | 38 | struct HourlyWeatherView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | HourlyWeatherView(weatherData: DummyData.weatherData, isCelsius: true) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/InfoRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkRowView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 15/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct InfoRowView: View { 11 | let text: String 12 | let imageName: String 13 | 14 | var body: some View { 15 | 16 | HStack(spacing: 10) { 17 | Image(imageName) 18 | .resizable() 19 | .frame(width: 30, height: 30) 20 | .foregroundStyle(Color(.icon)) 21 | .roundBackgroundView(color: Color(.linkButton)) 22 | 23 | Text(text) 24 | .boldAppFont(size: 16) 25 | .foregroundStyle(Color(.mainText)) 26 | 27 | Spacer() 28 | } 29 | .roundBackgroundView(color: Color(.cardBackground)) 30 | 31 | } 32 | } 33 | 34 | #Preview { 35 | InfoRowView(text: "", imageName: "") 36 | } 37 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/LocationInfoCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationInfoCardView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 06/04/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LocationInfoCardView: View { 11 | let location: Location 12 | let action: () -> Void 13 | 14 | init(location: Location, action: @escaping () -> Void) { 15 | self.location = location 16 | self.action = action 17 | } 18 | 19 | var body: some View { 20 | VStack(alignment: .leading, spacing: 10) { 21 | Text(location.name) 22 | .semiboldAppFont(size: 18) 23 | .foregroundStyle(Color(.mainText)) 24 | 25 | if let note = location.note { 26 | Text(note) 27 | .appFont(size: 15) 28 | .foregroundStyle(Color(.textGrey)) 29 | } 30 | 31 | if let url = location.webLink?.webUrl { 32 | Link("Website", destination: url) 33 | .tint(Color(.purple300)) 34 | .appFont(size: 14) 35 | } 36 | 37 | Button("Navigate", action: action) 38 | .buttonStyle(.appPrimary) 39 | .padding(.vertical, 5) 40 | } 41 | .roundBackgroundView(color: Color(.cardBackground)) 42 | } 43 | } 44 | 45 | #Preview { 46 | LocationInfoCardView(location: DummyData.location) { } 47 | } 48 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/MapPinShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapPinShape.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 06/04/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MapPinShape: Shape { 11 | func path(in rect: CGRect) -> Path { 12 | var path = Path() 13 | 14 | path.move(to: CGPoint(x: rect.midX, y: rect.maxY)) 15 | 16 | path.addQuadCurve(to: CGPoint(x: rect.midX, y: rect.minY), control: CGPoint(x: rect.minX, y: rect.minY)) 17 | 18 | path.addQuadCurve(to: CGPoint(x: rect.midX, y: rect.maxY), control: CGPoint(x: rect.maxX, y: rect.minY)) 19 | 20 | return path 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/MapPinView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapPinView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 06/04/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MapPinView: View { 11 | let imageName: String 12 | let isSelected: Bool 13 | 14 | var body: some View { 15 | ZStack(alignment: .top) { 16 | MapPinShape() 17 | .fill(isSelected ? Color(.purple300) : Color(.mapPin)) 18 | .frame(width: 70, height: 50) 19 | 20 | Image(imageName.lowercased()) 21 | .resizable() 22 | .frame(width: 20, height: 20) 23 | .foregroundStyle(isSelected ? .white : Color(.purple300)) 24 | .padding(.top, 5) 25 | } 26 | .scaleEffect(isSelected ? 1.2 : 1.0) 27 | .padding(.horizontal, -20) 28 | } 29 | } 30 | 31 | #Preview { 32 | MapPinView(imageName: "ev", isSelected: false) 33 | } 34 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/NavigationRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationRowView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 05/08/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NavigationRowView: View { 11 | let imageName: String 12 | let title: String 13 | 14 | var body: some View { 15 | HStack(spacing: 15) { 16 | Image(imageName) 17 | .tint(Color(.mainText)) 18 | 19 | Text(title) 20 | .semiboldAppFont(size: 16) 21 | .foregroundStyle(Color(.mainText)) 22 | 23 | Spacer() 24 | Image(systemName: ImageNames.chevronRight) 25 | .tint(Color(.mainText)) 26 | } 27 | .padding(4) 28 | .roundBackgroundView(color: Color(.cardBackground)) 29 | } 30 | } 31 | 32 | struct NavigationRowView_Previews: PreviewProvider { 33 | static var previews: some View { 34 | NavigationRowView(imageName: ImageNames.mapPin, title: AppStrings.sponsors) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/RemoteImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteImage.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 10/09/2022. 6 | // 7 | 8 | import Kingfisher 9 | import SwiftUI 10 | 11 | struct RemoteImageView: View { 12 | private let url: URL? 13 | 14 | init(url: URL?) { 15 | self.url = url 16 | } 17 | 18 | var body: some View { 19 | KFImage.url(url) 20 | .placeholder { 21 | Image(.placeholder) 22 | .resizable() 23 | } 24 | .resizable() 25 | .loadDiskFileSynchronously() 26 | // .cacheMemoryOnly() 27 | .fade(duration: 0.25) 28 | } 29 | } 30 | 31 | 32 | struct RemoteImageView_Previews: PreviewProvider { 33 | static var previews: some View { 34 | RemoteImageView(url: URL(string: "https://xsgames.co/randomusers/avatar.php?g=male") ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/SectionHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionHeaderView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 20/11/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SectionHeaderView: View { 11 | @Environment(\.font) private var font 12 | 13 | private let title: String 14 | 15 | init(title: String) { 16 | self.title = title 17 | } 18 | 19 | var body: some View { 20 | HStack { 21 | Text(title) 22 | .font(font) 23 | } 24 | .padding(.bottom, 8) 25 | .textCase(nil) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/SessionRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewSessionCard.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 16/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SessionRowView: View { 11 | @EnvironmentObject var baseViewModel: BaseViewModel 12 | 13 | let session: Session 14 | let showSpeakers: Bool 15 | var action: (() -> Void)? 16 | 17 | init( 18 | session: Session, 19 | showSpeakers: Bool = false, 20 | action: (() -> Void)? = nil 21 | ) { 22 | self.session = session 23 | self.showSpeakers = showSpeakers 24 | self.action = action 25 | } 26 | 27 | @ViewBuilder 28 | private func speakerRowView(speaker: Speaker) -> some View { 29 | HStack(spacing: 8) { 30 | RemoteImageView(url: speaker.imageUrl) 31 | .scaledToFit() 32 | .frame(width: 24, height: 24) 33 | .clipShape(Circle()) 34 | 35 | Text(speaker.name) 36 | .foregroundStyle(Color(.mainText)) 37 | .minimumScaleFactor(0.8) 38 | .lineLimit(2) 39 | .appFont(size: 14) 40 | } 41 | } 42 | 43 | var body: some View { 44 | VStack(alignment: .leading, spacing: 14) { 45 | 46 | HStack { 47 | Text("\(session.duration)") 48 | .appFont(size: 14) 49 | .foregroundStyle(Color(.mainText)) 50 | 51 | Spacer() 52 | 53 | if let action = action { 54 | Button(action: action) { 55 | Image(systemName: baseViewModel.isFavorite(session.id) ? ImageNames.bookmarkFill : ImageNames.bookmark) 56 | } 57 | } 58 | } 59 | 60 | Text("\(session.title)") 61 | .boldAppFont(size: 18) 62 | .lineLimit(2) 63 | .multilineTextAlignment(.leading) 64 | .foregroundStyle(Color(.mainText)) 65 | 66 | if let location = baseViewModel.getLocation(with: session.locationId) { 67 | Label("\(location.name)", image: ImageNames.location) 68 | .tint(Color(.purple300)) 69 | .capsuleBackgroundView(height: 30) 70 | } 71 | 72 | if showSpeakers && !baseViewModel.getSpeakers(with: session.speakerIds).isEmpty { 73 | HStack(spacing: 10) { 74 | ForEach(baseViewModel.getSpeakers(with: session.speakerIds)) { speaker in 75 | speakerRowView(speaker: speaker) 76 | } 77 | } 78 | } 79 | } 80 | .roundBackgroundView(color: Color(.cardBackground)) 81 | .padding(.bottom, 10) 82 | } 83 | } 84 | 85 | #Preview { 86 | SessionRowView(session: DummyData.sessions.first!) 87 | } 88 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/SpeakerCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpeakerCellView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 10/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SpeakerCardView: View { 11 | 12 | let speaker: Speaker 13 | 14 | var body: some View { 15 | VStack(spacing: 8) { 16 | RemoteImageView(url: speaker.imageUrl) 17 | .scaledToFit() 18 | .frame(width: 90, height: 90) 19 | .clipShape(Circle()) 20 | 21 | Text(speaker.name) 22 | .foregroundStyle(Color(.mainText)) 23 | .minimumScaleFactor(0.6) 24 | .lineLimit(2, reservesSpace: true) 25 | .semiboldAppFont(size: 14) 26 | } 27 | } 28 | } 29 | 30 | struct SpeakerCellView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | SpeakerCardView(speaker: DummyData.speakers[0]) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/SpeakerRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpeakerRowView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 15/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SpeakerRowView: View { 11 | let speaker: Speaker 12 | 13 | var body: some View { 14 | HStack(spacing: 12) { 15 | RemoteImageView(url: speaker.imageUrl) 16 | .scaledToFit() 17 | .frame(width: 62, height: 62) 18 | .clipShape(Circle()) 19 | 20 | VStack(alignment: .leading, spacing: 10) { 21 | Text(speaker.name) 22 | .foregroundStyle(Color(.mainText)) 23 | .minimumScaleFactor(0.8) 24 | .lineLimit(2) 25 | .boldAppFont(size: 18) 26 | 27 | if let position = speaker.currentPosition, !position.isEmpty { 28 | Text(position) 29 | .foregroundStyle(Color(.textGrey)) 30 | .minimumScaleFactor(0.8) 31 | .semiboldAppFont(size: 16) 32 | .multilineTextAlignment(.leading) 33 | } 34 | } 35 | Spacer() 36 | } 37 | .roundBackgroundView(color: Color(.cardBackground)) 38 | } 39 | } 40 | 41 | #Preview { 42 | SpeakerRowView(speaker: DummyData.speakers.first!) 43 | } 44 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/SponsorCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SponsorCard.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 07/08/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SponsorCard: View { 11 | @Environment(\.colorScheme) var colorScheme 12 | let height: CGFloat = UIDevice.current.userInterfaceIdiom == .pad ? 160 : 100 13 | 14 | let sponsor: Sponsor 15 | 16 | var body: some View { 17 | VStack { 18 | RemoteImageView(url: colorScheme == .dark ? sponsor.imageUrlDark : sponsor.imageUrlLight) 19 | .aspectRatio(contentMode: .fit) 20 | .frame(height: height) 21 | .frame(maxWidth: .infinity) 22 | .padding(.horizontal, 10) 23 | } 24 | .roundBackgroundView(color: Color(.cardBackground)) 25 | } 26 | } 27 | 28 | struct SponsorCard_Previews: PreviewProvider { 29 | static var previews: some View { 30 | SponsorCard(sponsor: DummyData.sponsors[0]) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/SponsorRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SponsorRow.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 27/10/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SponsorRow: View { 11 | @Environment(\.colorScheme) var colorScheme 12 | 13 | let sponsor: Sponsor 14 | 15 | var body: some View { 16 | VStack(alignment: .leading, spacing: 10) { 17 | RemoteImageView(url: colorScheme == .dark ? sponsor.imageUrlDark : sponsor.imageUrlLight) 18 | .aspectRatio(contentMode: .fit) 19 | .frame(height: 150) 20 | .frame(maxWidth: .infinity) 21 | .padding(.vertical, 5) 22 | .padding(.horizontal, 10) 23 | 24 | Divider() 25 | .frame(height: 2) 26 | .overlay(Color(.linkButton)) 27 | .padding(.vertical, 10) 28 | 29 | Text(sponsor.tagline) 30 | .multilineTextAlignment(.leading) 31 | .appFont(size: 18) 32 | 33 | if let url = sponsor.webUrl { 34 | Link(destination: url) { 35 | Text(sponsor.urlText) 36 | } 37 | .buttonStyle(.appPrimary) 38 | .padding(.vertical, 10) 39 | } 40 | } 41 | .roundBackgroundView(color: Color(.cardBackground)) 42 | } 43 | } 44 | 45 | struct SponsorRow_Previews: PreviewProvider { 46 | static var previews: some View { 47 | SponsorRow(sponsor: DummyData.sponsors[0]) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/TabBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 10/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TabBarView: View { 11 | 12 | var body: some View { 13 | TabView { 14 | ForEach(Tab.allCases) { tab in 15 | tab.view 16 | .tabItem { 17 | tab.icon 18 | .resizable() 19 | .aspectRatio(contentMode: .fit) 20 | } 21 | } 22 | } 23 | } 24 | } 25 | 26 | extension TabBarView { 27 | fileprivate enum Tab: CaseIterable, Identifiable { 28 | case home 29 | case mySchedule 30 | case attendee 31 | case info 32 | 33 | var id: Int { 34 | self.hashValue 35 | } 36 | 37 | var icon: Image { 38 | switch self { 39 | case .home: 40 | return Image(.home) 41 | case .mySchedule: 42 | return Image(.schedule) 43 | case .attendee: 44 | return Image(.attendee) 45 | case .info: 46 | return Image(.info) 47 | } 48 | } 49 | 50 | @ViewBuilder 51 | var view: some View { 52 | switch self { 53 | case .home: 54 | HomeScreen() 55 | case .mySchedule: 56 | MyScheduleView() 57 | case .attendee: 58 | AttendeeScreen() 59 | case .info: 60 | InfoView() 61 | } 62 | } 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /iOSDevUK/Presentation/ViewComponents/UniversityMapView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniversityMapView.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 01/04/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UniversityMapView: View { 11 | let eventDate: String? 12 | 13 | init(eventDate: String? = nil) { 14 | self.eventDate = eventDate 15 | } 16 | 17 | var body: some View { 18 | ZStack { 19 | Image(ImageNames.mapImage) 20 | .resizable() 21 | .aspectRatio(contentMode: .fill) 22 | .frame(height: eventDate != nil ? 170 : 144) 23 | .clipShape(RoundedRectangle(cornerRadius: 16)) 24 | 25 | VStack(alignment: .leading, spacing: 10) { 26 | 27 | if let eventDate { 28 | Text(eventDate) 29 | .appFont(size: 16) 30 | .foregroundStyle(Color(.buttonTitle)) 31 | .capsuleBackgroundView( 32 | height: 44, 33 | color: Color(.buttonBackground) 34 | ) 35 | } 36 | 37 | HStack(spacing: 10) { 38 | Image(ImageResource.mapPin) 39 | .resizable() 40 | .aspectRatio(contentMode: .fit) 41 | .frame(height: 28) 42 | .foregroundStyle(Color(.buttonBackground)) 43 | 44 | Text("Aberystwyth University") 45 | .semiboldAppFont(size: 20) 46 | .foregroundStyle(Color(.mainText)) 47 | 48 | Spacer() 49 | } 50 | .roundBackgroundView(color: Color(.buttonTitle)) 51 | } 52 | .padding(.horizontal, 20) 53 | .padding(.top, 2) 54 | } 55 | } 56 | } 57 | 58 | #Preview { 59 | UniversityMapView(eventDate: nil) 60 | } 61 | -------------------------------------------------------------------------------- /iOSDevUK/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iOSDevUK/Resources/Fonts/SFPRODISPLAYBLACKITALIC.OTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Resources/Fonts/SFPRODISPLAYBLACKITALIC.OTF -------------------------------------------------------------------------------- /iOSDevUK/Resources/Fonts/SFPRODISPLAYBOLD.OTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Resources/Fonts/SFPRODISPLAYBOLD.OTF -------------------------------------------------------------------------------- /iOSDevUK/Resources/Fonts/SFPRODISPLAYHEAVYITALIC.OTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Resources/Fonts/SFPRODISPLAYHEAVYITALIC.OTF -------------------------------------------------------------------------------- /iOSDevUK/Resources/Fonts/SFPRODISPLAYLIGHTITALIC.OTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Resources/Fonts/SFPRODISPLAYLIGHTITALIC.OTF -------------------------------------------------------------------------------- /iOSDevUK/Resources/Fonts/SFPRODISPLAYMEDIUM.OTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Resources/Fonts/SFPRODISPLAYMEDIUM.OTF -------------------------------------------------------------------------------- /iOSDevUK/Resources/Fonts/SFPRODISPLAYREGULAR.OTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Resources/Fonts/SFPRODISPLAYREGULAR.OTF -------------------------------------------------------------------------------- /iOSDevUK/Resources/Fonts/SFPRODISPLAYSEMIBOLDITALIC.OTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Resources/Fonts/SFPRODISPLAYSEMIBOLDITALIC.OTF -------------------------------------------------------------------------------- /iOSDevUK/Resources/Fonts/SFPRODISPLAYTHINITALIC.OTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Resources/Fonts/SFPRODISPLAYTHINITALIC.OTF -------------------------------------------------------------------------------- /iOSDevUK/Resources/Fonts/SFPRODISPLAYULTRALIGHTITALIC.OTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Resources/Fonts/SFPRODISPLAYULTRALIGHTITALIC.OTF -------------------------------------------------------------------------------- /iOSDevUK/Resources/Fonts/SFProDisplay-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Resources/Fonts/SFProDisplay-Light.ttf -------------------------------------------------------------------------------- /iOSDevUK/Resources/Fonts/SFProDisplay-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidFaraday/iOSDevUK/4333fe30e3c587f5330089527fcaaeecfe55b471/iOSDevUK/Resources/Fonts/SFProDisplay-Semibold.ttf -------------------------------------------------------------------------------- /iOSDevUK/Utils/LocalStorageService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalStorageService.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 12/08/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol LocalStorageServiceProtocol { 11 | func save(items: [String], for key: String) 12 | func loadArray(with key: String) -> [String] 13 | } 14 | 15 | final class LocalStorageService: LocalStorageServiceProtocol { 16 | 17 | static let shared = LocalStorageService() 18 | 19 | private init() {} 20 | 21 | func save(items: [String], for key: String) { 22 | UserDefaults.standard.set(items, forKey: key) 23 | } 24 | 25 | func loadArray(with key: String) -> [String] { 26 | UserDefaults.standard.stringArray(forKey: key) ?? [] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /iOSDevUK/Utils/LocationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationManager.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 25/03/2023. 6 | // 7 | 8 | import MapKit 9 | 10 | enum MapDetails { 11 | static let startingLatitude: CLLocationDegrees = 52.41483885670968 12 | static let startingLongitude: CLLocationDegrees = -4.076185527558135 13 | static let defaultLocationAberystwyth = CLLocation(latitude: startingLatitude, longitude: startingLongitude) 14 | static let aberystwythCoordinates = defaultLocationAberystwyth.coordinate 15 | static let defaultSpan = MKCoordinateSpan.init(latitudeDelta: 0.025, longitudeDelta: 0.025) 16 | } 17 | 18 | class LocationService: NSObject, CLLocationManagerDelegate, ObservableObject { 19 | 20 | @Published var region = MKCoordinateRegion(center: MapDetails.aberystwythCoordinates, 21 | span: MapDetails.defaultSpan) 22 | 23 | @Published var currentLocation = MapDetails.aberystwythCoordinates 24 | 25 | static let shared = LocationService() 26 | 27 | var locationManager: CLLocationManager? 28 | 29 | 30 | private override init() { 31 | super.init() 32 | requestLocationAccess() 33 | } 34 | 35 | func requestLocationAccess() { 36 | 37 | if locationManager == nil { 38 | locationManager = CLLocationManager() 39 | locationManager!.delegate = self 40 | locationManager!.desiredAccuracy = kCLLocationAccuracyBest 41 | locationManager!.requestWhenInUseAuthorization() 42 | 43 | locationManager!.activityType = .other 44 | locationManager!.pausesLocationUpdatesAutomatically = true 45 | 46 | locationManager!.startMonitoringSignificantLocationChanges() 47 | } 48 | } 49 | 50 | static func region(for coordinates: [CLLocationCoordinate2D]) -> MKCoordinateRegion { 51 | 52 | var rect = MKMapRect.null 53 | 54 | for coordinate in coordinates { 55 | 56 | let point = MKMapPoint(coordinate) 57 | rect = rect.union(MKMapRect(x: point.x, y: point.y, width: 0, height: 0)) 58 | } 59 | 60 | var region = MKCoordinateRegion(rect) 61 | region.span.latitudeDelta *= 1.5 62 | region.span.longitudeDelta *= 1.5 63 | 64 | return region 65 | } 66 | 67 | 68 | //MARK: - Delegates 69 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 70 | print("DEBUG: Failed to get location") 71 | } 72 | 73 | 74 | func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { 75 | guard let manager = locationManager else { return } 76 | 77 | if manager.authorizationStatus == .notDetermined { 78 | manager.requestWhenInUseAuthorization() 79 | } 80 | } 81 | 82 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 83 | print("DEBUG: Updated location") 84 | currentLocation = locations.last!.coordinate 85 | region = MKCoordinateRegion(center: currentLocation, span: MapDetails.defaultSpan) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /iOSDevUK/Utils/MappingUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MappingUtils.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 02/04/2023. 6 | // 7 | 8 | import Foundation 9 | import WeatherKit 10 | 11 | protocol MappingUtilsProtocol { 12 | func convert(input: HourWeather) -> WeatherData 13 | func convert(input: CurrentWeather?) -> WeatherData? 14 | } 15 | 16 | 17 | class MappingUtils: MappingUtilsProtocol { 18 | 19 | static let shared = MappingUtils() 20 | 21 | private init() {} 22 | 23 | func convert(input: HourWeather) -> WeatherData { 24 | 25 | WeatherData( 26 | tempDate: input.date, 27 | condition: input.condition.description, 28 | symbolName: input.symbolName, 29 | currentTempC: input.temperature.value, 30 | feelsLikeC: input.apparentTemperature.value 31 | ) 32 | } 33 | func convert(input: CurrentWeather?) -> WeatherData? { 34 | guard let input = input else { return nil } 35 | 36 | return WeatherData( 37 | tempDate: input.date, 38 | condition: input.condition.description, 39 | symbolName: input.symbolName, 40 | currentTempC: input.temperature.value, 41 | feelsLikeC: input.apparentTemperature.value 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /iOSDevUK/Utils/Reachability.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reachability.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 06/02/2023. 6 | // 7 | 8 | import Foundation 9 | import SystemConfiguration 10 | 11 | 12 | public class Reachability { 13 | 14 | class func HasConnection() -> Bool { 15 | 16 | var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) 17 | 18 | zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) 19 | zeroAddress.sin_family = sa_family_t(AF_INET) 20 | 21 | let defaultRoutReachability = withUnsafePointer(to: &zeroAddress, { 22 | $0.withMemoryRebound(to: sockaddr.self, capacity: 1, { 23 | zeroSockAddress in 24 | 25 | SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress) 26 | }) 27 | }) 28 | 29 | var flags : SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0) 30 | 31 | if SCNetworkReachabilityGetFlags(defaultRoutReachability!, &flags) == false { 32 | return false 33 | } 34 | 35 | let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0 36 | 37 | let needConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0 38 | 39 | return (isReachable && !needConnection) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /iOSDevUK/iOSDevUK.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.weatherkit 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iOSDevUK/iOSDevUKApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iOSDevUKApp.swift 3 | // iOSDevUK 4 | // 5 | // Created by David Kababyan on 10/09/2022. 6 | // 7 | 8 | import SwiftUI 9 | import Firebase 10 | 11 | @main 12 | struct iOSDevUKApp: App { 13 | 14 | init() { 15 | FirebaseApp.configure() 16 | } 17 | 18 | @State private var router = NavigationRouter() 19 | @State private var baseViewModel = BaseViewModel() 20 | 21 | var body: some Scene { 22 | WindowGroup { 23 | TabBarView() 24 | .environmentObject(router) 25 | .environmentObject(baseViewModel) 26 | .environmentObject(LocationService.shared) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /iOSDevUKTests/AllSessionsViewModelTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllSessionsViewModelTest.swift 3 | // iOSDevUKTests 4 | // 5 | // Created by David Kababyan on 08/12/2022. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | 11 | @testable import iOSDevUK 12 | 13 | final class AllSessionsViewModelTest: XCTestCase { 14 | 15 | private var cancellables: Set = [] 16 | 17 | func test_InitSetsSessions_ToTwo() { 18 | let sut = AllSessionsViewModel(sessions: DummyData.sessions) 19 | 20 | sut.$sessions 21 | .sink { newValue in 22 | XCTAssertEqual(newValue.count, 2) 23 | } 24 | .store(in: &cancellables) 25 | } 26 | 27 | 28 | @MainActor 29 | func test_SelectedDateGetsValue() { 30 | let sut = AllSessionsViewModel(sessions: DummyData.sessions) 31 | 32 | sut.$selectedDate 33 | .dropFirst() 34 | .sink { newValue in 35 | XCTAssertNotEqual(newValue, "") 36 | } 37 | .store(in: &cancellables) 38 | 39 | sut.setCurrentDate() 40 | } 41 | 42 | @MainActor 43 | func test_SelectedDateSetToToday() { 44 | let sut = AllSessionsViewModel(sessions: [DummyData.sessions[1]]) 45 | 46 | sut.$selectedDate 47 | .dropFirst() 48 | .sink { newValue in 49 | XCTAssertEqual(newValue, Date().dateAndWeekDay) 50 | } 51 | .store(in: &cancellables) 52 | 53 | sut.setCurrentDate() 54 | } 55 | 56 | @MainActor 57 | func test_SelectedDateSetTo10Thu() { 58 | let sut = AllSessionsViewModel(sessions: [DummyData.sessions[0]]) 59 | 60 | sut.$selectedDate 61 | .dropFirst() 62 | .sink { newValue in 63 | XCTAssertEqual(newValue, "10 Thu") 64 | } 65 | .store(in: &cancellables) 66 | 67 | sut.setCurrentDate() 68 | } 69 | 70 | @MainActor 71 | func test_SetCurrentDateWithEmptySessionReturns() { 72 | let sut = AllSessionsViewModel(sessions: []) 73 | 74 | sut.$selectedDate 75 | .sink { newValue in 76 | XCTAssertEqual(newValue, "") 77 | } 78 | .store(in: &cancellables) 79 | 80 | sut.setCurrentDate() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /iOSDevUKTests/FactorySetup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FactorySetup.swift 3 | // iOSDevUKTests 4 | // 5 | // Created by David Kababyan on 08/12/2022. 6 | // 7 | 8 | import Foundation 9 | import Factory 10 | @testable import iOSDevUK 11 | 12 | extension Container { 13 | static func setupMocks(objectsToReturn: [Codable], shouldReturnError: Bool = false) { 14 | Container.shared.firebaseRepository.register { MockFirebaseRepository(objectsToReturn: objectsToReturn, shouldReturnError: shouldReturnError) } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /iOSDevUKTests/SessionRowViewModelTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionRowViewModelTest.swift 3 | // iOSDevUKTests 4 | // 5 | // Created by David Kababyan on 10/12/2022. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | import Factory 11 | 12 | @testable import iOSDevUK 13 | 14 | final class SessionRowViewModelTest: XCTestCase { 15 | 16 | private var cancellables: Set = [] 17 | 18 | override func tearDownWithError() throws { 19 | cancellables = [] 20 | } 21 | 22 | func test_InitialPropertyValuesAreNil() { 23 | Container.setupMocks(objectsToReturn: [DummyData.location]) 24 | 25 | let sut = SessionRowViewModel() 26 | 27 | XCTAssertNil(sut.location) 28 | XCTAssertNil(sut.fetchError) 29 | } 30 | 31 | 32 | func test_fetchLocationWithoutId() { 33 | Container.setupMocks(objectsToReturn: [DummyData.location]) 34 | 35 | let sut = SessionRowViewModel() 36 | 37 | Task { 38 | await sut.fetchLocation(with: nil) 39 | } 40 | 41 | XCTAssertNil(sut.location) 42 | } 43 | 44 | 45 | func test_fetchLocationReturnsLocation() async { 46 | Container.setupMocks(objectsToReturn: [DummyData.location]) 47 | 48 | let sut = SessionRowViewModel() 49 | await sut.fetchLocation(with: "TestLocation123") 50 | 51 | 52 | sut.$location 53 | .dropFirst() 54 | .sink { newValue in 55 | XCTAssertNotNil(newValue) 56 | XCTAssertEqual(newValue?.id, "TestLocation123") 57 | } 58 | .store(in: &cancellables) 59 | } 60 | 61 | 62 | func test_fetchLocationReturnsError() async { 63 | Container.setupMocks(objectsToReturn: [DummyData.location], shouldReturnError: true) 64 | 65 | let sut = SessionRowViewModel() 66 | await sut.fetchLocation(with: "TestLocation123") 67 | 68 | sut.$fetchError 69 | .dropFirst() 70 | .sink { newValue in 71 | XCTAssertNotNil(newValue) 72 | XCTAssertEqual(newValue as? AppError, AppError.badSnapshot) 73 | } 74 | .store(in: &cancellables) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /iOSDevUKTests/SpeakerDetailViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpeakerDetailViewModelTests.swift 3 | // iOSDevUKTests 4 | // 5 | // Created by David Kababyan on 08/12/2022. 6 | // 7 | 8 | import XCTest 9 | import Factory 10 | import Combine 11 | 12 | @testable import iOSDevUK 13 | 14 | final class SpeakerDetailViewModelTests: XCTestCase { 15 | 16 | private var cancellables: Set = [] 17 | 18 | override func setUpWithError() throws { 19 | Container.setupMocks(objectsToReturn: DummyData.sessions) 20 | } 21 | 22 | override func tearDownWithError() throws { 23 | cancellables = [] 24 | } 25 | 26 | func test_FetchSpeakerSessions_ReturnTwo() { 27 | let sut = SpeakerDetailViewModel(speaker: DummyData.speakers[0]) 28 | 29 | let expectation = expectation(description: "waiting for network call") 30 | 31 | sut.$sessions 32 | .dropFirst() 33 | .sink { newValue in 34 | XCTAssertEqual(newValue.count, 2) 35 | expectation.fulfill() 36 | } 37 | .store(in: &cancellables) 38 | 39 | Task { 40 | await sut.getSpeakerSessions() 41 | } 42 | 43 | waitForExpectations(timeout: 0.1) 44 | } 45 | } 46 | --------------------------------------------------------------------------------