Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor ContentViewModel to have a single source of truth #614

Merged
merged 1 commit into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public struct DownloadRequest: Hashable, Sendable {

// MARK: Private

private static let downloadResumeDataExtension = ".resume"
private static let downloadResumeDataExtension = "resume"
}

public struct DownloadBatchRequest: Hashable, Sendable {
Expand Down
66 changes: 14 additions & 52 deletions Features/QuranContentFeature/ContentViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
// Copyright © 2019 Quran.com. All rights reserved.
//

import Combine
import NoorUI
import QuranAnnotations
import QuranKit
import QuranPagesFeature
import QuranText
import QuranTextKit
import SwiftUI
import UIKit
Expand Down Expand Up @@ -39,8 +39,7 @@ public final class ContentViewController: UIViewController, UIGestureRecognizerD
super.viewDidLoad()
view.backgroundColor = .reading
setUpGesture()
setUpPageCollectionBuilderChanges()
setUpHighlightsListener()
setUpPagesView()
}

public func gestureRecognizer(
Expand All @@ -66,59 +65,15 @@ public final class ContentViewController: UIViewController, UIGestureRecognizerD

// MARK: Private

private var pagingController: UIViewController?
private let viewModel: ContentViewModel
private var cancellables: Set<AnyCancellable> = []
private var lastHighlights: QuranHighlights?

private var pageViews: [PageView] {
findPageViews(in: self)
}

private func setUpHighlightsListener() {
viewModel.deps.highlightsService.$highlights
.receive(on: DispatchQueue.main)
.sink { [weak self] newHighlights in
self?.highlightsUpdatedTo(newHighlights)
}
.store(in: &cancellables)
}

private func highlightsUpdatedTo(_ highlights: QuranHighlights) {
defer {
lastHighlights = highlights
}

guard let oldValue = lastHighlights else {
return
}

if let ayah = highlights.verseToScrollTo(comparingTo: oldValue) {
viewModel.visiblePages = [ayah.page]
}
}

private func setUpPageCollectionBuilderChanges() {
viewModel.$pageViewBuilder
.receive(on: DispatchQueue.main)
.sink { [weak self] pageViewBuilder in
self?.install(pageViewBuilder)
}
.store(in: &cancellables)
}

private func install(_ pageViewBuilder: PageViewBuilder?) {
guard let pageViewBuilder else {
return
}

if let oldPagingController = pagingController {
removeChild(oldPagingController)
}

let pagesView = PagesView(viewModel: viewModel, pageBuilder: pageViewBuilder.build())
private func setUpPagesView() {
let pagesView = PagesView(viewModel: viewModel)
let pagingController = UIHostingController(rootView: pagesView)
self.pagingController = pagingController
addFullScreenChild(pagingController)
}

Expand Down Expand Up @@ -186,18 +141,25 @@ public final class ContentViewController: UIViewController, UIGestureRecognizerD
}

private struct PagesView: View {
private struct PagesId: Hashable {
let quranMode: QuranMode
let selectedTranslations: [Translation.ID]
}

// MARK: Internal

@ObservedObject var viewModel: ContentViewModel
let pageBuilder: (Page) -> UIViewController

var body: some View {
GeometryReader { geometry in
QuranPaginationView(
pagingStrategy: pagingStrategy(with: geometry),
selection: $viewModel.visiblePages,
pages: viewModel.pages
pages: viewModel.deps.quran.pages
) { page in
StaticViewControllerRepresentable(viewController: pageBuilder(page))
StaticViewControllerRepresentable(viewController: viewModel.pageViewBuilder.build(at: page))
}
.id(PagesId(quranMode: viewModel.quranMode, selectedTranslations: viewModel.selectedTranslations))
}
}

Expand Down
73 changes: 35 additions & 38 deletions Features/QuranContentFeature/ContentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,26 +58,27 @@ public final class ContentViewModel: ObservableObject {
self.input = input

visiblePages = [input.initialPage]
pages = deps.quran.pages

highlights = deps.highlightsService.highlights
twoPagesEnabled = deps.quranContentStatePreferences.twoPagesEnabled
verticalScrollingEnabled = deps.quranContentStatePreferences.verticalScrollingEnabled
quranMode = deps.quranContentStatePreferences.quranMode
selectedTranslations = deps.selectedTranslationsPreferences.selectedTranslations

deps.highlightsService.$highlights
.sink { [weak self] in self?.highlights = $0 }
.store(in: &cancellables)
deps.quranContentStatePreferences.$twoPagesEnabled
.sink { [weak self] in self?.twoPagesEnabled = $0 }
.store(in: &cancellables)
deps.quranContentStatePreferences.$verticalScrollingEnabled
.sink { [weak self] in self?.verticalScrollingEnabled = $0 }
.store(in: &cancellables)
deps.quranContentStatePreferences.$quranMode
.sink { [weak self] _ in self?.reloadAllPages() }
.sink { [weak self] in self?.quranMode = $0 }
.store(in: &cancellables)
deps.selectedTranslationsPreferences.$selectedTranslations
.sink { [weak self] _ in self?.reloadAllPages() }
.sink { [weak self] in self?.selectedTranslations = $0 }
.store(in: &cancellables)

loadNotes()
configureAsInitialPage()
configureInitialPage()
}

// MARK: Public
Expand All @@ -98,30 +99,43 @@ public final class ContentViewModel: ObservableObject {
}

public func highlightWord(_ word: Word?) {
deps.highlightsService.highlights.pointedWord = word
highlights.pointedWord = word
}

public func highlightReadingAyah(_ ayah: AyahNumber?) {
deps.highlightsService.highlights.readingVerses = [ayah].compactMap { $0 }
highlights.readingVerses = [ayah].compactMap { $0 }
}

// MARK: Internal

let deps: Deps
weak var listener: ContentListener?

@Published var quranMode: QuranMode
@Published var selectedTranslations: [Translation.ID]
@Published var twoPagesEnabled: Bool
@Published var pageViewBuilder: PageViewBuilder?

let pages: [Page]
@Published var highlights: QuranHighlights {
didSet {
if oldValue != highlights {
deps.highlightsService.highlights = highlights

if let ayah = highlights.verseToScrollTo(comparingTo: oldValue) {
visiblePages = [ayah.page]
}
}
}
}

var pagingStrategy: PagingStrategy {
let shouldDisplayTwoPages = !verticalScrollingEnabled && twoPagesEnabled
return shouldDisplayTwoPages ? .doublePage : .singlePage
twoPagesEnabled ? .doublePage : .singlePage
}

var verticalScrollingEnabled: Bool {
didSet { reloadAllPages() }
var pageViewBuilder: PageViewBuilder {
switch deps.quranContentStatePreferences.quranMode {
case .arabic: return deps.imageDataSourceBuilder
case .translation: return deps.translationDataSourceBuilder
}
}

func onViewLongPressStarted(at point: CGPoint, sourceView: UIView, verse: AyahNumber) {
Expand Down Expand Up @@ -165,7 +179,7 @@ public final class ContentViewModel: ObservableObject {

private var longPressData: LongPressData? {
didSet {
deps.highlightsService.highlights.shareVerses = selectedVerses ?? []
highlights.shareVerses = selectedVerses ?? []
}
}

Expand All @@ -181,13 +195,6 @@ public final class ContentViewModel: ObservableObject {
return start.array(to: end)
}

private var newPageCollectionBuilder: PageViewBuilder {
switch deps.quranContentStatePreferences.quranMode {
case .arabic: return deps.imageDataSourceBuilder
case .translation: return deps.translationDataSourceBuilder
}
}

private static func dictionaryFrom<K: Hashable, U>(_ array: [(K, U)]) -> [K: U] {
var dict: [K: U] = [:]
for element in array {
Expand All @@ -196,15 +203,14 @@ public final class ContentViewModel: ObservableObject {
return dict
}

private func configureAsInitialPage() {
private func configureInitialPage() {
deps.lastPageUpdater.configure(initialPage: input.initialPage, lastPage: input.lastPage)
reloadAllPages()
deps.highlightsService.highlights.searchVerses = [input.highlightingSearchAyah].compactMap { $0 }
highlights.searchVerses = [input.highlightingSearchAyah].compactMap { $0 }
}

private func visiblePagesUpdated() {
// remove search highlight when page changes
deps.highlightsService.highlights.searchVerses = []
highlights.searchVerses = []

let pages = visiblePages
let isTranslationView = deps.quranContentStatePreferences.quranMode == .translation
Expand All @@ -227,20 +233,11 @@ public final class ContentViewModel: ObservableObject {
deps.lastPageUpdater.updateTo(pages: pages)
}

private func reloadAllPages() {
switch deps.quranContentStatePreferences.quranMode {
case .arabic:
pageViewBuilder = deps.imageDataSourceBuilder
case .translation:
pageViewBuilder = deps.translationDataSourceBuilder
}
}

private func loadNotes() {
deps.noteService.notes(quran: deps.quran)
.map { notes in notes.flatMap { note in note.verses.map { ($0, note) } } }
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.deps.highlightsService.highlights.noteVerses = Self.dictionaryFrom($0) }
.sink { [weak self] in self?.highlights.noteVerses = Self.dictionaryFrom($0) }
.store(in: &cancellables)
}
}
Expand Down
39 changes: 19 additions & 20 deletions Features/QuranImageFeature/ContentImageBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,9 @@ public struct ContentImageBuilder: PageViewBuilder {
public init(container: AppDependencies, highlightsService: QuranHighlightsService) {
self.container = container
self.highlightsService = highlightsService
}

// MARK: Public

public func build() -> (Page) -> PageView {
let reading = ReadingPreferences.shared.reading
let readingDirectory = readingDirectory(reading)
let readingDirectory = Self.readingDirectory(reading, container: container)

let imageService = ImageDataService(
ayahInfoDatabase: reading.ayahInfoDatabase(in: readingDirectory),
Expand All @@ -42,34 +38,37 @@ public struct ContentImageBuilder: PageViewBuilder {
)

let pages = reading.quran.pages
let cacheableImageService = createCahceableImageService(imageService: imageService, pages: pages)
let cacheablePageMarkers = createPageMarkersService(imageService: imageService, reading: reading, pages: pages)

return { page in
let controller = ContentImageViewController(
page: page,
dataService: cacheableImageService,
pageMarkerService: cacheablePageMarkers,
highlightsService: highlightsService
)
return controller
}
cacheableImageService = Self.createCahceableImageService(imageService: imageService, pages: pages)
cacheablePageMarkers = Self.createPageMarkersService(imageService: imageService, reading: reading, pages: pages)
}

// MARK: Public

public func build(at page: Page) -> PageView {
ContentImageViewController(
page: page,
dataService: cacheableImageService,
pageMarkerService: cacheablePageMarkers,
highlightsService: highlightsService
)
}

// MARK: Private

private let container: AppDependencies
private let highlightsService: QuranHighlightsService
private let cacheableImageService: PagesCacheableService<Page, ImagePage>
private let cacheablePageMarkers: PagesCacheableService<Page, PageMarkers>?

private func readingDirectory(_ reading: Reading) -> URL {
private static func readingDirectory(_ reading: Reading, container: AppDependencies) -> URL {
let remoteResource = container.remoteResources?.resource(for: reading)
let remotePath = remoteResource?.downloadDestination.url
let bundlePath = { Bundle.main.url(forResource: reading.localPath, withExtension: nil) }
logger.info("Images: Use \(remoteResource != nil ? "remote" : "bundle") For reading \(reading)")
return remotePath ?? bundlePath()!
}

private func createCahceableImageService(imageService: ImageDataService, pages: [Page]) -> PagesCacheableService<Page, ImagePage> {
private static func createCahceableImageService(imageService: ImageDataService, pages: [Page]) -> PagesCacheableService<Page, ImagePage> {
let cache = Cache<Page, ImagePage>()
cache.countLimit = 5

Expand All @@ -86,7 +85,7 @@ public struct ContentImageBuilder: PageViewBuilder {
return dataService
}

private func createPageMarkersService(
private static func createPageMarkersService(
imageService: ImageDataService,
reading: Reading,
pages: [Page]
Expand Down
2 changes: 1 addition & 1 deletion Features/QuranPagesFeature/PageViewBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import UIKit

@MainActor
public protocol PageViewBuilder {
func build() -> (Page) -> PageView
func build(at page: Page) -> PageView
}

@MainActor
Expand Down
Loading
Loading