diff --git a/README.md b/README.md new file mode 100644 index 0000000..56c4fce --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Deepa - iOSConfSG app in SwiftUI edition + +Hi, Deepa is a work in progress to rewrite the iOS Conf SG app, slated to be used in our upcoming conference: 12-13 January 2023 +For more info about the conference, visit https://iosconf.sg + +## Contributions + +The following contributions are accepted: + +1. Show the conference Schedule for both days +2. Handle a talk detail +3. Handle a feedback submission +3. Show the workshop schedule + +## Build the app + +You need at least, Xcode 14 to build the app locally. It has been setup with Apollo Graphql iOS library to fetch the Schedule using a subscription + diff --git a/deepa.xcodeproj/project.pbxproj b/deepa.xcodeproj/project.pbxproj index 38f5cd5..d2c4445 100644 --- a/deepa.xcodeproj/project.pbxproj +++ b/deepa.xcodeproj/project.pbxproj @@ -25,6 +25,10 @@ 5A936E6328D5AECA00C6065B /* WorkshopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A936E6228D5AECA00C6065B /* WorkshopView.swift */; }; 5A936E6828D5AF2900C6065B /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A936E6728D5AF2900C6065B /* InfoView.swift */; }; 5A936E6A28D5AF4100C6065B /* NewsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A936E6928D5AF4100C6065B /* NewsView.swift */; }; + 5A936E6D28D6144200C6065B /* Talk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A936E6C28D6144200C6065B /* Talk.swift */; }; + 5A936E6F28D6148600C6065B /* Speaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A936E6E28D6148600C6065B /* Speaker.swift */; }; + 5A936E7228D6234A00C6065B /* Atlantis in Frameworks */ = {isa = PBXBuildFile; productRef = 5A936E7128D6234A00C6065B /* Atlantis */; }; + 972E5C1C2913D0AF0021AEF2 /* WorkshopDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 972E5C1B2913D0AF0021AEF2 /* WorkshopDetailsView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -65,6 +69,10 @@ 5A936E6228D5AECA00C6065B /* WorkshopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopView.swift; sourceTree = ""; }; 5A936E6728D5AF2900C6065B /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = ""; }; 5A936E6928D5AF4100C6065B /* NewsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsView.swift; sourceTree = ""; }; + 5A936E6C28D6144200C6065B /* Talk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Talk.swift; sourceTree = ""; }; + 5A936E6E28D6148600C6065B /* Speaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Speaker.swift; sourceTree = ""; }; + 5A936E7328D623F500C6065B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 972E5C1B2913D0AF0021AEF2 /* WorkshopDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkshopDetailsView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -73,6 +81,7 @@ buildActionMask = 2147483647; files = ( 5A936E5928D5A8B200C6065B /* Apollo in Frameworks */, + 5A936E7228D6234A00C6065B /* Atlantis in Frameworks */, 5A936E5B28D5A8B200C6065B /* ApolloWebSocket in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -117,11 +126,13 @@ 5A936E1C28D377C200C6065B /* deepa */ = { isa = PBXGroup; children = ( + 5A936E7328D623F500C6065B /* Info.plist */, 5A936E4E28D5961500C6065B /* schema.json */, 5A936E5028D5967300C6065B /* api.graphql */, 5A936E5228D596D300C6065B /* API.swift */, 5A936E1D28D377C200C6065B /* deepaApp.swift */, 5A936E1F28D377C200C6065B /* ContentView.swift */, + 5A936E6B28D6143100C6065B /* Models */, 5A936E5C28D5ADC400C6065B /* Schedule */, 5A936E6128D5AEB100C6065B /* Workshop */, 5A936E6428D5AEED00C6065B /* News */, @@ -172,6 +183,7 @@ children = ( 5A936E5D28D5ADDC00C6065B /* ScheduleView.swift */, 5A936E5F28D5ADE700C6065B /* ScheduleViewModel.swift */, + 972E5C1B2913D0AF0021AEF2 /* WorkshopDetailsView.swift */, ); path = Schedule; sourceTree = ""; @@ -200,6 +212,15 @@ path = Info; sourceTree = ""; }; + 5A936E6B28D6143100C6065B /* Models */ = { + isa = PBXGroup; + children = ( + 5A936E6C28D6144200C6065B /* Talk.swift */, + 5A936E6E28D6148600C6065B /* Speaker.swift */, + ); + path = Models; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -220,6 +241,7 @@ packageProductDependencies = ( 5A936E5828D5A8B200C6065B /* Apollo */, 5A936E5A28D5A8B200C6065B /* ApolloWebSocket */, + 5A936E7128D6234A00C6065B /* Atlantis */, ); productName = deepa; productReference = 5A936E1A28D377C200C6065B /* deepa.app */; @@ -295,6 +317,7 @@ mainGroup = 5A936E1128D377C200C6065B; packageReferences = ( 5A936E5728D5A8B200C6065B /* XCRemoteSwiftPackageReference "apollo-ios" */, + 5A936E7028D6234A00C6065B /* XCRemoteSwiftPackageReference "atlantis" */, ); productRefGroup = 5A936E1B28D377C200C6065B /* Products */; projectDirPath = ""; @@ -363,8 +386,11 @@ files = ( 5A936E6828D5AF2900C6065B /* InfoView.swift in Sources */, 5A936E5E28D5ADDC00C6065B /* ScheduleView.swift in Sources */, + 972E5C1C2913D0AF0021AEF2 /* WorkshopDetailsView.swift in Sources */, 5A936E5628D5974900C6065B /* NetworkManager.swift in Sources */, + 5A936E6D28D6144200C6065B /* Talk.swift in Sources */, 5A936E6A28D5AF4100C6065B /* NewsView.swift in Sources */, + 5A936E6F28D6148600C6065B /* Speaker.swift in Sources */, 5A936E6028D5ADE700C6065B /* ScheduleViewModel.swift in Sources */, 5A936E6328D5AECA00C6065B /* WorkshopView.swift in Sources */, 5A936E2028D377C200C6065B /* ContentView.swift in Sources */, @@ -456,6 +482,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -508,6 +536,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; @@ -524,10 +554,12 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"deepa/Preview Content\""; - DEVELOPMENT_TEAM = X8ZFLUE894; + DEVELOPMENT_TEAM = Z7X6647APW; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = deepa/Info.plist; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Atlantis would use Bonjour Service to discover Proxyman app from your local network."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -543,7 +575,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 12.3; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.niveumlabs.deepa; + PRODUCT_BUNDLE_IDENTIFIER = com.donchia.deepa; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -562,10 +594,12 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"deepa/Preview Content\""; - DEVELOPMENT_TEAM = X8ZFLUE894; + DEVELOPMENT_TEAM = Z7X6647APW; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = deepa/Info.plist; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Atlantis would use Bonjour Service to discover Proxyman app from your local network."; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -581,7 +615,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 12.3; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.niveumlabs.deepa; + PRODUCT_BUNDLE_IDENTIFIER = com.donchia.deepa; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; @@ -731,6 +765,14 @@ minimumVersion = 0.53.0; }; }; + 5A936E7028D6234A00C6065B /* XCRemoteSwiftPackageReference "atlantis" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ProxymanApp/atlantis"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.18.2; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -744,6 +786,11 @@ package = 5A936E5728D5A8B200C6065B /* XCRemoteSwiftPackageReference "apollo-ios" */; productName = ApolloWebSocket; }; + 5A936E7128D6234A00C6065B /* Atlantis */ = { + isa = XCSwiftPackageProductDependency; + package = 5A936E7028D6234A00C6065B /* XCRemoteSwiftPackageReference "atlantis" */; + productName = Atlantis; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 5A936E1228D377C200C6065B /* Project object */; diff --git a/deepa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/deepa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 079e7a9..7f79366 100644 --- a/deepa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/deepa.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,6 +9,15 @@ "version" : "0.53.0" } }, + { + "identity" : "atlantis", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ProxymanApp/atlantis", + "state" : { + "revision" : "2b26bf7fe2c5751335dca015b9e77514210d28d4", + "version" : "1.18.2" + } + }, { "identity" : "sqlite.swift", "kind" : "remoteSourceControl", diff --git a/deepa/Assets.xcassets/AccentColor.colorset/Contents.json b/deepa/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..d90b065 100644 --- a/deepa/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/deepa/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,10 @@ { "colors" : [ { + "color" : { + "platform" : "universal", + "reference" : "systemOrangeColor" + }, "idiom" : "universal" } ], diff --git a/deepa/ContentView.swift b/deepa/ContentView.swift index 69fd65e..5399806 100644 --- a/deepa/ContentView.swift +++ b/deepa/ContentView.swift @@ -16,7 +16,7 @@ struct ContentView: View { } WorkshopView() .tabItem { - Label("Workshop", systemImage: "laptopcomputer") + Label("Workshop", systemImage: "swift") } NewsView() .tabItem { diff --git a/deepa/Helpers/NetworkManager.swift b/deepa/Helpers/NetworkManager.swift index 5c2fd3f..073373c 100644 --- a/deepa/Helpers/NetworkManager.swift +++ b/deepa/Helpers/NetworkManager.swift @@ -13,29 +13,32 @@ class NetworkManager { static let shared = NetworkManager() let httpsEndpoint = "https://iosconfsg.herokuapp.com/v1/graphql" let wsEndpoint = "ws://iosconfsg.herokuapp.com/v1/graphql" - var apolloClient: ApolloClient? - private init() { - createApolloClient() - } + /// A web socket transport to use for subscriptions + private lazy var webSocketTransport: WebSocketTransport = { + let url = URL(string: wsEndpoint)! + let webSocketClient = WebSocket(url: url, protocol: .graphql_transport_ws) + return WebSocketTransport(websocket: webSocketClient) + }() - func createApolloClient() { - self.apolloClient = { - guard let wsEndpointUrl = URL(string: wsEndpoint) else { return nil } - guard let httpsEndpointUrl = URL(string: httpsEndpoint) else { return nil} - - let request = URLRequest(url: wsEndpointUrl) - let websocket = WebSocket(request: request, protocol: .graphql_transport_ws) - let websocketTransport = WebSocketTransport(websocket: websocket) - - let store = ApolloStore(cache: InMemoryNormalizedCache()) - - let provider = DefaultInterceptorProvider(store: store) - let httpNetworkTransport = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: httpsEndpointUrl) - let splitNetworkTransport = SplitNetworkTransport(uploadingNetworkTransport: httpNetworkTransport, webSocketNetworkTransport: websocketTransport) - - return ApolloClient(networkTransport: splitNetworkTransport, store: store) - - }() - } + /// An HTTP transport to use for queries and mutations + + private lazy var normalTransport: RequestChainNetworkTransport = { + let url = URL(string: httpsEndpoint)! + return RequestChainNetworkTransport(interceptorProvider: DefaultInterceptorProvider(store: self.store), endpointURL: url) + }() + + + /// A split network transport to allow the use of both of the above + /// transports through a single `NetworkTransport` instance. + private lazy var splitNetworkTransport = SplitNetworkTransport( + uploadingNetworkTransport: self.normalTransport, + webSocketNetworkTransport: self.webSocketTransport + ) + + /// Create a client using the `SplitNetworkTransport`. + private(set) lazy var client = ApolloClient(networkTransport: self.splitNetworkTransport, store: self.store) + + /// A common store to use for `normalTransport` and `client`. + private lazy var store = ApolloStore() } diff --git a/deepa/Info.plist b/deepa/Info.plist new file mode 100644 index 0000000..2d8b719 --- /dev/null +++ b/deepa/Info.plist @@ -0,0 +1,12 @@ + + + + +NSLocalNetworkUsageDescription +Atlantis would use Bonjour Service to discover Proxyman app from your local network. +NSBonjourServices + + _Proxyman._tcp + + + diff --git a/deepa/Info/InfoView.swift b/deepa/Info/InfoView.swift index 063d34a..8610896 100644 --- a/deepa/Info/InfoView.swift +++ b/deepa/Info/InfoView.swift @@ -6,15 +6,54 @@ // import SwiftUI +import SafariServices -struct InfoView: View { - var body: some View { - Text("Info") +struct AboutMenu: Identifiable { + var id = UUID() + var title: String + var link: String +} + +struct SFSafariViewWrapper: UIViewControllerRepresentable { + let url: URL + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> SFSafariViewController { + return SFSafariViewController(url: url) + } + + func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) { + return } } -struct InfoView_Previews: PreviewProvider { - static var previews: some View { - InfoView() +struct InfoView: View { + @State var menuList: Array = [ + AboutMenu(title: "Code of Conduct", link: "https://iosconf.sg/coc"), + AboutMenu(title: "Sponsors", link: "https://iosconf.sg/#sponsors"), + AboutMenu(title: "Software", link: "https://iosconf.sg/software"), + AboutMenu(title: "FAQ", link: "https://iosconf.sg/faq")] + @State private var showSafari: Bool = false + + var body: some View { + NavigationView { + List { + Section { + ForEach(menuList) { menu in + Button(action: { + showSafari.toggle() + }) { + Text("\(menu.title)") + }.fullScreenCover(isPresented: $showSafari, content: { + SFSafariViewWrapper(url: URL(string: "\(menu.link)")!).ignoresSafeArea(.all) + }) + } + } + Section { + Link("Open iOSConfSG Slack", destination: URL(string: "www.slack.com")!) + } + } + .listStyle(.grouped) + .navigationTitle("About") + } } } diff --git a/deepa/Models/Speaker.swift b/deepa/Models/Speaker.swift new file mode 100644 index 0000000..90c3cd3 --- /dev/null +++ b/deepa/Models/Speaker.swift @@ -0,0 +1,20 @@ +// +// Speaker.swift +// deepa +// +// Created by Vina Melody on 17/9/22. +// + +import Foundation + +struct Speaker: Identifiable { + var id: Int + var name: String + var shortBio: String? + var twitter: String? + var linkedIn: String? + var company: String? + var companyUrl: String? + var imageUrl: String? + var imageFilename: String? +} diff --git a/deepa/Models/Talk.swift b/deepa/Models/Talk.swift new file mode 100644 index 0000000..54fb7e1 --- /dev/null +++ b/deepa/Models/Talk.swift @@ -0,0 +1,35 @@ +// +// Talk.swift +// deepa +// +// Created by Vina Melody on 17/9/22. +// + +import Foundation + +public enum TalkType: String { + case registration + case openingAddress + case closingAddress + case shortbreak + case lunch + case normalTalk + case lightningTalk + case afterparty + case workshop + case groupPhoto + case quiz + case energyboost + case combinedTalk +} + +struct Talk: Identifiable { + var id: Int + var title: String + var talkType: TalkType + var startAt: Date? + var endAt: Date? + var talkDescription: String? + var activityName: String? + var speakers: [Speaker] +} diff --git a/deepa/News/NewsView.swift b/deepa/News/NewsView.swift index 0c28b17..8f30f98 100644 --- a/deepa/News/NewsView.swift +++ b/deepa/News/NewsView.swift @@ -6,15 +6,30 @@ // import SwiftUI +import WebKit -struct NewsView: View { - var body: some View { - Text("News") +struct WebView: UIViewRepresentable { + + let headerString = "
" + + private let webContent = """ + + """ + + func makeUIView(context: Context) -> WKWebView { + return WKWebView() + } + + func updateUIView(_ webView: WKWebView, context: Context) { + webView.loadHTMLString(headerString + webContent, baseURL: nil) } } -struct NewsView_Previews: PreviewProvider { - static var previews: some View { - NewsView() +struct NewsView: View { + var body: some View { + NavigationView { + WebView() + .navigationTitle("News") + } } } diff --git a/deepa/Schedule/ScheduleView.swift b/deepa/Schedule/ScheduleView.swift index 3244b50..dde5dab 100644 --- a/deepa/Schedule/ScheduleView.swift +++ b/deepa/Schedule/ScheduleView.swift @@ -6,21 +6,148 @@ // import SwiftUI +import Foundation struct ScheduleView: View { + + @StateObject var viewModel: ScheduleViewModel + + init() { + let viewModel = ScheduleViewModel { + print("fail init") + } + _viewModel = StateObject(wrappedValue: viewModel) + + } + + static let extractDate: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "dd/MM/yyyy" + return formatter + }() + + @State private var schedulePicker = 0 + + var body: some View { + NavigationView { + VStack { + + if schedulePicker == 0 { + List { + ForEach(viewModel.schedule) { information in + if Self.extractDate.string(from: information.startAt ?? Date.now) == "20/01/2022" { + VStack { + NavigationLink(destination: WorkshopDetailsView(title: information.title, startAt: information.startAt ?? Date.now, endAt: information.endAt ?? Date.now, talkDescription: information.talkDescription ?? "", activityName: information.activityName ?? "", speakers: information.speakers)) { + ListContent(title: information.title, speakers: information.speakers, startAt: information.startAt ?? Date.now, endAt: information.endAt ?? Date.now) + } + } + } + } + }.listStyle(.plain) + } else { + List { + ForEach(viewModel.schedule) { information in + if Self.extractDate.string(from: information.startAt ?? Date.now) == "21/01/2022" { + VStack { + NavigationLink(destination: WorkshopDetailsView(title: information.title, startAt: information.startAt ?? Date.now, endAt: information.endAt ?? Date.now, talkDescription: information.talkDescription ?? "", activityName: information.activityName ?? "", speakers: information.speakers)) { + ListContent(title: information.title, speakers: information.speakers, startAt: information.startAt ?? Date.now, endAt: information.endAt ?? Date.now) + } + } + } + } + }.listStyle(.plain) + } + + Spacer() + + }.navigationTitle("Schedule") + .toolbar { + ToolbarItem(placement: .automatic) { + Picker("Schedule", selection: $schedulePicker) { + Text("Day 1").tag(0) + Text("Day 2").tag(1) + } + .pickerStyle(.segmented) + } + } + } + .onAppear { + viewModel.fetchSchedule() + } + } + + func handleGraphqlError() { + + } +} + +struct ListContent: View { + @State var title: String + @State var speakers: Array + @State var startAt: Date + @State var endAt: Date + + static let dateFormat: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "E, MMM dd HH:mm" + return formatter + }() + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Schedule!") + HStack { + SpeakersImage(speakers: speakers) + + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.title3) + .fontWeight(.bold) + .foregroundColor(.orange) + + if speakers.count == 1 { + Text(speakers[0].name) + .font(.subheadline) + } else { + Text(concatSpeakerName(speakers: speakers, requireCompany: false)) + .font(.subheadline) + } + + HStack(spacing: 0) { + Text(startAt, formatter: Self.dateFormat) + Text(" - ") + Text(endAt, formatter: Self.dateFormat) + }.font(.caption) + .fontWeight(.light) + } } - .padding() } } -struct ScheduleView_Previews: PreviewProvider { - static var previews: some View { - ScheduleView() +extension View { + func concatSpeakerName(speakers: Array, requireCompany: Bool) -> String { + var speakersList: Array = [] + if requireCompany { + for speaker in speakers { + speakersList.append("\(speaker.name) (\(speaker.company ?? ""))") + } + } else { + for speaker in speakers { + speakersList.append(speaker.name) + } + } + let speakers = speakersList.joined(separator: " & ") + return speakers + } + + func getURL2022(speakerImageURL: String) -> String { + if speakerImageURL.count > 0 { + var url = speakerImageURL + for (index, char) in "2022.".enumerated() { + var i = url.index(url.startIndex, offsetBy: index+8) + url.insert(char, at: i) + } + return url + } else { + return "" + } } } diff --git a/deepa/Schedule/ScheduleViewModel.swift b/deepa/Schedule/ScheduleViewModel.swift index 19f009f..861c04d 100644 --- a/deepa/Schedule/ScheduleViewModel.swift +++ b/deepa/Schedule/ScheduleViewModel.swift @@ -6,3 +6,93 @@ // import Foundation +import Apollo + +class ScheduleViewModel: ObservableObject { + private var apollo: ApolloClient! + private var subscription: Cancellable? + private var graphql: [GetScheduleSubscription.Data.Schedule] = [] + @Published var schedule: [Talk] = [] + + let dateFormatter: DateFormatter = { + let df = DateFormatter() + df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + return df + }() + + init(failInitClosure: @escaping (()-> Void)) { +// guard let connection = NetworkManager.shared.client else { +// failInitClosure() +// return +// } + self.apollo = NetworkManager.shared.client + } + + func fetchSchedule() { + subscription = apollo.subscribe(subscription: GetScheduleSubscription(), resultHandler: { [weak self] result in + switch result { + case .success(let graphqlResult): + self?.createSchedule(from: graphqlResult) + case .failure(let error): + self?.handleError(error) + } + }) + } + + func createSchedule(from result: GraphQLResult) { + + guard let data = result.data else { return } + let rawSchedule = data.schedule + + if !self.schedule.isEmpty { + self.schedule.removeAll() + } + + for item in rawSchedule { + guard let id = item.id, + let title = item.title, + let type = item.talkType, + let talkType = TalkType(rawValue: type) else { + return + } + + var speakers: [Speaker] = [] + + if !item.speakers.isEmpty { + speakers = createSpeakers(from: item) + } + + let talk = Talk(id: id, + title: title, + talkType: talkType, + startAt: dateFormatter.date(from: item.startAt ?? ""), + endAt: dateFormatter.date(from: item.endAt ?? ""), + talkDescription: item.talkDescription, + activityName: item.activity ?? "", + speakers: speakers) + self.schedule.append(talk) + + print(self.schedule) + } + } + + func handleError(_ error: Error) { + print("Error \(error)") + } + + func createSpeakers(from graphqlTalk: GetScheduleSubscription.Data.Schedule) -> [Speaker] { + let speakers = graphqlTalk.speakers.map { speaker -> Speaker in + return Speaker( + id: speaker.id ?? 1, + name: speaker.name ?? "", + shortBio: speaker.shortBio, + twitter: speaker.twitter, + linkedIn: speaker.linkedinUrl, + company: speaker.company, + companyUrl: speaker.companyUrl, + imageUrl: speaker.imageUrl, + imageFilename: speaker.imageFilename) + } + return speakers + } +} diff --git a/deepa/Schedule/WorkshopDetailsView.swift b/deepa/Schedule/WorkshopDetailsView.swift new file mode 100644 index 0000000..1d8554e --- /dev/null +++ b/deepa/Schedule/WorkshopDetailsView.swift @@ -0,0 +1,92 @@ +// +// WorkshopDetailsView.swift +// deepa +// +// Created by Don Chia on 3/11/22. +// + +import SwiftUI + +struct WorkshopDetailsView: View { + @State var title: String + @State var startAt: Date + @State var endAt: Date + @State var talkDescription: String + @State var activityName: String + @State var speakers: Array + + static let dateFormat: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "EEEE MMM d HH:mm" + return formatter + }() + + var body: some View { + ZStack(alignment: .leading) { + Color(.systemGroupedBackground).ignoresSafeArea(.all) + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 16) { + Text(Self.dateFormat.string(from: startAt) + " - " + Self.dateFormat.string(from: endAt)) + .font(.subheadline) + + HStack { + SpeakersImage(speakers: speakers) + + VStack(alignment: .leading, spacing: 2) { + if speakers.count == 1 { + Text("\(speakers[0].name) (\(speakers[0].company ?? ""))") + .font(.subheadline) + } else { + Text(concatSpeakerName(speakers: speakers, requireCompany: true)) + .font(.subheadline) + } + + if speakers.count == 1 { + Link("@\(speakers[0].twitter ?? "")", destination: URL(string: "https://twitter.com/\(speakers[0].twitter ?? "")")!) + } else { + VStack(alignment: .leading) { + ForEach(speakers) { speaker in + Link("@\(speaker.twitter ?? "")", destination: URL(string: "https://twitter.com/\(speaker.twitter ?? "")")!) + } + } + } + } + Spacer() + } + + Text(talkDescription) + .font(Font.body.leading(.loose)) + } + .frame(maxWidth: .infinity) + .padding() + .background(Color(UIColor.systemBackground)) + Spacer() + } + }.navigationBarTitle(title) + } +} + +struct SpeakersImage: View { + @State var speakers: Array + + var body: some View { + if speakers.count > 1 { + Rectangle() + .frame(width: 80, height: 80) + .background(.gray) + .cornerRadius(6) + } else { + AsyncImage(url: URL(string: getURL2022(speakerImageURL: speakers[0].imageUrl ?? ""))) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Rectangle() + .frame(width: 80, height: 80) + .background(.gray) + } + .frame(width: 80, height: 80) + .cornerRadius(6) + } + } +} diff --git a/deepa/Workshop/WorkshopView.swift b/deepa/Workshop/WorkshopView.swift index 12482e1..ef93078 100644 --- a/deepa/Workshop/WorkshopView.swift +++ b/deepa/Workshop/WorkshopView.swift @@ -8,8 +8,74 @@ import SwiftUI struct WorkshopView: View { + @StateObject var viewModel: ScheduleViewModel + + init() { + let viewModel = ScheduleViewModel { + print("fail init") + } + _viewModel = StateObject(wrappedValue: viewModel) + + } + + static let extractDate: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "dd/MM/yyyy" + return formatter + }() + + @State private var schedulePicker = 0 + var body: some View { - Text("workshop") + NavigationView { + VStack { + + if schedulePicker == 0 { + List { + ForEach(viewModel.schedule) { information in + if Self.extractDate.string(from: information.startAt ?? Date.now) == "17/01/2022" { + VStack { + NavigationLink(destination: WorkshopDetailsView(title: information.title, startAt: information.startAt ?? Date.now, endAt: information.endAt ?? Date.now, talkDescription: information.talkDescription ?? "", activityName: information.activityName ?? "", speakers: information.speakers)) { + ListContent(title: information.title, speakers: information.speakers, startAt: information.startAt ?? Date.now, endAt: information.endAt ?? Date.now) + } + } + } + } + }.listStyle(.plain) + } else { + List { + ForEach(viewModel.schedule) { information in + if Self.extractDate.string(from: information.startAt ?? Date.now) == "18/01/2022" { + VStack { + NavigationLink(destination: WorkshopDetailsView(title: information.title, startAt: information.startAt ?? Date.now, endAt: information.endAt ?? Date.now, talkDescription: information.talkDescription ?? "", activityName: information.activityName ?? "", speakers: information.speakers)) { + ListContent(title: information.title, speakers: information.speakers, startAt: information.startAt ?? Date.now, endAt: information.endAt ?? Date.now) + } + } + } + } + }.listStyle(.plain) + } + + Spacer() + + }.navigationTitle("Schedule") + .toolbar { + ToolbarItem(placement: .automatic) { + Picker("Schedule", selection: $schedulePicker) { + Text("Day 1").tag(0) + Text("Day 2").tag(1) + } + .pickerStyle(.segmented) + } + } + } + .onAppear { + viewModel.fetchSchedule() + } + } + + func handleGraphqlError() { + } }