Files
SwiftUI-LifeGame/Root/Sources/Core/PatternService.swift
T
2022-04-27 13:49:36 +09:00

141 lines
4.6 KiB
Swift

//
// PatternService.swift
// LifeGameApp
//
// Created by Yusuke Hosonuma on 2020/09/12.
//
import Foundation
import Combine
import FirebaseFirestore
// Note:
//
// This service is designed by fail-safe,
// therefore some method call is return empty (publisher) that login is neeeded.
public struct PatternURL {
public let url: URL
public let stared: Bool
}
final public class PatternService {
public static let shared = PatternService()
private let authentication: Authentication = .shared
private let patternIDRepository: FirestorePatternIndexRepository = .shared
private var staredRepository: FirestoreStaredRepository? { authentication.repositories?.stared }
private var historyRepository: FirestoreHistoryRepository? { authentication.repositories?.history }
public func patternURLs(by type: String? = nil) -> AnyPublisher<[PatternURL], Never> {
patternIDRepository
.all
.map { document -> [URL] in
if let type = type {
// TODO: カテゴリ単位でドキュメントを分けているので、そこから個別に取得したほうが高速(になるはず)
return document.data.filter { $0.patternType == type }.map(\.jsonURL)
} else {
return document.data.map(\.jsonURL)
}
}
.combineLatest(listenStaredPatternURLs())
.map { allURLs, staredURLs in
allURLs.map {
PatternURL(url: $0, stared: staredURLs.contains($0))
}
}
.eraseToAnyPublisher()
}
public func fetch(from url: URL) -> AnyPublisher<PatternItem?, Never> {
let patternPublisher = URLSession.shared
.dataTaskPublisher(for: url)
.retry(3)
.map { (data, response) -> Data in
data
}
.decode(type: LifeWikiPattern.self, decoder: JSONDecoder())
return Publishers.Zip(patternPublisher, staredPatternIDs().setFailureType(to: Error.self))
.map { pattern, staredIds in
PatternItem(
patternID: pattern.id,
title: pattern.title,
board: pattern.makeBoard(),
stared: staredIds.contains(pattern.id)
)
}
.replaceError(with: nil) // TODO: エラーを投げる設計に変えたほうがいい
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
// MARK: - Star
public func staredPatternURLs(by type: String? = nil) -> AnyPublisher<[PatternURL], Never> {
guard let staredRepository = staredRepository else {
return Just([]).eraseToAnyPublisher()
}
return staredRepository
.all()
.map { urls in
urls.map { PatternURL(url: $0.jsonURL, stared: true) }
}
.eraseToAnyPublisher()
}
public func listenStaredPatternURLs() -> AnyPublisher<[URL], Never> {
guard let staredRepository = staredRepository else {
return Just([]).eraseToAnyPublisher()
}
return staredRepository
.publisher
.map { $0.map(\.jsonURL) }
.eraseToAnyPublisher()
}
public func toggleStar(item: PatternItem) {
guard let staredRepository = staredRepository else { return }
if item.stared {
staredRepository.delete(by: item.patternID)
} else {
staredRepository.setData(StaredDocument(patternID: item.patternID))
}
}
// MARK: - History
public func recordHistory(patternID: String) {
guard let historyRepository = historyRepository else { return }
historyRepository.setData(by: patternID, document: HistoryDocument())
}
public func listenHistoryPatternURLs() -> AnyPublisher<[URL], Never> {
guard let historyRepository = historyRepository else {
return Just([]).eraseToAnyPublisher()
}
return historyRepository
.publisher
.map { $0.map(\.jsonURL) }
.eraseToAnyPublisher()
}
// MARK: - Private
private func staredPatternIDs() -> AnyPublisher<[String], Never> {
guard let staredRepository = staredRepository else {
return Just([]).eraseToAnyPublisher()
}
return staredRepository // TODO: たぶん登録順でソートさせたくなるはず(あるいはタイトル?)
.all()
.map { $0.map(\.patternID) }
.eraseToAnyPublisher()
}
}