Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 79 additions & 35 deletions Core/Core/Configuration/Connectivity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Connectivity.swift
// OpenEdX
//
// Created by  Stepanok Ivan on 15.12.2022.
// Created by Stepanok Ivan on 15.12.2022.
//

import Alamofire
Expand All @@ -14,55 +14,99 @@ public enum InternetState: Sendable {
case notReachable
}

//sourcery: AutoMockable
// sourcery: AutoMockable
@MainActor
public protocol ConnectivityProtocol: Sendable {
var isInternetAvaliable: Bool { get }
var isMobileData: Bool { get }
var internetReachableSubject: CurrentValueSubject<InternetState?, Never> { get }
}

@MainActor
public class Connectivity: ConnectivityProtocol {
let networkManager = NetworkReachabilityManager()

public var isInternetAvaliable: Bool {
// false
networkManager?.isReachable ?? false

private let networkManager = NetworkReachabilityManager()
private let verificationURL: URL
private let verificationTimeout: TimeInterval
private let cacheValidity: TimeInterval = 30

private var lastVerificationDate: TimeInterval?
private var lastVerificationResult: Bool = true

public let internetReachableSubject = CurrentValueSubject<InternetState?, Never>(nil)

private(set) var _isInternetAvailable: Bool = true {
didSet {
Task { @MainActor in
internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable)
}
}
}

public var isMobileData: Bool {
if let networkManager {
return networkManager.isReachableOnCellular
} else {
return false

public var isInternetAvaliable: Bool {
if let last = lastVerificationDate,
Date().timeIntervalSince1970 - last < cacheValidity {
return lastVerificationResult
}

Task {
await performVerification()
}

return lastVerificationResult
}

public let internetReachableSubject = CurrentValueSubject<InternetState?, Never>(nil)

public init() {
checkInternet()

public var isMobileData: Bool {
networkManager?.isReachableOnCellular == true
}

func checkInternet() {
if let networkManager {
networkManager.startListening { status in
DispatchQueue.main.async {
switch status {
case .unknown:
self.internetReachableSubject.send(InternetState.notReachable)
case .notReachable:
self.internetReachableSubject.send(InternetState.notReachable)
case .reachable:
self.internetReachableSubject.send(InternetState.reachable)
}

public init(
config: ConfigProtocol,
timeout: TimeInterval = 15
) {
self.verificationURL = config.baseURL
self.verificationTimeout = timeout

networkManager?.startListening(onQueue: .global()) { [weak self] status in
guard let self = self else { return }
Task { @MainActor in
switch status {
case .reachable:
await self.performVerification()
case .notReachable, .unknown:
self.updateAvailability(false, at: 0)
}
}
} else {
DispatchQueue.main.async {
self.internetReachableSubject.send(InternetState.notReachable)
}
}

deinit {
networkManager?.stopListening()
}

private func performVerification() async {
let now = Date().timeIntervalSince1970
let live = await verifyInternet()
updateAvailability(live, at: now)
}

private func updateAvailability(_ available: Bool, at timestamp: TimeInterval) {
_isInternetAvailable = available
lastVerificationDate = timestamp
lastVerificationResult = available
}

private func verifyInternet() async -> Bool {
var request = URLRequest(url: verificationURL)
request.httpMethod = "HEAD"
request.timeoutInterval = verificationTimeout
do {
let (_, response) = try await URLSession.shared.data(for: request)
if let http = response as? HTTPURLResponse, (200..<400).contains(http.statusCode) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens on 401..405 responses? As example 404 is a "not found" response, not a connection issues. Also 500...599 is a server response.

return true
}
} catch {
return false
}
return false
}
}
3 changes: 2 additions & 1 deletion Core/Core/View/Base/OfflineSnackBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public struct OfflineSnackBarView: View {

struct OfflineSnackBarView_Previews: PreviewProvider {
static var previews: some View {
OfflineSnackBarView(connectivity: Connectivity(), reloadAction: {})
let configMock = ConfigMock()
OfflineSnackBarView(connectivity: Connectivity(config: configMock), reloadAction: {})
}
}
3 changes: 2 additions & 1 deletion Core/Core/View/Base/WebBrowser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public struct WebBrowser: View {

struct WebBrowser_Previews: PreviewProvider {
static var previews: some View {
WebBrowser(url: "", pageTitle: "", connectivity: Connectivity())
let configMock = ConfigMock()
WebBrowser(url: "", pageTitle: "", connectivity: Connectivity(config: configMock))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ public struct CourseContainerView: View {
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
manager: DownloadManagerMock(),
storage: CourseStorageMock(),
isActive: true,
Expand All @@ -403,7 +403,7 @@ public struct CourseContainerView: View {
interactor: CourseInteractor.mock,
router: CourseRouterMock(),
cssInjector: CSSInjectorMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
config: ConfigMock(),
courseID: "1",
courseName: "a",
Expand All @@ -414,7 +414,7 @@ public struct CourseContainerView: View {
interactor: CourseInteractor.mock,
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
),
courseID: "",
title: "Title of Course",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public final class CourseContainerViewModel: BaseCourseViewModel {

private let interactor: CourseInteractorProtocol
private let authInteractor: AuthInteractorProtocol

let analytics: CourseAnalytics
let coreAnalytics: CoreAnalytics
private(set) var storage: CourseStorage
Expand Down Expand Up @@ -214,7 +215,7 @@ public final class CourseContainerViewModel: BaseCourseViewModel {
courseVideoStructure: nil
)
}

@MainActor
func getCourseStructure(courseID: String) async throws -> CourseStructure? {
if isInternetAvaliable {
Expand Down Expand Up @@ -1766,7 +1767,7 @@ extension CourseContainerViewModel {
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
manager: DownloadManagerMock(),
storage: CourseStorageMock(),
isActive: true,
Expand Down
2 changes: 1 addition & 1 deletion Course/Course/Presentation/Content/CourseContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ public struct CourseContentView: View {
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
manager: DownloadManagerMock(),
storage: CourseStorageMock(),
isActive: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ struct AllContentView: View {
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
manager: DownloadManagerMock(),
storage: CourseStorageMock(),
isActive: true,
Expand Down
2 changes: 1 addition & 1 deletion Course/Course/Presentation/Dates/CourseDatesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ struct CourseDatesView_Previews: PreviewProvider {
interactor: CourseInteractor(repository: CourseRepositoryMock()),
router: CourseRouterMock(),
cssInjector: CSSInjectorMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
config: ConfigMock(),
courseID: "",
courseName: "",
Expand Down
2 changes: 1 addition & 1 deletion Course/Course/Presentation/Handouts/HandoutsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ struct HandoutsView_Previews: PreviewProvider {
let viewModel = HandoutsViewModel(interactor: CourseInteractor.mock,
router: CourseRouterMock(),
cssInjector: CSSInjectorMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
courseID: "",
analytics: CourseAnalyticsMock())
HandoutsView(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -466,15 +466,15 @@ public struct CourseOutlineAndProgressView: View {
interactor: CourseInteractor.mock,
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
connectivity: Connectivity()
connectivity: Connectivity(config: ConfigMock())
)
let vmOutline = CourseContainerViewModel(
interactor: CourseInteractor.mock,
authInteractor: AuthInteractor.mock,
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
manager: DownloadManagerMock(),
storage: CourseStorageMock(),
isActive: true,
Expand All @@ -487,7 +487,7 @@ public struct CourseOutlineAndProgressView: View {
courseHelper: CourseDownloadHelper(courseStructure: nil, manager: DownloadManagerMock())
)

return PreviewContainer(
PreviewContainer(
viewModelContainer: vmOutline,
viewModelProgress: vmProgress
)
Expand All @@ -514,7 +514,7 @@ private struct PreviewContainer: View {
collapsed: $collapsed,
viewHeight: $viewHeight,
dateTabIndex: 2,
connectivity: Connectivity()
connectivity: Connectivity(config: ConfigMock())
)
.loadFonts()
.task {
Expand Down
2 changes: 1 addition & 1 deletion Course/Course/Presentation/Offline/OfflineView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ struct OfflineView: View {
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
manager: DownloadManagerMock(),
storage: CourseStorageMock(),
isActive: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ struct LargestDownloadsView_Previews: PreviewProvider {
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
manager: DownloadManagerMock(),
storage: CourseStorageMock(),
isActive: true,
Expand Down
4 changes: 3 additions & 1 deletion Course/Course/Presentation/Outline/CourseOutlineView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ public struct CourseOutlineView: View {
collapsed: $collapsed,
viewHeight: $viewHeight
)

RefreshProgressView(isShowRefresh: $viewModel.isShowRefresh)

VStack(alignment: .leading) {

if isVideo,
Expand Down Expand Up @@ -334,7 +336,7 @@ struct CourseOutlineView_Previews: PreviewProvider {
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
config: ConfigMock(),
connectivity: Connectivity(),
connectivity: Connectivity(config: ConfigMock()),
manager: DownloadManagerMock(),
storage: CourseStorageMock(),
isActive: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ struct CourseVerticalView_Previews: PreviewProvider {
sequentialIndex: 0,
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
connectivity: Connectivity()
connectivity: Connectivity(config: ConfigMock())
)

return Group {
CourseVerticalView(
title: "Course title",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct CourseProgressScreenView: View {

@StateObject
private var viewModel: CourseProgressViewModel
private let initialCourseStructure: CourseStructure?

private let connectivity: ConnectivityProtocol

Expand All @@ -37,7 +38,7 @@ struct CourseProgressScreenView: View {
self._viewHeight = viewHeight
self._viewModel = StateObject(wrappedValue: { viewModel }())
self.connectivity = connectivity
self.viewModel.courseStructure = courseStructure
self.initialCourseStructure = courseStructure
}

public var body: some View {
Expand Down Expand Up @@ -111,6 +112,9 @@ struct CourseProgressScreenView: View {
.ignoresSafeArea()
)
.onFirstAppear {
if viewModel.courseStructure == nil {
viewModel.courseStructure = initialCourseStructure
}
Task {
await viewModel.getCourseProgress(courseID: courseID)
}
Expand Down Expand Up @@ -222,3 +226,25 @@ struct CourseProgressScreenView: View {
}
}
}

#if DEBUG
#Preview {
let vm = CourseProgressViewModel(
interactor: CourseInteractor.mock,
router: CourseRouterMock(),
analytics: CourseAnalyticsMock(),
connectivity: Connectivity(config: ConfigMock())
)

CourseProgressScreenView(
courseID: "test",
coordinate: .constant(0),
collapsed: .constant(false),
viewHeight: .constant(0),
viewModel: vm,
connectivity: Connectivity(config: ConfigMock()),
courseStructure: nil
)
.loadFonts()
}
#endif
Loading
Loading