From aaf0cee96c093d5d1d4def5257a5d40a53390d5c Mon Sep 17 00:00:00 2001 From: Shafqat Muneer Date: Wed, 5 Jun 2024 21:24:07 +0500 Subject: [PATCH] feat: Program Screen Error Handling --- Core/Core.xcodeproj/project.pbxproj | 12 ++ .../FullScreenErrorView.swift | 96 ++++++++++++++ Core/Core/View/Base/Webview/WebView.swift | 19 ++- Course/Course.xcodeproj/project.pbxproj | 14 +- .../CalendarSyncProgressView.swift | 0 .../DatesSuccessView}/DatesSuccessView.swift | 0 .../Presentation/Unit/CourseUnitView.swift | 32 +---- .../DiscoveryWebviewViewModel.swift | 8 ++ .../WebPrograms/ProgramWebviewView.swift | 124 ++++++++++++------ .../WebPrograms/ProgramWebviewViewModel.swift | 7 + 10 files changed, 238 insertions(+), 74 deletions(-) create mode 100644 Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift rename Course/Course/{Views => Presentation/Subviews/CalendarSyncProgressView}/CalendarSyncProgressView.swift (100%) rename Course/Course/{Views => Presentation/Subviews/DatesSuccessView}/DatesSuccessView.swift (100%) diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index 3d8d938c7..cc5ec5dc9 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -144,6 +144,7 @@ 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141F1D2F2B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift */; }; 142EDD6C2B831D1400F9F320 /* BranchSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 142EDD6B2B831D1400F9F320 /* BranchSDK */; }; 14769D3C2B9822EE00AB36D4 /* CoreAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14769D3B2B9822EE00AB36D4 /* CoreAnalytics.swift */; }; + 9784D47E2BF7762800AFEFFF /* FullScreenErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */; }; A51CDBE72B6D21F2009B6D4E /* SegmentConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */; }; A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A32342B233DEC005FE38A /* ThemeConfig.swift */; }; A595689B2B6173DF00ED4F90 /* BranchConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A595689A2B6173DF00ED4F90 /* BranchConfig.swift */; }; @@ -340,6 +341,7 @@ 349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B74C6685E416657F3C5F5A8 /* Pods-App-Core.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releaseprod.xcconfig"; sourceTree = ""; }; 60153262DBC2F9E660D7E11B /* Pods-App-Core.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.release.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.release.xcconfig"; sourceTree = ""; }; + 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullScreenErrorView.swift; sourceTree = ""; }; 9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugdev.xcconfig"; sourceTree = ""; }; A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentConfig.swift; sourceTree = ""; }; A53A32342B233DEC005FE38A /* ThemeConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeConfig.swift; sourceTree = ""; }; @@ -717,6 +719,7 @@ 0770DE7728D0C49E006D8A5D /* Base */ = { isa = PBXGroup; children = ( + 9784D47C2BF7761F00AFEFFF /* FullScreenErrorView */, 064987882B4D69FE0071642A /* Webview */, E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */, 02A4833B29B8C57800D33F33 /* DownloadView.swift */, @@ -769,6 +772,14 @@ path = Analytics; sourceTree = ""; }; + 9784D47C2BF7761F00AFEFFF /* FullScreenErrorView */ = { + isa = PBXGroup; + children = ( + 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */, + ); + path = FullScreenErrorView; + sourceTree = ""; + }; BA30427C2B20B235009B64B7 /* SocialAuth */ = { isa = PBXGroup; children = ( @@ -1127,6 +1138,7 @@ 0260E58028FD792800BBBE18 /* WebUnitViewModel.swift in Sources */, 02A4833A29B8A9AB00D33F33 /* DownloadManager.swift in Sources */, 06078B702BA49C3100576798 /* Dictionary+JSON.swift in Sources */, + 9784D47E2BF7762800AFEFFF /* FullScreenErrorView.swift in Sources */, 027BD3AE2909475000392132 /* KeyboardScrollerOptions.swift in Sources */, BAFB99922B14E23D007D09F9 /* AppleSignInConfig.swift in Sources */, 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */, diff --git a/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift b/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift new file mode 100644 index 000000000..0b65238a1 --- /dev/null +++ b/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift @@ -0,0 +1,96 @@ +// +// FullScreenErrorView.swift +// Course +// +// Created by Shafqat Muneer on 5/14/24. +// + +import SwiftUI +import Theme + +public struct FullScreenErrorView: View { + + public enum ErrorType { + case noInternet + case noInternetWithReload + case generic + } + + private let errorType: ErrorType + private var reloadAction: () -> Void = {} + + public init( + type: ErrorType + ) { + self.errorType = type + } + + public init( + type: ErrorType, + reloadAction: @escaping () -> Void + ) { + self.errorType = type + self.reloadAction = reloadAction + } + + public var body: some View { + GeometryReader { proxy in + VStack(spacing: 28) { + Spacer() + switch errorType { + case .noInternet, .noInternetWithReload: + CoreAssets.noWifi.swiftUIImage + .renderingMode(.template) + .foregroundStyle(Color.primary) + .scaledToFit() + + Text(CoreLocalization.Error.Internet.noInternetTitle) + .font(Theme.Fonts.titleLarge) + .foregroundColor(Theme.Colors.textPrimary) + + Text(CoreLocalization.Error.Internet.noInternetDescription) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.center) + .padding(.horizontal, 50) + case .generic: + CoreAssets.notAvaliable.swiftUIImage + .renderingMode(.template) + .foregroundStyle(Color.primary) + .scaledToFit() + + Text(CoreLocalization.View.Snackbar.tryAgainBtn) + .font(Theme.Fonts.titleLarge) + .foregroundColor(Theme.Colors.textPrimary) + + Text(CoreLocalization.Error.unknownError) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.center) + .padding(.horizontal, 50) + } + + if errorType != .noInternet { + UnitButtonView(type: .reload, action: { + self.reloadAction() + }) + } + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: proxy.size.height) + .background( + Theme.Colors.background + ) + } + } +} + +#if DEBUG +//swiftlint:disable all +struct FullScreenErrorView_Previews: PreviewProvider { + static var previews: some View { + FullScreenErrorView(type: .noInternetWithReload) + } +} +//swiftlint:enable all +#endif diff --git a/Core/Core/View/Base/Webview/WebView.swift b/Core/Core/View/Base/Webview/WebView.swift index cb36ea255..929cdb26d 100644 --- a/Core/Core/View/Base/Webview/WebView.swift +++ b/Core/Core/View/Base/Webview/WebView.swift @@ -17,6 +17,8 @@ public protocol WebViewNavigationDelegate: AnyObject { shouldLoad request: URLRequest, navigationAction: WKNavigationAction ) async -> Bool + + func showWebViewError() } public struct WebView: UIViewRepresentable { @@ -70,6 +72,10 @@ public struct WebView: UIViewRepresentable { public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { webView.isHidden = false + DispatchQueue.main.async { + self.parent.isLoading = false + } + parent.webViewNavDelegate?.showWebViewError() } public func webView( @@ -78,6 +84,10 @@ public struct WebView: UIViewRepresentable { withError error: Error ) { webView.isHidden = false + DispatchQueue.main.async { + self.parent.isLoading = false + } + parent.webViewNavDelegate?.showWebViewError() } public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { @@ -189,7 +199,14 @@ public struct WebView: UIViewRepresentable { @objc private func reload() { parent.isLoading = true - webview?.reload() + if webview?.url?.absoluteString.isEmpty ?? true { + if let url = URL(string: parent.viewModel.url) { + let request = URLRequest(url: url) + webview?.load(request) + } + } else { + webview?.reload() + } } public func userContentController( diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index ed692cdd8..f84d0b9a1 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -311,7 +311,6 @@ 02B6B3B528E1D10700232911 /* Domain */, 02EAE2CA28E1F0A700529644 /* Presentation */, 97EA4D822B84EFA900663F58 /* Managers */, - 97CA95212B875EA200A9EDEA /* Views */, 02B6B3B428E1C49400232911 /* Localizable.strings */, ); path = Course; @@ -526,13 +525,20 @@ path = Mock; sourceTree = ""; }; - 97CA95212B875EA200A9EDEA /* Views */ = { + 9784D4752BF39EEF00AFEFFF /* CalendarSyncProgressView */ = { isa = PBXGroup; children = ( 97C99C352B9A08FE004EEDE2 /* CalendarSyncProgressView.swift */, + ); + path = CalendarSyncProgressView; + sourceTree = ""; + }; + 9784D4762BF39EFD00AFEFFF /* DatesSuccessView */ = { + isa = PBXGroup; + children = ( 97CA95242B875EE200A9EDEA /* DatesSuccessView.swift */, ); - path = Views; + path = DatesSuccessView; sourceTree = ""; }; 97EA4D822B84EFA900663F58 /* Managers */ = { @@ -592,6 +598,8 @@ BAD9CA482B2C88D500DE790A /* Subviews */ = { isa = PBXGroup; children = ( + 9784D4762BF39EFD00AFEFFF /* DatesSuccessView */, + 9784D4752BF39EEF00AFEFFF /* CalendarSyncProgressView */, 02D4FC2C2BBD7C7500C47748 /* MessageSectionView */, BAC0E0D92B32F0A2006B68A9 /* CourseVideoDownloadBarView */, BA58CF622B471047005B102E /* VideoDownloadQualityBarView */, diff --git a/Course/Course/Views/CalendarSyncProgressView.swift b/Course/Course/Presentation/Subviews/CalendarSyncProgressView/CalendarSyncProgressView.swift similarity index 100% rename from Course/Course/Views/CalendarSyncProgressView.swift rename to Course/Course/Presentation/Subviews/CalendarSyncProgressView/CalendarSyncProgressView.swift diff --git a/Course/Course/Views/DatesSuccessView.swift b/Course/Course/Presentation/Subviews/DatesSuccessView/DatesSuccessView.swift similarity index 100% rename from Course/Course/Views/DatesSuccessView.swift rename to Course/Course/Presentation/Subviews/DatesSuccessView/DatesSuccessView.swift diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index dff0a0ea9..6ceb302bf 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -189,7 +189,7 @@ public struct CourseUnitView: View { Spacer(minLength: 150) } } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { @@ -219,7 +219,7 @@ public struct CourseUnitView: View { Spacer(minLength: 150) } } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } // MARK: Web @@ -233,7 +233,7 @@ public struct CourseUnitView: View { ) // not need to add frame limit there because we did that with injection } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { EmptyView() @@ -247,7 +247,7 @@ public struct CourseUnitView: View { Spacer() .frame(minHeight: 100) } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { EmptyView() @@ -275,7 +275,7 @@ public struct CourseUnitView: View { //No need iPad paddings there bacause they were added //to PostsView that placed inside DiscussionView } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { EmptyView() @@ -569,25 +569,3 @@ struct CourseUnitView_Previews: PreviewProvider { } //swiftlint:enable all #endif - -struct NoInternetView: View { - - var body: some View { - VStack(spacing: 28) { - Spacer() - CoreAssets.noWifi.swiftUIImage - .renderingMode(.template) - .foregroundStyle(Color.primary) - .scaledToFit() - Text(CoreLocalization.Error.Internet.noInternetTitle) - .font(Theme.Fonts.titleLarge) - .foregroundColor(Theme.Colors.textPrimary) - Text(CoreLocalization.Error.Internet.noInternetDescription) - .font(Theme.Fonts.bodyLarge) - .foregroundColor(Theme.Colors.textPrimary) - .multilineTextAlignment(.center) - .padding(.horizontal, 50) - Spacer() - }.frame(maxWidth: .infinity, maxHeight: .infinity) - } -} diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift index 836323072..6a0bdc467 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift @@ -14,6 +14,8 @@ public class DiscoveryWebviewViewModel: ObservableObject { @Published var courseDetails: CourseDetails? @Published private(set) var showProgress = false @Published var showError: Bool = false + @Published var webViewError: Bool = false + var errorMessage: String? { didSet { withAnimation { @@ -247,4 +249,10 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { private func isValidAppURLScheme(_ url: URL) -> Bool { return url.scheme ?? "" == config.URIScheme } + + public func showWebViewError() { + DispatchQueue.main.async { + self.webViewError = true + } + } } diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift index ad28e6938..3728898a7 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift @@ -18,7 +18,7 @@ public enum ProgramViewType: Equatable { public struct ProgramWebviewView: View { @State private var isLoading: Bool = true - @ObservedObject private var viewModel: ProgramWebviewViewModel + @StateObject private var viewModel: ProgramWebviewViewModel private var router: DiscoveryRouter private var viewType: ProgramViewType public var pathID: String @@ -42,66 +42,82 @@ public struct ProgramWebviewView: View { viewType: ProgramViewType = .program, pathID: String = "" ) { - self.viewModel = viewModel + self._viewModel = .init(wrappedValue: viewModel) self.router = router self.viewType = viewType self.pathID = pathID - - if let url = URL(string: URLString) { - viewModel.request = URLRequest(url: url) - } } public var body: some View { GeometryReader { proxy in - VStack(alignment: .center) { - WebView( - viewModel: .init( - url: URLString, - baseURL: "", - injections: [.colorInversionCss] - ), - isLoading: $isLoading, - refreshCookies: { - await viewModel.updateCookies( - force: true - ) - }, - navigationDelegate: viewModel - ) - .accessibilityIdentifier("program_webview") - - if isLoading || viewModel.showProgress || viewModel.updatingCookies { - HStack(alignment: .center) { - ProgressBar( - size: 40, - lineWidth: 8 - ) - .padding(.vertical, proxy.size.height / 2) - .accessibilityIdentifier("progress_bar") + ZStack(alignment: .center) { + VStack(alignment: .center) { + WebView( + viewModel: .init( + url: URLString, + baseURL: "", + injections: [.colorInversionCss] + ), + isLoading: $isLoading, + refreshCookies: { + await viewModel.updateCookies( + force: true + ) + }, + navigationDelegate: viewModel + ) + .accessibilityIdentifier("program_webview") + + if isLoading || viewModel.showProgress || viewModel.updatingCookies { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(.vertical, proxy.size.height / 2) + .accessibilityIdentifier("progress_bar") + } + .frame(width: proxy.size.width, height: proxy.size.height) + } + + // MARK: - Show Error + if viewModel.showError { + VStack { + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, 20) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } } - .frame(width: proxy.size.width, height: proxy.size.height) } - // MARK: - Show Error - if viewModel.showError { - VStack { - SnackBarView(message: viewModel.errorMessage) - } - .padding(.bottom, 20) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil - } + if viewModel.webViewError { + FullScreenErrorView( + type: viewModel.connectivity.isInternetAvaliable ? .generic : .noInternetWithReload + ) { + viewModel.webViewError = false + NotificationCenter.default.post( + name: .webviewReloadNotification, + object: nil + ) } } } + .onFirstAppear { + if let url = URL(string: URLString) { + viewModel.request = URLRequest(url: url) + } + } // MARK: - Offline mode SnackBar OfflineSnackBarView( connectivity: viewModel.connectivity, reloadAction: { + viewModel.webViewError = false NotificationCenter.default.post( name: .webviewReloadNotification, object: nil @@ -114,3 +130,25 @@ public struct ProgramWebviewView: View { .animation(.default, value: viewModel.showError) } } + +#if DEBUG +//swiftlint:disable all +struct ProgramWebviewView_Previews: PreviewProvider { + static var previews: some View { + ProgramWebviewView( + viewModel: ProgramWebviewViewModel( + router: DiscoveryRouterMock(), + config: ConfigMock(), + interactor: DiscoveryInteractor.mock, + connectivity: Connectivity(), + analytics: DiscoveryAnalyticsMock(), + authInteractor: AuthInteractor.mock + ), + router: DiscoveryRouterMock(), + viewType: .program, + pathID: "" + ) + } +} +//swiftlint:enable all +#endif diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift index ad0c89987..ce8c10b07 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift @@ -14,6 +14,7 @@ public class ProgramWebviewViewModel: ObservableObject, WebviewCookiesUpdateProt @Published var courseDetails: CourseDetails? @Published private(set) var showProgress = false @Published var showError: Bool = false + @Published var webViewError: Bool = false @Published public var updatingCookies: Bool = false @Published public var cookiesReady: Bool = false @@ -235,4 +236,10 @@ extension ProgramWebviewViewModel: WebViewNavigationDelegate { private func isValidAppURLScheme(_ url: URL) -> Bool { return url.scheme ?? "" == config.URIScheme } + + public func showWebViewError() { + DispatchQueue.main.async { + self.webViewError = true + } + } }