Skip to content

Commit

Permalink
Merge pull request #1008 from kiwix/1006-improve-launch-sequence-for-…
Browse files Browse the repository at this point in the history
…custom-apps

Improve launch sequence for custom apps
  • Loading branch information
kelson42 authored Oct 18, 2024
2 parents 4cffee9 + ba3aef1 commit c78d229
Show file tree
Hide file tree
Showing 10 changed files with 507 additions and 223 deletions.
2 changes: 1 addition & 1 deletion App/App_macOS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ struct RootView: View {
} detail: {
switch navigation.currentItem {
case .loading:
LoadingView()
LoadingDataView()
case .reading:
BrowserTab().environmentObject(browser)
.withHostingWindow { window in
Expand Down
68 changes: 60 additions & 8 deletions App/CompactViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Combine
import SwiftUI
import UIKit
import CoreData
import Defaults

final class CompactViewController: UIHostingController<AnyView>, UISearchControllerDelegate, UISearchResultsUpdating {
private let searchViewModel: SearchViewModel
Expand Down Expand Up @@ -133,6 +134,7 @@ final class CompactViewController: UIHostingController<AnyView>, UISearchControl

private struct CompactView: View {
@EnvironmentObject private var navigation: NavigationViewModel
@EnvironmentObject private var library: LibraryViewModel
@State private var presentedSheet: PresentedSheet?

private enum PresentedSheet: String, Identifiable {
Expand All @@ -149,16 +151,22 @@ private struct CompactView: View {

var body: some View {
if case .loading = navigation.currentItem {
LoadingView()
LoadingDataView()
} else if case let .tab(tabID) = navigation.currentItem {
let browser = BrowserViewModel.getCached(tabID: tabID)
let model = if FeatureFlags.hasLibrary {
CatalogLaunchViewModel(library: library, browser: browser)
} else {
NoCatalogLaunchViewModel(browser: browser)
}
Content(tabID: tabID, showLibrary: {
if presentedSheet == nil {
presentedSheet = .library
} else {
// there's a sheet already presented by the user
// do nothing
}
})
}, model: model)
.id(tabID)
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Expand Down Expand Up @@ -189,7 +197,7 @@ private struct CompactView: View {
Spacer()
}
}
.environmentObject(BrowserViewModel.getCached(tabID: tabID))
.environmentObject(browser)
.sheet(item: $presentedSheet) { presentedSheet in
switch presentedSheet {
case .library:
Expand All @@ -212,22 +220,49 @@ private struct CompactView: View {
}
}

private struct Content: View {
private struct Content<LaunchModel>: View where LaunchModel: LaunchProtocol {
@Environment(\.scenePhase) private var scenePhase
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@EnvironmentObject private var browser: BrowserViewModel
@EnvironmentObject private var library: LibraryViewModel
@EnvironmentObject private var navigation: NavigationViewModel
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \ZimFile.size, ascending: false)],
predicate: ZimFile.openedPredicate
) private var zimFiles: FetchedResults<ZimFile>

/// this is still hacky a bit, as the change from here re-validates the view
/// which triggers the model to be revalidated
@Default(.hasSeenCategories) private var hasSeenCategories
let tabID: NSManagedObjectID?
let showLibrary: () -> Void
@ObservedObject var model: LaunchModel

var body: some View {
Group {
if browser.url == nil {
Welcome(showLibrary: showLibrary)
} else {
WebView().ignoresSafeArea()
// swiftlint:disable:next redundant_discardable_let
let _ = model.updateWith(hasZimFiles: !zimFiles.isEmpty,
hasSeenCategories: hasSeenCategories)
switch model.state {
case .loadingData:
LoadingDataView()
case .webPage(let isLoading):
WebView()
.ignoresSafeArea()
.overlay {
if isLoading {
LoadingProgressView()
}
}
case .catalog(let catalogSequence):
switch catalogSequence {
case .fetching:
FetchingCatalogView()
case .list:
LocalLibraryList()
case .welcome(let welcomeViewState):
WelcomeCatalog(viewState: welcomeViewState)
}
}
}
.focusedSceneValue(\.browserViewModel, browser)
Expand Down Expand Up @@ -262,6 +297,23 @@ private struct Content: View {
browser.refreshVideoState()
}
}
.onChange(of: library.state) { state in
guard state == .complete else { return }
showTheLibrary()
}
}

private func showTheLibrary() {
guard model.state.shouldShowCatalog else { return }
#if os(macOS)
navigation.currentItem = .categories
#else
if horizontalSizeClass == .regular {
navigation.currentItem = .categories
} else {
showLibrary()
}
#endif
}
}
#endif
2 changes: 1 addition & 1 deletion App/SplitViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ final class SplitViewController: UISplitViewController {
let controller = UIHostingController(rootView: Settings())
setViewController(UINavigationController(rootViewController: controller), for: .secondary)
case .loading:
let controller = UIHostingController(rootView: LoadingView())
let controller = UIHostingController(rootView: LoadingDataView())
setViewController(UINavigationController(rootViewController: controller), for: .secondary)
default:
let controller = UIHostingController(rootView: Text("vc-not-implemented"))
Expand Down
16 changes: 14 additions & 2 deletions ViewModel/BrowserViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ final class BrowserViewModel: NSObject, ObservableObject,

// MARK: - Properties

@Published private(set) var isLoading: Bool?
@Published private(set) var canGoBack = false
@Published private(set) var canGoForward = false
@Published private(set) var articleTitle: String = ""
@Published private(set) var zimFileName: String = ""
@Published private(set) var articleBookmarked = false
@Published private(set) var outlineItems = [OutlineItem]()
@Published private(set) var outlineItemTree = [OutlineItem]()
@MainActor @Published private(set) var hasURL: Bool = false
@MainActor @Published private(set) var url: URL? {
didSet {
if !FeatureFlags.hasLibrary, url == nil {
Expand All @@ -67,6 +69,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
bookmarkFetchedResultsController.fetchRequest.predicate = Self.bookmarksPredicateFor(url: url)
try? bookmarkFetchedResultsController.performFetch()
}
hasURL = url != nil
}
}
@Published var externalURL: URL?
Expand All @@ -87,6 +90,7 @@ final class BrowserViewModel: NSObject, ObservableObject,
}
#endif
let webView: WKWebView
private var isLoadingObserver: NSKeyValueObservation?
private var canGoBackObserver: NSKeyValueObservation?
private var canGoForwardObserver: NSKeyValueObservation?
private var titleURLObserver: AnyCancellable?
Expand All @@ -96,8 +100,8 @@ final class BrowserViewModel: NSObject, ObservableObject,

// MARK: - Lifecycle

@MainActor
init(tabID: NSManagedObjectID? = nil) {
// swiftlint:disable:next function_body_length
@MainActor init(tabID: NSManagedObjectID? = nil) {
self.tabID = tabID
webView = WKWebView(frame: .zero, configuration: WebViewConfiguration())
if !Bundle.main.isProduction, #available(iOS 16.4, macOS 13.3, *) {
Expand Down Expand Up @@ -159,6 +163,14 @@ final class BrowserViewModel: NSObject, ObservableObject,
guard let title, let url else { return }
self?.didUpdate(title: title, url: url)
}

isLoadingObserver = webView.observe(\.isLoading, options: .new) { [weak self] _, change in
Task { @MainActor [weak self] in
if change.newValue != self?.isLoading {
self?.isLoading = change.newValue
}
}
}
}

/// Get the webpage in a binary format
Expand Down
172 changes: 172 additions & 0 deletions ViewModel/LaunchViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// This file is part of Kiwix for iOS & macOS.
//
// Kiwix is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// any later version.
//
// Kiwix is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kiwix; If not, see https://www.gnu.org/licenses/.

import Foundation
import Combine
import Defaults

enum LaunchSequence: Equatable {
case loadingData
case webPage(isLoading: Bool)
case catalog(CatalogSequence)

var shouldShowCatalog: Bool {
switch self {
case .loadingData: return true
case .webPage: return false
case .catalog(.fetching): return true
case .catalog(.welcome): return true
case .catalog(.list): return false
}
}
}

enum CatalogSequence: Equatable {
case fetching
case list
case welcome(WelcomeViewState)
}

enum WelcomeViewState: Equatable {
case loading
case error
case complete
}

protocol LaunchProtocol: ObservableObject {
var state: LaunchSequence { get }
func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool)
}

// MARK: No Library (custom apps)

/// Keeps us int the .loadingData state,
/// while the main page is not fully loaded for the first time
final class NoCatalogLaunchViewModel: LaunchViewModelBase {

private static var wasLoaded = false

convenience init(browser: BrowserViewModel) {
self.init(browserIsLoading: browser.$isLoading)
}

/// - Parameter browserIsLoading: assumed to start with a nil value (see: WKWebView.isLoading)
init(browserIsLoading: Published<Bool?>.Publisher) {
super.init()
browserIsLoading.sink { [weak self] (isLoading: Bool?) in
guard let self = self else { return }
switch isLoading {
case .none:
updateTo(.loadingData)
case .some(true):
if !Self.wasLoaded {
updateTo(.loadingData)
} else {
updateTo(.webPage(isLoading: true))
}
case .some(false):
Self.wasLoaded = true
updateTo(.webPage(isLoading: false))
}
}.store(in: &cancellables)
}

override func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool) {
// to be ignored on purpose
}
}

// MARK: With Catalog Library
final class CatalogLaunchViewModel: LaunchViewModelBase {

private var hasZIMFiles = CurrentValueSubject<Bool, Never>(false)
private var hasSeenCategoriesOnce = CurrentValueSubject<Bool, Never>(false)

convenience init(library: LibraryViewModel,
browser: BrowserViewModel) {
self.init(libraryState: library.$state, browserIsLoading: browser.$isLoading)
}

// swiftlint:disable closure_parameter_position
init(libraryState: Published<LibraryState>.Publisher,
browserIsLoading: Published<Bool?>.Publisher) {
super.init()

hasZIMFiles.combineLatest(
libraryState,
browserIsLoading,
hasSeenCategoriesOnce
).sink { [weak self] (
hasZIMs: Bool,
libState: LibraryState,
isBrowserLoading: Bool?,
hasSeenCategories: Bool
) in
guard let self = self else { return }

switch (isBrowserLoading, hasZIMs, libState) {

// MARK: initial app start state
case (_, _, .initial): updateTo(.loadingData)

// MARK: browser must be empty as there are no ZIMs:
case (_, false, .inProgress):
if hasSeenCategories {
updateTo(.catalog(.welcome(.loading)))
} else {
updateTo(.catalog(.fetching))
}
case (_, false, .complete):
if hasSeenCategories {
updateTo(.catalog(.welcome(.complete)))
} else {
updateTo(.catalog(.fetching))
}
case (_, false, .error):
// safety path to display the welcome buttons
// in case of a fetch error, the user can try again
hasSeenCategoriesOnce.send(true)
updateTo(.catalog(.welcome(.error)))

// MARK: has zims and opens a new empty tab
case (.none, true, _): updateTo(.catalog(.list))

// MARK: actively browsing
case (.some(true), true, _): updateTo(.webPage(isLoading: true))
case (.some(false), true, _): updateTo(.webPage(isLoading: false))
}
}.store(in: &cancellables)
}
// swiftlint:enable closure_parameter_position

override func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool) {
hasZIMFiles.send(hasZimFiles)
hasSeenCategoriesOnce.send(hasSeenCategories)
}
}

class LaunchViewModelBase: LaunchProtocol, ObservableObject {
var state: LaunchSequence = .loadingData
var cancellables = Set<AnyCancellable>()

func updateTo(_ newState: LaunchSequence) {
guard newState != state else { return }
state = newState
}

func updateWith(hasZimFiles: Bool, hasSeenCategories: Bool) {
fatalError("should be overriden")
}
}
Loading

0 comments on commit c78d229

Please sign in to comment.