Compare commits
6 Commits
1ce72b73db
...
aebd3a910d
Author | SHA1 | Date |
---|---|---|
Shadowfacts | aebd3a910d | |
Shadowfacts | 9f9a214b0a | |
Shadowfacts | a6d29b203c | |
Shadowfacts | c41a81414d | |
Shadowfacts | f320311a78 | |
Shadowfacts | 0d89a0f38f |
|
@ -131,6 +131,20 @@ public class FervorClient {
|
||||||
return try await performRequest(request)
|
return try await performRequest(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func read(ids: [FervorID]) async throws -> [FervorID] {
|
||||||
|
var request = URLRequest(url: buildURL(path: "/api/v1/items/read"))
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setURLEncodedBody(params: ["ids": ids.joined(separator: ",")])
|
||||||
|
return try await performRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func unread(ids: [FervorID]) async throws -> [FervorID] {
|
||||||
|
var request = URLRequest(url: buildURL(path: "/api/v1/items/unread"))
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setURLEncodedBody(params: ["ids": ids.joined(separator: ",")])
|
||||||
|
return try await performRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
public struct Auth {
|
public struct Auth {
|
||||||
public let accessToken: String
|
public let accessToken: String
|
||||||
public let refreshToken: String?
|
public let refreshToken: String?
|
||||||
|
|
|
@ -102,7 +102,7 @@ class PersistentContainer: NSPersistentContainer {
|
||||||
try await self.saveViewContext()
|
try await self.saveViewContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncItems(_ syncUpdate: ItemsSyncUpdate) async throws {
|
func syncItems(_ syncUpdate: ItemsSyncUpdate, setSyncState: @escaping (FervorController.SyncState) -> Void) async throws {
|
||||||
try await backgroundContext.perform {
|
try await backgroundContext.perform {
|
||||||
self.logger.debug("syncItems: deleting \(syncUpdate.delete.count, privacy: .public) items")
|
self.logger.debug("syncItems: deleting \(syncUpdate.delete.count, privacy: .public) items")
|
||||||
let deleteReq = Item.fetchRequest()
|
let deleteReq = Item.fetchRequest()
|
||||||
|
@ -122,7 +122,8 @@ class PersistentContainer: NSPersistentContainer {
|
||||||
let existing = try self.backgroundContext.fetch(req)
|
let existing = try self.backgroundContext.fetch(req)
|
||||||
self.logger.debug("syncItems: updating \(existing.count, privacy: .public) items, inserting \(syncUpdate.upsert.count - existing.count, privacy: .public)")
|
self.logger.debug("syncItems: updating \(existing.count, privacy: .public) items, inserting \(syncUpdate.upsert.count - existing.count, privacy: .public)")
|
||||||
// todo: this feels like it'll be slow when there are many items
|
// todo: this feels like it'll be slow when there are many items
|
||||||
for item in syncUpdate.upsert {
|
for (index, item) in syncUpdate.upsert.enumerated() {
|
||||||
|
setSyncState(.updateItems(current: index, total: syncUpdate.upsert.count))
|
||||||
if let existing = existing.first(where: { $0.id == item.id }) {
|
if let existing = existing.first(where: { $0.id == item.id }) {
|
||||||
existing.updateFromServer(item)
|
existing.updateFromServer(item)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -15,12 +15,13 @@ public struct ExcerptGenerator {
|
||||||
|
|
||||||
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ExcerptGenerator")
|
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ExcerptGenerator")
|
||||||
|
|
||||||
static func generateAll(_ fervorController: FervorController) {
|
static func generateAll(_ fervorController: FervorController) async {
|
||||||
let req = Item.fetchRequest()
|
let req = Item.fetchRequest()
|
||||||
req.predicate = NSPredicate(format: "generatedExcerpt = NO")
|
req.predicate = NSPredicate(format: "generatedExcerpt = NO")
|
||||||
req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
|
req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
|
||||||
req.fetchBatchSize = 50
|
req.fetchBatchSize = 50
|
||||||
fervorController.persistentContainer.performBackgroundTask { ctx in
|
let ctx = fervorController.persistentContainer.newBackgroundContext()
|
||||||
|
await ctx.perform {
|
||||||
guard let items = try? ctx.fetch(req) else { return }
|
guard let items = try? ctx.fetch(req) else { return }
|
||||||
var count = 0
|
var count = 0
|
||||||
for item in items {
|
for item in items {
|
||||||
|
@ -45,7 +46,7 @@ public struct ExcerptGenerator {
|
||||||
NSUpdatedObjectsKey: Array(updated)
|
NSUpdatedObjectsKey: Array(updated)
|
||||||
], into: [fervorController.persistentContainer.viewContext])
|
], into: [fervorController.persistentContainer.viewContext])
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Unable to save context: \(error.localizedDescription, privacy: .public)")
|
logger.error("Unable to save context: \(String(describing: error), privacy: .public)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Fervor
|
import Fervor
|
||||||
import OSLog
|
import OSLog
|
||||||
|
import Combine
|
||||||
|
|
||||||
class FervorController {
|
class FervorController {
|
||||||
|
|
||||||
|
@ -25,6 +26,8 @@ class FervorController {
|
||||||
|
|
||||||
private(set) var persistentContainer: PersistentContainer!
|
private(set) var persistentContainer: PersistentContainer!
|
||||||
|
|
||||||
|
@Published private(set) var syncState = SyncState.done
|
||||||
|
|
||||||
init(instanceURL: URL) {
|
init(instanceURL: URL) {
|
||||||
self.instanceURL = instanceURL
|
self.instanceURL = instanceURL
|
||||||
self.client = FervorClient(instanceURL: instanceURL, accessToken: nil)
|
self.client = FervorClient(instanceURL: instanceURL, accessToken: nil)
|
||||||
|
@ -42,6 +45,12 @@ class FervorController {
|
||||||
self.persistentContainer = PersistentContainer(account: account, fervorController: self)
|
self.persistentContainer = PersistentContainer(account: account, fervorController: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setSyncState(_ state: SyncState) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.syncState = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func register() async throws -> ClientRegistration {
|
func register() async throws -> ClientRegistration {
|
||||||
let registration = try await client.register(clientName: "Frenzy iOS", website: nil, redirectURI: FervorController.oauthRedirectURI)
|
let registration = try await client.register(clientName: "Frenzy iOS", website: nil, redirectURI: FervorController.oauthRedirectURI)
|
||||||
clientID = registration.clientID
|
clientID = registration.clientID
|
||||||
|
@ -56,49 +65,64 @@ class FervorController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncAll() async throws {
|
func syncAll() async throws {
|
||||||
|
guard syncState == .done else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// always return to .done, even if we throw and stop syncing early
|
||||||
|
defer { setSyncState(.done) }
|
||||||
|
|
||||||
|
setSyncState(.groupsAndFeeds)
|
||||||
|
|
||||||
logger.info("Syncing groups and feeds")
|
logger.info("Syncing groups and feeds")
|
||||||
async let groups = try client.groups()
|
async let groups = try client.groups()
|
||||||
async let feeds = try client.feeds()
|
async let feeds = try client.feeds()
|
||||||
try await persistentContainer.sync(serverGroups: groups, serverFeeds: feeds)
|
try await persistentContainer.sync(serverGroups: groups, serverFeeds: feeds)
|
||||||
|
|
||||||
|
setSyncState(.items)
|
||||||
|
|
||||||
let lastSync = try await persistentContainer.lastSyncDate()
|
let lastSync = try await persistentContainer.lastSyncDate()
|
||||||
logger.info("Syncing items with last sync date: \(String(describing: lastSync), privacy: .public)")
|
logger.info("Syncing items with last sync date: \(String(describing: lastSync), privacy: .public)")
|
||||||
let update = try await client.syncItems(lastSync: lastSync)
|
let update = try await client.syncItems(lastSync: lastSync)
|
||||||
try await persistentContainer.syncItems(update)
|
try await persistentContainer.syncItems(update, setSyncState: setSyncState(_:))
|
||||||
try await persistentContainer.updateLastSyncDate(update.syncTimestamp)
|
try await persistentContainer.updateLastSyncDate(update.syncTimestamp)
|
||||||
|
|
||||||
|
setSyncState(.excerpts)
|
||||||
|
await ExcerptGenerator.generateAll(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func syncReadToServer() async throws {
|
func syncReadToServer() async {
|
||||||
var count = 0
|
var count = 0
|
||||||
// todo: there should be a batch update api endpoint
|
|
||||||
for case let item as Item in persistentContainer.viewContext.updatedObjects {
|
|
||||||
let f = item.read ? client.read(item:) : client.unread(item:)
|
|
||||||
do {
|
|
||||||
let _ = try await f(item.id!)
|
|
||||||
count += 1
|
|
||||||
} catch {
|
|
||||||
logger.error("Failed to sync read state: \(error.localizedDescription, privacy: .public)")
|
|
||||||
item.needsReadStateSync = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to sync items which failed last time
|
// try to sync items which failed last time
|
||||||
let req = Item.fetchRequest()
|
let req = Item.fetchRequest()
|
||||||
req.predicate = NSPredicate(format: "needsReadStateSync = YES")
|
req.predicate = NSPredicate(format: "needsReadStateSync = YES")
|
||||||
if let needsSync = try? persistentContainer.viewContext.fetch(req) {
|
if var needsSync = try? persistentContainer.viewContext.fetch(req) {
|
||||||
for item in needsSync {
|
let firstReadIndex = needsSync.partition(by: \.read)
|
||||||
let f = item.read ? client.read(item:) : client.unread(item:)
|
let unreadIDs = needsSync[..<firstReadIndex].map(\.id.unsafelyUnwrapped)
|
||||||
|
let readIDs = needsSync[firstReadIndex...].map(\.id.unsafelyUnwrapped)
|
||||||
|
var updatedIDs = Set<FervorID>()
|
||||||
|
if !unreadIDs.isEmpty {
|
||||||
do {
|
do {
|
||||||
let _ = try await f(item.id!)
|
let ids = try await client.unread(ids: unreadIDs)
|
||||||
count += 1
|
updatedIDs.formUnion(ids)
|
||||||
item.needsReadStateSync = false
|
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to sync read state again: \(error.localizedDescription, privacy: .public)")
|
logger.error("Failed to sync unread state: \(String(describing: error), privacy: .public)")
|
||||||
item.needsReadStateSync = true
|
|
||||||
// todo: this should probably fail after a certain number of attempts
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !readIDs.isEmpty {
|
||||||
|
do {
|
||||||
|
let ids = try await client.read(ids: readIDs)
|
||||||
|
updatedIDs.formUnion(ids)
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to sync read state: \(String(describing: error), privacy: .public)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count += updatedIDs.count
|
||||||
|
for item in needsSync where updatedIDs.contains(item.id!) {
|
||||||
|
item.needsReadStateSync = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Synced \(count, privacy: .public) read/unread to server")
|
logger.info("Synced \(count, privacy: .public) read/unread to server")
|
||||||
|
@ -106,7 +130,7 @@ class FervorController {
|
||||||
do {
|
do {
|
||||||
try persistentContainer.viewContext.save()
|
try persistentContainer.viewContext.save()
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to save view context: \(error.localizedDescription, privacy: .public)")
|
logger.error("Failed to save view context: \(String(describing: error), privacy: .public)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +142,7 @@ class FervorController {
|
||||||
_ = try await f(item.id!)
|
_ = try await f(item.id!)
|
||||||
item.needsReadStateSync = false
|
item.needsReadStateSync = false
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to mark item (un)read: \(error.localizedDescription, privacy: .public)")
|
logger.error("Failed to mark item (un)read: \(String(describing: error), privacy: .public)")
|
||||||
item.needsReadStateSync = true
|
item.needsReadStateSync = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,9 +150,19 @@ class FervorController {
|
||||||
do {
|
do {
|
||||||
try persistentContainer.viewContext.save()
|
try persistentContainer.viewContext.save()
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to save view context: \(error.localizedDescription, privacy: .public)")
|
logger.error("Failed to save view context: \(String(describing: error), privacy: .public)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension FervorController {
|
||||||
|
enum SyncState: Equatable {
|
||||||
|
case groupsAndFeeds
|
||||||
|
case items
|
||||||
|
case updateItems(current: Int, total: Int)
|
||||||
|
case excerpts
|
||||||
|
case done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -81,11 +81,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
if let fervorController = fervorController {
|
if let fervorController = fervorController {
|
||||||
Task(priority: .userInitiated) {
|
Task(priority: .userInitiated) {
|
||||||
do {
|
await fervorController.syncReadToServer()
|
||||||
try await fervorController.syncReadToServer()
|
|
||||||
} catch {
|
|
||||||
logger.error("Unable to sync read state to server: \(error.localizedDescription, privacy: .public)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,12 +107,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
}
|
}
|
||||||
Task(priority: .userInitiated) {
|
Task(priority: .userInitiated) {
|
||||||
do {
|
do {
|
||||||
try await self.fervorController.syncAll()
|
try await fervorController.syncAll()
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Unable to sync from server: \(error.localizedDescription, privacy: .public)")
|
logger.error("Unable to sync from server: \(String(describing: error), privacy: .public)")
|
||||||
}
|
}
|
||||||
|
|
||||||
ExcerptGenerator.generateAll(fervorController)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,6 +124,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
window!.overrideUserInterfaceStyle = .dark
|
window!.overrideUserInterfaceStyle = .dark
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func switchToAccount(_ account: LocalData.Account) {
|
||||||
|
LocalData.mostRecentAccountID = account.id
|
||||||
|
fervorController = FervorController(account: account)
|
||||||
|
createAppUI()
|
||||||
|
syncFromServer()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,15 +148,6 @@ extension SceneDelegate: LoginViewControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SceneDelegate: HomeViewControllerDelegate {
|
|
||||||
func switchToAccount(_ account: LocalData.Account) {
|
|
||||||
LocalData.mostRecentAccountID = account.id
|
|
||||||
fervorController = FervorController(account: account)
|
|
||||||
createAppUI()
|
|
||||||
syncFromServer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if targetEnvironment(macCatalyst)
|
#if targetEnvironment(macCatalyst)
|
||||||
extension NSToolbarItem.Identifier {
|
extension NSToolbarItem.Identifier {
|
||||||
static let toggleItemRead = NSToolbarItem.Identifier("ToggleItemRead")
|
static let toggleItemRead = NSToolbarItem.Identifier("ToggleItemRead")
|
||||||
|
|
|
@ -49,6 +49,7 @@ class AppSplitViewController: UISplitViewController {
|
||||||
setViewController(secondaryNav, for: .secondary)
|
setViewController(secondaryNav, for: .secondary)
|
||||||
|
|
||||||
let home = HomeViewController(fervorController: fervorController)
|
let home = HomeViewController(fervorController: fervorController)
|
||||||
|
home.delegate = self
|
||||||
let nav = AppNavigationController(rootViewController: home)
|
let nav = AppNavigationController(rootViewController: home)
|
||||||
setViewController(nav, for: .compact)
|
setViewController(nav, for: .compact)
|
||||||
}
|
}
|
||||||
|
@ -61,3 +62,10 @@ extension AppSplitViewController: ItemsViewControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AppSplitViewController: HomeViewControllerDelegate {
|
||||||
|
func switchToAccount(_ account: LocalData.Account) {
|
||||||
|
if let delegate = view.window?.windowScene?.delegate as? SceneDelegate {
|
||||||
|
delegate.switchToAccount(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import CoreData
|
import CoreData
|
||||||
|
import Combine
|
||||||
|
|
||||||
protocol HomeViewControllerDelegate: AnyObject {
|
protocol HomeViewControllerDelegate: AnyObject {
|
||||||
func switchToAccount(_ account: LocalData.Account)
|
func switchToAccount(_ account: LocalData.Account)
|
||||||
|
@ -26,6 +27,10 @@ class HomeViewController: UIViewController {
|
||||||
private var groupResultsController: NSFetchedResultsController<Group>!
|
private var groupResultsController: NSFetchedResultsController<Group>!
|
||||||
private var feedResultsController: NSFetchedResultsController<Feed>!
|
private var feedResultsController: NSFetchedResultsController<Feed>!
|
||||||
|
|
||||||
|
private var lastSyncState = FervorController.SyncState.done
|
||||||
|
private var syncStateView: SyncStateView?
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(fervorController: FervorController) {
|
init(fervorController: FervorController) {
|
||||||
self.fervorController = fervorController
|
self.fervorController = fervorController
|
||||||
|
|
||||||
|
@ -87,6 +92,13 @@ class HomeViewController: UIViewController {
|
||||||
feedResultsController = NSFetchedResultsController(fetchRequest: feedReq, managedObjectContext: fervorController.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
|
feedResultsController = NSFetchedResultsController(fetchRequest: feedReq, managedObjectContext: fervorController.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
|
||||||
feedResultsController.delegate = self
|
feedResultsController.delegate = self
|
||||||
try! feedResultsController.performFetch()
|
try! feedResultsController.performFetch()
|
||||||
|
|
||||||
|
fervorController.$syncState
|
||||||
|
.debounce(for: .milliseconds(250), scheduler: RunLoop.main, options: nil)
|
||||||
|
.sink { [unowned self] in
|
||||||
|
self.syncStateChanged($0)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -136,6 +148,56 @@ class HomeViewController: UIViewController {
|
||||||
return dataSource
|
return dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func syncStateChanged(_ newState: FervorController.SyncState) {
|
||||||
|
if newState == .done && syncStateView == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateView(_ syncStateView: SyncStateView) {
|
||||||
|
switch newState {
|
||||||
|
case .groupsAndFeeds:
|
||||||
|
syncStateView.label.text = "Syncing groups and feeds"
|
||||||
|
case .items:
|
||||||
|
syncStateView.label.text = "Syncing items"
|
||||||
|
case .updateItems(current: let current, total: let total):
|
||||||
|
syncStateView.label.text = "Updating \(current + 1) of \(total) item\(total == 1 ? "" : "s")"
|
||||||
|
case .excerpts:
|
||||||
|
syncStateView.label.text = "Generating excerpts"
|
||||||
|
case .done:
|
||||||
|
syncStateView.label.text = "Done syncing"
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.25, delay: 1, options: .curveEaseIn) {
|
||||||
|
syncStateView.transform = CGAffineTransform(translationX: 0, y: syncStateView.bounds.height)
|
||||||
|
} completion: { _ in
|
||||||
|
syncStateView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let syncStateView = self.syncStateView {
|
||||||
|
updateView(syncStateView)
|
||||||
|
} else {
|
||||||
|
let syncStateView = SyncStateView()
|
||||||
|
self.syncStateView = syncStateView
|
||||||
|
syncStateView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(syncStateView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
syncStateView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
syncStateView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
syncStateView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
view.layoutIfNeeded()
|
||||||
|
syncStateView.transform = CGAffineTransform(translationX: 0, y: syncStateView.bounds.height)
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) {
|
||||||
|
syncStateView.transform = .identity
|
||||||
|
} completion: { _ in
|
||||||
|
updateView(syncStateView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HomeViewController {
|
extension HomeViewController {
|
||||||
|
@ -307,3 +369,39 @@ extension HomeViewController: LoginViewControllerDelegate {
|
||||||
(view.window!.windowScene!.delegate as! SceneDelegate).didLogin(with: controller)
|
(view.window!.windowScene!.delegate as! SceneDelegate).didLogin(with: controller)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SyncStateView: UIView {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
let blur = UIBlurEffect(style: .regular)
|
||||||
|
let blurView = UIVisualEffectView(effect: blur)
|
||||||
|
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(blurView)
|
||||||
|
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(label)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
|
||||||
|
label.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
|
||||||
|
label.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor),
|
||||||
|
|
||||||
|
safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 50),
|
||||||
|
])
|
||||||
|
|
||||||
|
layer.shadowColor = UIColor.black.cgColor
|
||||||
|
layer.shadowOpacity = 0.1
|
||||||
|
layer.shadowRadius = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue