Skip to content

Commit

Permalink
Refactor ContentViewModel to have a single source of truth (#614)
Browse files Browse the repository at this point in the history
  • Loading branch information
mohamede1945 authored Dec 26, 2023
1 parent 85dcc7a commit 4535f8f
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 121 deletions.
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

0 comments on commit 4535f8f

Please sign in to comment.