diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents new file mode 100644 index 000000000..fd2b557b8 --- /dev/null +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CoreDataStack/CoreDataStack.h b/CoreDataStack/CoreDataStack.h new file mode 100644 index 000000000..2e729ae7f --- /dev/null +++ b/CoreDataStack/CoreDataStack.h @@ -0,0 +1,18 @@ +// +// CoreDataStack.h +// CoreDataStack +// +// Created by MainasuK Cirno on 2021/1/27. +// + +#import + +//! Project version number for CoreDataStack. +FOUNDATION_EXPORT double CoreDataStackVersionNumber; + +//! Project version string for CoreDataStack. +FOUNDATION_EXPORT const unsigned char CoreDataStackVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/CoreDataStack/CoreDataStack.swift b/CoreDataStack/CoreDataStack.swift new file mode 100644 index 000000000..d9046eafa --- /dev/null +++ b/CoreDataStack/CoreDataStack.swift @@ -0,0 +1,101 @@ +// +// CoreDataStack.swift +// CoreDataStack +// +// Created by Cirno MainasuK on 2021-1-27. +// + +import os +import Foundation +import CoreData + +public final class CoreDataStack { + + private(set) var storeDescriptions: [NSPersistentStoreDescription] + + init(persistentStoreDescriptions storeDescriptions: [NSPersistentStoreDescription]) { + self.storeDescriptions = storeDescriptions + } + + public convenience init(databaseName: String = "shared") { + let storeURL = URL.storeURL(for: "group.com.joinmastodon.mastodon-temp", databaseName: databaseName) + let storeDescription = NSPersistentStoreDescription(url: storeURL) + self.init(persistentStoreDescriptions: [storeDescription]) + } + + public private(set) lazy var persistentContainer: NSPersistentContainer = { + /* + The persistent container for the application. This implementation + creates and returns a container, having loaded the store for the + application to it. This property is optional since there are legitimate + error conditions that could cause the creation of the store to fail. + */ + let container = CoreDataStack.persistentContainer() + CoreDataStack.configure(persistentContainer: container, storeDescriptions: storeDescriptions) + CoreDataStack.load(persistentContainer: container) + + return container + }() + + static func persistentContainer() -> NSPersistentContainer { + let bundles = [Bundle(for: Toots.self)] + guard let managedObjectModel = NSManagedObjectModel.mergedModel(from: bundles) else { + fatalError("cannot locate bundles") + } + + let container = NSPersistentContainer(name: "CoreDataStack", managedObjectModel: managedObjectModel) + return container + } + + static func configure(persistentContainer container: NSPersistentContainer, storeDescriptions: [NSPersistentStoreDescription]) { + container.persistentStoreDescriptions = storeDescriptions + } + + static func load(persistentContainer container: NSPersistentContainer) { + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + if let reason = error.userInfo["reason"] as? String, + (reason == "Can't find mapping model for migration" || reason == "Persistent store migration failed, missing mapping model.") { + if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url { + try? container.persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: nil) + os_log("%{public}s[%{public}ld], %{public}s: cannot migrate model. rebuild database…", ((#file as NSString).lastPathComponent), #line, #function) + } else { + assertionFailure() + } + } + + fatalError("Unresolved error \(error), \(error.userInfo)") + } + + container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + + // it's looks like the remote notification only trigger when app enter and leave background + container.viewContext.automaticallyMergesChangesFromParent = true + + os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, storeDescription.debugDescription) + }) + } + +} + +extension CoreDataStack { + + public func rebuild() { + let oldStoreURL = persistentContainer.persistentStoreCoordinator.url(for: persistentContainer.persistentStoreCoordinator.persistentStores.first!) + try! persistentContainer.persistentStoreCoordinator.destroyPersistentStore(at: oldStoreURL, ofType: NSSQLiteStoreType, options: nil) + + CoreDataStack.load(persistentContainer: persistentContainer) + } + +} diff --git a/CoreDataStack/Entity/HomeTimelineIndex.swift b/CoreDataStack/Entity/HomeTimelineIndex.swift new file mode 100644 index 000000000..3eb0a28df --- /dev/null +++ b/CoreDataStack/Entity/HomeTimelineIndex.swift @@ -0,0 +1,64 @@ +// +// HomeTimelineIndex.swift +// CoreDataStack +// +// Created by MainasuK Cirno on 2021/1/27. +// + +import Foundation +import CoreData + +final class HomeTimelineIndex: NSManagedObject { + + public typealias ID = String + @NSManaged public private(set) var identifier: ID + @NSManaged public private(set) var domain: String + @NSManaged public private(set) var userIdentifier: String + + @NSManaged public private(set) var createdAt: Date + + // many-to-one relationship + @NSManaged public private(set) var toots: Toots + +} + +extension HomeTimelineIndex { + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + property: Property, + toots: Toots + ) -> HomeTimelineIndex { + let index: HomeTimelineIndex = context.insertObject() + + index.identifier = property.identifier + index.domain = property.domain + index.userIdentifier = toots.author.identifier + index.createdAt = toots.createdAt + + index.toots = toots + + return index + } + +} + +extension HomeTimelineIndex { + public struct Property { + public let identifier: String + public let domain: String + + public init(domain: String) { + self.identifier = UUID().uuidString + "@" + domain + self.domain = domain + } + } +} + +extension HomeTimelineIndex: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \HomeTimelineIndex.createdAt, ascending: false)] + } +} + diff --git a/CoreDataStack/Entity/MastodonUser.swift b/CoreDataStack/Entity/MastodonUser.swift new file mode 100644 index 000000000..c649ca014 --- /dev/null +++ b/CoreDataStack/Entity/MastodonUser.swift @@ -0,0 +1,96 @@ +// +// MastodonUser.swift +// CoreDataStack +// +// Created by MainasuK Cirno on 2021/1/27. +// + +import Foundation +import CoreData + +final class MastodonUser: NSManagedObject { + + public typealias ID = String + @NSManaged public private(set) var identifier: ID + @NSManaged public private(set) var domain: String + + @NSManaged public private(set) var id: String + @NSManaged public private(set) var acct: String + @NSManaged public private(set) var username: String + @NSManaged public private(set) var displayName: String? + + @NSManaged public private(set) var createdAt: Date + @NSManaged public private(set) var updatedAt: Date + + @NSManaged public private(set) var toots: Set? + +} + +extension MastodonUser { + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + property: Property + ) -> MastodonUser { + let user: MastodonUser = context.insertObject() + + user.identifier = property.identifier + user.domain = property.domain + + user.id = property.id + user.acct = property.acct + user.username = property.username + user.displayName = property.displayName + + user.createdAt = property.createdAt + user.updatedAt = property.networkDate + + return user + } + +} + +extension MastodonUser { + public struct Property { + public let identifier: String + public let domain: String + + public let id: String + public let acct: String + public let username: String + public let displayName: String? + + public let createdAt: Date + public let networkDate: Date + + public init( + id: String, + domain: String, + acct: String, + username: String, + displayName: String?, + content: String, + createdAt: Date, + networkDate: Date + ) { + self.identifier = id + "@" + domain + self.domain = domain + self.id = id + self.acct = acct + self.username = username + self.displayName = displayName.flatMap { displayName in + return displayName.isEmpty ? nil : displayName + } + self.createdAt = createdAt + self.networkDate = networkDate + } + } +} + +extension MastodonUser: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \MastodonUser.createdAt, ascending: false)] + } +} + diff --git a/CoreDataStack/Entity/Toots.swift b/CoreDataStack/Entity/Toots.swift new file mode 100644 index 000000000..ad5e64caa --- /dev/null +++ b/CoreDataStack/Entity/Toots.swift @@ -0,0 +1,88 @@ +// +// Toots.swift +// CoreDataStack +// +// Created by MainasuK Cirno on 2021/1/27. +// + +import Foundation +import CoreData + +final class Toots: NSManagedObject { + + public typealias ID = String + @NSManaged public private(set) var identifier: ID + @NSManaged public private(set) var domain: String + + @NSManaged public private(set) var id: String + @NSManaged public private(set) var content: String + + @NSManaged public private(set) var createdAt: Date + @NSManaged public private(set) var updatedAt: Date + + // many-to-one relationship + @NSManaged public private(set) var author: MastodonUser + + // one-to-many relationship + @NSManaged public private(set) var homeTimelineIndexes: Set? + +} + +extension Toots { + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + property: Property, + author: MastodonUser + ) -> Toots { + let toots: Toots = context.insertObject() + + toots.identifier = property.identifier + toots.domain = property.domain + + toots.id = property.id + toots.content = property.content + toots.createdAt = property.createdAt + toots.updatedAt = property.networkDate + + toots.author = author + + return toots + } + +} + +extension Toots { + public struct Property { + public let identifier: String + public let domain: String + + public let id: String + public let content: String + public let createdAt: Date + public let networkDate: Date + + public init( + id: String, + domain: String, + content: String, + createdAt: Date, + networkDate: Date + ) { + self.identifier = id + "@" + domain + self.domain = domain + self.id = id + self.content = content + self.createdAt = createdAt + self.networkDate = networkDate + } + } +} + +extension Toots: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \Toots.createdAt, ascending: false)] + } +} + diff --git a/CoreDataStack/Extension/Collection.swift b/CoreDataStack/Extension/Collection.swift new file mode 100644 index 000000000..1b2574b54 --- /dev/null +++ b/CoreDataStack/Extension/Collection.swift @@ -0,0 +1,30 @@ +// +// Collection.swift +// CoreDataStack +// +// Created by Cirno MainasuK on 2020-10-14. +// Copyright © 2020 Twidere. All rights reserved. +// + +import Foundation +import CoreData + +extension Collection where Iterator.Element: NSManagedObject { + public func fetchFaults() { + guard !self.isEmpty else { return } + guard let context = self.first?.managedObjectContext else { + fatalError("Managed object must have context") + } + let faults = self.filter { $0.isFault } + guard let object = faults.first else { return } + let request = NSFetchRequest() + request.entity = object.entity + request.returnsObjectsAsFaults = false + request.predicate = NSPredicate(format: "self in %@", faults) + do { + let _ = try context.fetch(request) + } catch { + assertionFailure(error.localizedDescription) + } + } +} diff --git a/CoreDataStack/Extension/NSManagedObjectContext.swift b/CoreDataStack/Extension/NSManagedObjectContext.swift new file mode 100644 index 000000000..159d4bd1e --- /dev/null +++ b/CoreDataStack/Extension/NSManagedObjectContext.swift @@ -0,0 +1,50 @@ +// +// NSManagedObjectContext.swift +// CoreDataStack +// +// Created by Cirno MainasuK on 2020-8-10. +// Copyright © 2020 Dimension. All rights reserved. +// + +import os +import Foundation +import Combine +import CoreData + +extension NSManagedObjectContext { + public func insert() -> T where T: Managed { + guard let object = NSEntityDescription.insertNewObject(forEntityName: T.entityName, into: self) as? T else { + fatalError("cannot insert object: \(T.self)") + } + + return object + } + + public func saveOrRollback() throws { + do { + guard hasChanges else { + return + } + try save() + } catch { + rollback() + + os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + throw error + } + } + + public func performChanges(block: @escaping () -> Void) -> Future, Never> { + Future { promise in + self.perform { + block() + do { + try self.saveOrRollback() + promise(.success(Result.success(()))) + } catch { + promise(.success(Result.failure(error))) + } + } + } + } +} diff --git a/CoreDataStack/Extension/URL.swift b/CoreDataStack/Extension/URL.swift new file mode 100644 index 000000000..b9cae4f5c --- /dev/null +++ b/CoreDataStack/Extension/URL.swift @@ -0,0 +1,23 @@ +// +// URL.swift +// CoreDataStack +// +// Created by Cirno MainasuK on 2021-1-27. +// + +import Foundation + +public extension URL { + + /// Returns a URL for the given app group and database pointing to the sqlite database. + static func storeURL(for appGroup: String, databaseName: String) -> URL { + guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { + fatalError("Shared file container could not be created.") + } + + return fileContainer + .appendingPathComponent("Databases", isDirectory: true) + .appendingPathComponent("\(databaseName).sqlite") + } + +} diff --git a/CoreDataStack/Info.plist b/CoreDataStack/Info.plist new file mode 100644 index 000000000..9bcb24442 --- /dev/null +++ b/CoreDataStack/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/CoreDataStack/Protocol/Managed.swift b/CoreDataStack/Protocol/Managed.swift new file mode 100644 index 000000000..3d297779b --- /dev/null +++ b/CoreDataStack/Protocol/Managed.swift @@ -0,0 +1,82 @@ +// +// Managed.swift +// CoreDataStack +// +// Created by Cirno MainasuK on 2020-8-6. +// Copyright © 2020 Dimension. All rights reserved. +// + +import Foundation +import CoreData + +public protocol Managed: class, NSFetchRequestResult { + static var entityName: String { get } + static var defaultSortDescriptors: [NSSortDescriptor] { get } +} + +extension Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [] + } + + public static var sortedFetchRequest: NSFetchRequest { + let request = NSFetchRequest(entityName: entityName) + request.sortDescriptors = defaultSortDescriptors + return request + } + +} + +extension NSManagedObjectContext { + public func insertObject() -> T where T: Managed { + guard let object = NSEntityDescription.insertNewObject(forEntityName: T.entityName, into: self) as? T else { + fatalError("Wrong object type") + } + + return object + } +} + +extension Managed where Self: NSManagedObject { + public static var entityName: String { return entity().name! } +} + +extension Managed where Self: NSManagedObject { + public static func findOrCreate(in context: NSManagedObjectContext, matching predicate: NSPredicate, configure: (Self) -> Void) -> Self { + guard let object = findOrFetch(in: context, matching: predicate) else { + let newObject: Self = context.insertObject() + configure(newObject) + return newObject + } + + return object + } + + public static func findOrFetch(in context: NSManagedObjectContext, matching predicate: NSPredicate) -> Self? { + guard let object = materializedObject(in: context, matching: predicate) else { + return fetch(in: context) { request in + request.predicate = predicate + request.returnsObjectsAsFaults = false + request.fetchLimit = 1 + }.first + } + + return object + } + + public static func materializedObject(in context: NSManagedObjectContext, matching predicate: NSPredicate) -> Self? { + for object in context.registeredObjects where !object.isFault { + guard let result = object as? Self, predicate.evaluate(with: result) else { continue } + return result + } + + return nil + } + + public static func fetch(in context: NSManagedObjectContext, configurationBlock: (NSFetchRequest) -> Void = { _ in }) -> [Self] { + let request = NSFetchRequest(entityName: Self.entityName) + configurationBlock(request) + return try! context.fetch(request) + } +} + diff --git a/CoreDataStack/Protocol/NetworkUpdatable.swift b/CoreDataStack/Protocol/NetworkUpdatable.swift new file mode 100644 index 000000000..15f728a03 --- /dev/null +++ b/CoreDataStack/Protocol/NetworkUpdatable.swift @@ -0,0 +1,12 @@ +// +// NetworkUpdatable.swift +// CoreDataStack +// +// Created by Cirno MainasuK on 2020-9-4. +// + +import Foundation + +public protocol NetworkUpdatable { + var networkDate: Date { get } +} diff --git a/CoreDataStackTests/CoreDataStackTests.swift b/CoreDataStackTests/CoreDataStackTests.swift new file mode 100644 index 000000000..7248e3b9a --- /dev/null +++ b/CoreDataStackTests/CoreDataStackTests.swift @@ -0,0 +1,33 @@ +// +// CoreDataStackTests.swift +// CoreDataStackTests +// +// Created by MainasuK Cirno on 2021/1/27. +// + +import XCTest +@testable import CoreDataStack + +class CoreDataStackTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/CoreDataStackTests/Info.plist b/CoreDataStackTests/Info.plist new file mode 100644 index 000000000..64d65ca49 --- /dev/null +++ b/CoreDataStackTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 75888b09e..8e71f12f2 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; 7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */; }; DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; - DB3D100025BAA6DA00EAA174 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3D0FFF25BAA6DA00EAA174 /* ViewController.swift */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB3D102425BAA7B400EAA174 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3D102225BAA7B400EAA174 /* Assets.swift */; }; DB3D102525BAA7B400EAA174 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3D102325BAA7B400EAA174 /* Strings.swift */; }; @@ -23,6 +22,29 @@ DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DE025BAA00100D1B89D /* LaunchScreen.storyboard */; }; DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; }; DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; }; + DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; }; + DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; }; + DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; }; + DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1125C1105C008580ED /* CoreDataStack.swift */; }; + DB89BA1B25C1107F008580ED /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1825C1107F008580ED /* Collection.swift */; }; + DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1925C1107F008580ED /* NSManagedObjectContext.swift */; }; + DB89BA1D25C1107F008580ED /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA1A25C1107F008580ED /* URL.swift */; }; + DB89BA2725C110B4008580ED /* Toots.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA2625C110B4008580ED /* Toots.swift */; }; + DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA3525C1145C008580ED /* CoreData.xcdatamodeld */; }; + DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA4125C1165F008580ED /* NetworkUpdatable.swift */; }; + DB89BA4425C1165F008580ED /* Managed.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89BA4225C1165F008580ED /* Managed.swift */; }; + DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52425C131D1002E6C99 /* MastodonUser.swift */; }; + DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */; }; + DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52C25C13561002E6C99 /* DocumentStore.swift */; }; + DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF52D25C13561002E6C99 /* AppContext.swift */; }; + DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54225C13647002E6C99 /* SceneCoordinator.swift */; }; + DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54325C13647002E6C99 /* NeedsDependency.swift */; }; + DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */; }; + DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55625C137A8002E6C99 /* HomeViewController.swift */; }; + DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; }; + DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,8 +62,43 @@ remoteGlobalIDString = DB427DD125BAA00100D1B89D; remoteInfo = Mastodon; }; + DB89B9F825C10FD0008580ED /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB89B9ED25C10FD0008580ED; + remoteInfo = CoreDataStack; + }; + DB89B9FA25C10FD0008580ED /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB427DD125BAA00100D1B89D; + remoteInfo = Mastodon; + }; + DB89BA0125C10FD0008580ED /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB89B9ED25C10FD0008580ED; + remoteInfo = CoreDataStack; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + DB89BA0825C10FD0008580ED /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = ""; }; 459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = ""; }; @@ -52,7 +109,6 @@ BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = ""; }; CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; }; - DB3D0FFF25BAA6DA00EAA174 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; DB3D102225BAA7B400EAA174 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; DB3D102325BAA7B400EAA174 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; @@ -69,6 +125,31 @@ DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastodonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = ""; }; DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = ""; }; + DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DB89B9F625C10FD0008580ED /* CoreDataStackTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoreDataStackTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStackTests.swift; sourceTree = ""; }; + DB89B9FF25C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DB89BA1025C10FF5008580ED /* Mastodon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mastodon.entitlements; sourceTree = ""; }; + DB89BA1125C1105C008580ED /* CoreDataStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = ""; }; + DB89BA1825C1107F008580ED /* Collection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; + DB89BA1925C1107F008580ED /* NSManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContext.swift; sourceTree = ""; }; + DB89BA1A25C1107F008580ED /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; + DB89BA2625C110B4008580ED /* Toots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toots.swift; sourceTree = ""; }; + DB89BA3625C1145C008580ED /* CoreData.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreData.xcdatamodel; sourceTree = ""; }; + DB89BA4125C1165F008580ED /* NetworkUpdatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkUpdatable.swift; sourceTree = ""; }; + DB89BA4225C1165F008580ED /* Managed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Managed.swift; sourceTree = ""; }; + DB8AF52425C131D1002E6C99 /* MastodonUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUser.swift; sourceTree = ""; }; + DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewStateStore.swift; sourceTree = ""; }; + DB8AF52C25C13561002E6C99 /* DocumentStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentStore.swift; sourceTree = ""; }; + DB8AF52D25C13561002E6C99 /* AppContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; + DB8AF54225C13647002E6C99 /* SceneCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneCoordinator.swift; sourceTree = ""; }; + DB8AF54325C13647002E6C99 /* NeedsDependency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeedsDependency.swift; sourceTree = ""; }; + DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = ""; }; + DB8AF55625C137A8002E6C99 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; + DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = ""; }; EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -77,6 +158,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, 7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */, DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, @@ -99,6 +181,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DB89B9EB25C10FD0008580ED /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DB89B9F325C10FD0008580ED /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -161,6 +258,8 @@ DB427DD425BAA00100D1B89D /* Mastodon */, DB427DEB25BAA00100D1B89D /* MastodonTests */, DB427DF625BAA00100D1B89D /* MastodonUITests */, + DB89B9EF25C10FD0008580ED /* CoreDataStack */, + DB89B9FC25C10FD0008580ED /* CoreDataStackTests */, DB427DD325BAA00100D1B89D /* Products */, 1EBA4F56E920856A3FC84ACB /* Pods */, 4E8E8B18DB8471A676012CF9 /* Frameworks */, @@ -173,6 +272,8 @@ DB427DD225BAA00100D1B89D /* Mastodon.app */, DB427DE825BAA00100D1B89D /* MastodonTests.xctest */, DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */, + DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */, + DB89B9F625C10FD0008580ED /* CoreDataStackTests.xctest */, ); name = Products; sourceTree = ""; @@ -180,8 +281,12 @@ DB427DD425BAA00100D1B89D /* Mastodon */ = { isa = PBXGroup; children = ( + DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, DB427DE325BAA00100D1B89D /* Info.plist */, - DB3D0FFF25BAA6DA00EAA174 /* ViewController.swift */, + DB8AF52A25C13561002E6C99 /* State */, + DB8AF56225C138BC002E6C99 /* Extension */, + DB8AF55525C1379F002E6C99 /* Scene */, + DB8AF54125C13647002E6C99 /* Coordinator */, DB3D101B25BAA79200EAA174 /* Generated */, DB3D0FF825BAA6B200EAA174 /* Resources */, DB3D0FF725BAA68500EAA174 /* Supporting Files */, @@ -207,8 +312,115 @@ path = MastodonUITests; sourceTree = ""; }; + DB89B9EF25C10FD0008580ED /* CoreDataStack */ = { + isa = PBXGroup; + children = ( + DB89B9F125C10FD0008580ED /* Info.plist */, + DB89B9F025C10FD0008580ED /* CoreDataStack.h */, + DB89BA1125C1105C008580ED /* CoreDataStack.swift */, + DB89BA3525C1145C008580ED /* CoreData.xcdatamodeld */, + DB89BA4025C1165F008580ED /* Protocol */, + DB89BA1725C1107F008580ED /* Extension */, + DB89BA2C25C110B7008580ED /* Entity */, + ); + path = CoreDataStack; + sourceTree = ""; + }; + DB89B9FC25C10FD0008580ED /* CoreDataStackTests */ = { + isa = PBXGroup; + children = ( + DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */, + DB89B9FF25C10FD0008580ED /* Info.plist */, + ); + path = CoreDataStackTests; + sourceTree = ""; + }; + DB89BA1725C1107F008580ED /* Extension */ = { + isa = PBXGroup; + children = ( + DB89BA1825C1107F008580ED /* Collection.swift */, + DB89BA1925C1107F008580ED /* NSManagedObjectContext.swift */, + DB89BA1A25C1107F008580ED /* URL.swift */, + ); + path = Extension; + sourceTree = ""; + }; + DB89BA2C25C110B7008580ED /* Entity */ = { + isa = PBXGroup; + children = ( + DB89BA2625C110B4008580ED /* Toots.swift */, + DB8AF52425C131D1002E6C99 /* MastodonUser.swift */, + DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */, + ); + path = Entity; + sourceTree = ""; + }; + DB89BA4025C1165F008580ED /* Protocol */ = { + isa = PBXGroup; + children = ( + DB89BA4125C1165F008580ED /* NetworkUpdatable.swift */, + DB89BA4225C1165F008580ED /* Managed.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + DB8AF52A25C13561002E6C99 /* State */ = { + isa = PBXGroup; + children = ( + DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */, + DB8AF52C25C13561002E6C99 /* DocumentStore.swift */, + DB8AF52D25C13561002E6C99 /* AppContext.swift */, + ); + path = State; + sourceTree = ""; + }; + DB8AF54125C13647002E6C99 /* Coordinator */ = { + isa = PBXGroup; + children = ( + DB8AF54225C13647002E6C99 /* SceneCoordinator.swift */, + DB8AF54325C13647002E6C99 /* NeedsDependency.swift */, + ); + path = Coordinator; + sourceTree = ""; + }; + DB8AF54E25C13703002E6C99 /* MainTab */ = { + isa = PBXGroup; + children = ( + DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */, + ); + path = MainTab; + sourceTree = ""; + }; + DB8AF55525C1379F002E6C99 /* Scene */ = { + isa = PBXGroup; + children = ( + DB8AF54E25C13703002E6C99 /* MainTab */, + DB8AF55625C137A8002E6C99 /* HomeViewController.swift */, + ); + path = Scene; + sourceTree = ""; + }; + DB8AF56225C138BC002E6C99 /* Extension */ = { + isa = PBXGroup; + children = ( + DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, + ); + path = Extension; + sourceTree = ""; + }; /* End PBXGroup section */ +/* Begin PBXHeadersBuildPhase section */ + DB89B9E925C10FD0008580ED /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + /* Begin PBXNativeTarget section */ DB427DD125BAA00100D1B89D /* Mastodon */ = { isa = PBXNativeTarget; @@ -220,10 +432,12 @@ DB427DD025BAA00100D1B89D /* Resources */, 5532CB85BBE168B25B20720B /* [CP] Embed Pods Frameworks */, DB3D100425BAA71500EAA174 /* ShellScript */, + DB89BA0825C10FD0008580ED /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( + DB89BA0225C10FD0008580ED /* PBXTargetDependency */, ); name = Mastodon; packageProductDependencies = ( @@ -273,6 +487,43 @@ productReference = DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + DB89B9ED25C10FD0008580ED /* CoreDataStack */ = { + isa = PBXNativeTarget; + buildConfigurationList = DB89BA0525C10FD0008580ED /* Build configuration list for PBXNativeTarget "CoreDataStack" */; + buildPhases = ( + DB89B9E925C10FD0008580ED /* Headers */, + DB89B9EA25C10FD0008580ED /* Sources */, + DB89B9EB25C10FD0008580ED /* Frameworks */, + DB89B9EC25C10FD0008580ED /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CoreDataStack; + productName = CoreDataStack; + productReference = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; + productType = "com.apple.product-type.framework"; + }; + DB89B9F525C10FD0008580ED /* CoreDataStackTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DB89BA0925C10FD0008580ED /* Build configuration list for PBXNativeTarget "CoreDataStackTests" */; + buildPhases = ( + DB89B9F225C10FD0008580ED /* Sources */, + DB89B9F325C10FD0008580ED /* Frameworks */, + DB89B9F425C10FD0008580ED /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DB89B9F925C10FD0008580ED /* PBXTargetDependency */, + DB89B9FB25C10FD0008580ED /* PBXTargetDependency */, + ); + name = CoreDataStackTests; + productName = CoreDataStackTests; + productReference = DB89B9F625C10FD0008580ED /* CoreDataStackTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -293,6 +544,14 @@ CreatedOnToolsVersion = 12.4; TestTargetID = DB427DD125BAA00100D1B89D; }; + DB89B9ED25C10FD0008580ED = { + CreatedOnToolsVersion = 12.4; + LastSwiftMigration = 1240; + }; + DB89B9F525C10FD0008580ED = { + CreatedOnToolsVersion = 12.4; + TestTargetID = DB427DD125BAA00100D1B89D; + }; }; }; buildConfigurationList = DB427DCD25BAA00100D1B89D /* Build configuration list for PBXProject "Mastodon" */; @@ -314,6 +573,8 @@ DB427DD125BAA00100D1B89D /* Mastodon */, DB427DE725BAA00100D1B89D /* MastodonTests */, DB427DF225BAA00100D1B89D /* MastodonUITests */, + DB89B9ED25C10FD0008580ED /* CoreDataStack */, + DB89B9F525C10FD0008580ED /* CoreDataStackTests */, ); }; /* End PBXProject section */ @@ -344,6 +605,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DB89B9EC25C10FD0008580ED /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DB89B9F425C10FD0008580ED /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -471,8 +746,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DB3D100025BAA6DA00EAA174 /* ViewController.swift in Sources */, + DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, + DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */, + DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, + DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, + DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, + DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */, + DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, + DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */, DB3D102525BAA7B400EAA174 /* Strings.swift in Sources */, DB3D102425BAA7B400EAA174 /* Assets.swift in Sources */, DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */, @@ -495,6 +777,31 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DB89B9EA25C10FD0008580ED /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */, + DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */, + DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */, + DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */, + DB89BA1B25C1107F008580ED /* Collection.swift in Sources */, + DB89BA2725C110B4008580ED /* Toots.swift in Sources */, + DB89BA4425C1165F008580ED /* Managed.swift in Sources */, + DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */, + DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */, + DB89BA1D25C1107F008580ED /* URL.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DB89B9F225C10FD0008580ED /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -508,6 +815,21 @@ target = DB427DD125BAA00100D1B89D /* Mastodon */; targetProxy = DB427DF425BAA00100D1B89D /* PBXContainerItemProxy */; }; + DB89B9F925C10FD0008580ED /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB89B9ED25C10FD0008580ED /* CoreDataStack */; + targetProxy = DB89B9F825C10FD0008580ED /* PBXContainerItemProxy */; + }; + DB89B9FB25C10FD0008580ED /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB427DD125BAA00100D1B89D /* Mastodon */; + targetProxy = DB89B9FA25C10FD0008580ED /* PBXContainerItemProxy */; + }; + DB89BA0225C10FD0008580ED /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB89B9ED25C10FD0008580ED /* CoreDataStack */; + targetProxy = DB89BA0125C10FD0008580ED /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -589,7 +911,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -644,7 +966,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -658,8 +980,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; @@ -679,8 +1003,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; @@ -780,6 +1106,103 @@ }; name = Release; }; + DB89BA0625C10FD0008580ED /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 7LFDZ96332; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = CoreDataStack/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.CoreDataStack; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + DB89BA0725C10FD0008580ED /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 7LFDZ96332; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = CoreDataStack/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.CoreDataStack; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + DB89BA0A25C10FD0008580ED /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7LFDZ96332; + INFOPLIST_FILE = CoreDataStackTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.CoreDataStackTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mastodon.app/Mastodon"; + }; + name = Debug; + }; + DB89BA0B25C10FD0008580ED /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7LFDZ96332; + INFOPLIST_FILE = CoreDataStackTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.CoreDataStackTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mastodon.app/Mastodon"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -819,6 +1242,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DB89BA0525C10FD0008580ED /* Build configuration list for PBXNativeTarget "CoreDataStack" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DB89BA0625C10FD0008580ED /* Debug */, + DB89BA0725C10FD0008580ED /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DB89BA0925C10FD0008580ED /* Build configuration list for PBXNativeTarget "CoreDataStackTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DB89BA0A25C10FD0008580ED /* Debug */, + DB89BA0B25C10FD0008580ED /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -843,6 +1284,19 @@ productName = AlamofireImage; }; /* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + DB89BA3525C1145C008580ED /* CoreData.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + DB89BA3625C1145C008580ED /* CoreData.xcdatamodel */, + ); + currentVersion = DB89BA3625C1145C008580ED /* CoreData.xcdatamodel */; + path = CoreData.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = DB427DCA25BAA00100D1B89D /* Project object */; } diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 7e0fb56e0..5c1dc5b90 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -4,6 +4,11 @@ SchemeUserState + CoreDataStack.xcscheme_^#shared#^_ + + orderHint + 6 + Mastodon.xcscheme_^#shared#^_ orderHint diff --git a/Mastodon/Coordinator/NeedsDependency.swift b/Mastodon/Coordinator/NeedsDependency.swift new file mode 100644 index 000000000..70421a822 --- /dev/null +++ b/Mastodon/Coordinator/NeedsDependency.swift @@ -0,0 +1,28 @@ +// +// NeedsDependency.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-1-27. +// + +import UIKit + +protocol NeedsDependency: class { + var context: AppContext! { get set } + var coordinator: SceneCoordinator! { get set } +} + +extension UISceneSession { + private struct AssociatedKeys { + static var sceneCoordinator = "SceneCoordinator" + } + + weak var sceneCoordinator: SceneCoordinator? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.sceneCoordinator) as? SceneCoordinator + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.sceneCoordinator, newValue, .OBJC_ASSOCIATION_ASSIGN) + } + } +} diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift new file mode 100644 index 000000000..600846d4c --- /dev/null +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -0,0 +1,124 @@ +// +// SceneCoordinator.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-1-27. + +import UIKit +import SafariServices + +final public class SceneCoordinator { + + private weak var scene: UIScene! + private weak var sceneDelegate: SceneDelegate! + private weak var appContext: AppContext! + + let id = UUID().uuidString + + init(scene: UIScene, sceneDelegate: SceneDelegate, appContext: AppContext) { + self.scene = scene + self.sceneDelegate = sceneDelegate + self.appContext = appContext + + scene.session.sceneCoordinator = self + } +} + +extension SceneCoordinator { + enum Transition { + case show // push + case showDetail // replace + case modal(animated: Bool, completion: (() -> Void)? = nil) + case custom(transitioningDelegate: UIViewControllerTransitioningDelegate) + case customPush + case safariPresent(animated: Bool, completion: (() -> Void)? = nil) + case activityViewControllerPresent(animated: Bool, completion: (() -> Void)? = nil) + case alertController(animated: Bool, completion: (() -> Void)? = nil) + } + + enum Scene { + + } +} + +extension SceneCoordinator { + + func setup() { + let viewController = MainTabBarController(context: appContext, coordinator: self) + sceneDelegate.window?.rootViewController = viewController + } + + @discardableResult + func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? { + guard let viewController = get(scene: scene) else { + return nil + } + guard var presentingViewController = sender ?? sceneDelegate.window?.rootViewController?.topMost else { + return nil + } + + if let mainTabBarController = presentingViewController as? MainTabBarController, + let navigationController = mainTabBarController.selectedViewController as? UINavigationController, + let topViewController = navigationController.topViewController { + presentingViewController = topViewController + } + + switch transition { + case .show: + presentingViewController.show(viewController, sender: sender) + + case .showDetail: + let navigationController = UINavigationController(rootViewController: viewController) + presentingViewController.showDetailViewController(navigationController, sender: sender) + + case .modal(let animated, let completion): + let modalNavigationController = UINavigationController(rootViewController: viewController) + if let adaptivePresentationControllerDelegate = viewController as? UIAdaptivePresentationControllerDelegate { + modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate + } + presentingViewController.present(modalNavigationController, animated: animated, completion: completion) + + case .custom(let transitioningDelegate): + viewController.modalPresentationStyle = .custom + viewController.transitioningDelegate = transitioningDelegate + sender?.present(viewController, animated: true, completion: nil) + + case .customPush: + // set delegate in view controller + assert(sender?.navigationController?.delegate != nil) + sender?.navigationController?.pushViewController(viewController, animated: true) + + case .safariPresent(let animated, let completion): + presentingViewController.present(viewController, animated: animated, completion: completion) + + case .activityViewControllerPresent(let animated, let completion): + presentingViewController.present(viewController, animated: animated, completion: completion) + + case .alertController(let animated, let completion): + presentingViewController.present(viewController, animated: animated, completion: completion) + } + + return viewController + } + +} + +private extension SceneCoordinator { + + func get(scene: Scene) -> UIViewController? { + let viewController: UIViewController? + + // TODO: + viewController = nil + + setupDependency(for: viewController as? NeedsDependency) + + return viewController + } + + private func setupDependency(for needs: NeedsDependency?) { + needs?.context = appContext + needs?.coordinator = self + } + +} diff --git a/Mastodon/Extension/UIViewController.swift b/Mastodon/Extension/UIViewController.swift new file mode 100644 index 000000000..c3782fa14 --- /dev/null +++ b/Mastodon/Extension/UIViewController.swift @@ -0,0 +1,67 @@ +// +// UIViewController.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-1-27. +// + +import UIKit + +extension UIViewController { + + /// Returns the top most view controller from given view controller's stack. + var topMost: UIViewController? { + // presented view controller + if let presentedViewController = presentedViewController { + return presentedViewController.topMost + } + + // UITabBarController + if let tabBarController = self as? UITabBarController, + let selectedViewController = tabBarController.selectedViewController { + return selectedViewController.topMost + } + + // UINavigationController + if let navigationController = self as? UINavigationController, + let visibleViewController = navigationController.visibleViewController { + return visibleViewController.topMost + } + + // UIPageController + if let pageViewController = self as? UIPageViewController, + pageViewController.viewControllers?.count == 1 { + return pageViewController.viewControllers?.first?.topMost ?? self + } + + // child view controller + for subview in self.view?.subviews ?? [] { + if let childViewController = subview.next as? UIViewController { + return childViewController.topMost + } + } + + return self + } + +} + +extension UIViewController { + + /// https://bluelemonbits.com/2018/08/26/inserting-cells-at-the-top-of-a-uitableview-with-no-scrolling/ + static func topVisibleTableViewCellIndexPath(in tableView: UITableView, navigationBar: UINavigationBar) -> IndexPath? { + let navigationBarRectInTableView = tableView.convert(navigationBar.bounds, from: navigationBar) + let navigationBarMaxYPosition = CGPoint(x: 0, y: navigationBarRectInTableView.origin.y + navigationBarRectInTableView.size.height + 1) // +1pt for UIKit cell locate + let mostTopVisiableIndexPath = tableView.indexPathForRow(at: navigationBarMaxYPosition) + return mostTopVisiableIndexPath + } + + static func tableViewCellOriginOffsetToWindowTop(in tableView: UITableView, at indexPath: IndexPath, navigationBar: UINavigationBar) -> CGFloat { + let rectForTopRow = tableView.rectForRow(at: indexPath) + let navigationBarRectInTableView = tableView.convert(navigationBar.bounds, from: navigationBar) + let navigationBarMaxYPosition = CGPoint(x: 0, y: navigationBarRectInTableView.origin.y + navigationBarRectInTableView.size.height) // without +1pt + let differenceBetweenTopRowAndNavigationBar = rectForTopRow.origin.y - navigationBarMaxYPosition.y + return differenceBetweenTopRowAndNavigationBar + } + +} diff --git a/Mastodon/Mastodon.entitlements b/Mastodon/Mastodon.entitlements new file mode 100644 index 000000000..d334a5e6d --- /dev/null +++ b/Mastodon/Mastodon.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.joinmastodon.mastodon-temp + + + diff --git a/Mastodon/Scene/HomeViewController.swift b/Mastodon/Scene/HomeViewController.swift new file mode 100644 index 000000000..6a533558d --- /dev/null +++ b/Mastodon/Scene/HomeViewController.swift @@ -0,0 +1,27 @@ +// +// HomeViewController.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021/1/27. +// + +import UIKit + +final class HomeViewController: UIViewController, NeedsDependency { + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + +} + + +extension HomeViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Home" + view.backgroundColor = .systemBackground + } + +} diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift new file mode 100644 index 000000000..f15db318b --- /dev/null +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -0,0 +1,89 @@ +// +// MainTabBarController.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-1-27. +// + +import os.log +import UIKit +import Combine +import SafariServices + +class MainTabBarController: UITabBarController { + + var disposeBag = Set() + + weak var context: AppContext! + weak var coordinator: SceneCoordinator! + + enum Tab: Int, CaseIterable { + case home + + + var title: String { + switch self { + case .home: return "Home" + } + } + + var image: UIImage { + switch self { + case .home: return UIImage(systemName: "house")! + } + } + + func viewController(context: AppContext, coordinator: SceneCoordinator) -> UIViewController { + let viewController: UIViewController + switch self { + case .home: + let _viewController = HomeViewController() + _viewController.context = context + _viewController.coordinator = coordinator + viewController = _viewController + } + viewController.title = self.title + return UINavigationController(rootViewController: viewController) + } + } + + init(context: AppContext, coordinator: SceneCoordinator) { + self.context = context + self.coordinator = coordinator + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +extension MainTabBarController { + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + let tabs = Tab.allCases + let viewControllers: [UIViewController] = tabs.map { tab in + let viewController = tab.viewController(context: context, coordinator: coordinator) + viewController.tabBarItem.title = "" // set text to empty string for image only style (SDK failed to layout when set to nil) + viewController.tabBarItem.image = tab.image + return viewController + } + setViewControllers(viewControllers, animated: false) + selectedIndex = 0 + + // TODO: custom accent color + let tabBarAppearance = UITabBarAppearance() + tabBarAppearance.configureWithDefaultBackground() + tabBar.standardAppearance = tabBarAppearance + + #if DEBUG + // selectedIndex = 1 + #endif + } + +} diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift new file mode 100644 index 000000000..0742597c4 --- /dev/null +++ b/Mastodon/State/AppContext.swift @@ -0,0 +1,56 @@ +// +// AppContext.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-1-27. +// + +import os.log +import UIKit +import Combine +import CoreData +import CoreDataStack + +class AppContext: ObservableObject { + + var disposeBag = Set() + + @Published var viewStateStore = ViewStateStore() + + let coreDataStack: CoreDataStack + let managedObjectContext: NSManagedObjectContext + let backgroundManagedObjectContext: NSManagedObjectContext + + + let documentStore: DocumentStore + private var documentStoreSubscription: AnyCancellable! + + let overrideTraitCollection = CurrentValueSubject(nil) + + init() { + let _coreDataStack = CoreDataStack() + let _managedObjectContext = _coreDataStack.persistentContainer.viewContext + let _backgroundManagedObjectContext = _coreDataStack.persistentContainer.newBackgroundContext() + coreDataStack = _coreDataStack + managedObjectContext = _managedObjectContext + backgroundManagedObjectContext = _backgroundManagedObjectContext + + documentStore = DocumentStore() + documentStoreSubscription = documentStore.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + self.objectWillChange.send() + } + + backgroundManagedObjectContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave, object: backgroundManagedObjectContext) + .sink { [weak self] notification in + guard let self = self else { return } + self.managedObjectContext.perform { + self.managedObjectContext.mergeChanges(fromContextDidSave: notification) + } + } + .store(in: &disposeBag) + } + +} diff --git a/Mastodon/State/DocumentStore.swift b/Mastodon/State/DocumentStore.swift new file mode 100644 index 000000000..b39a29245 --- /dev/null +++ b/Mastodon/State/DocumentStore.swift @@ -0,0 +1,11 @@ +// +// DocumentStore.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-1-27. +// + +import UIKit +import Combine + +class DocumentStore: ObservableObject { } diff --git a/Mastodon/State/ViewStateStore.swift b/Mastodon/State/ViewStateStore.swift new file mode 100644 index 000000000..07d8b844f --- /dev/null +++ b/Mastodon/State/ViewStateStore.swift @@ -0,0 +1,14 @@ +// +// ViewStateStore.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-1-27. +// + +import Combine + +struct ViewStateStore { + +} + +enum ViewState { } diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 1a2730211..78c24f21f 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -10,7 +10,7 @@ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - + let appContext = AppContext() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. @@ -34,3 +34,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } + +extension AppDelegate { + func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { + #if DEBUG + return .all + #else + return UIDevice.current.userInterfaceIdiom == .pad ? .all : .portrait + #endif + } +} + + +extension AppContext { + static var shared: AppContext { + let appDelegate = UIApplication.shared.delegate as! AppDelegate + return appDelegate.appContext + } +} diff --git a/Mastodon/Supporting Files/Base.lproj/Main.storyboard b/Mastodon/Supporting Files/Base.lproj/Main.storyboard index 25a763858..e111225fd 100644 --- a/Mastodon/Supporting Files/Base.lproj/Main.storyboard +++ b/Mastodon/Supporting Files/Base.lproj/Main.storyboard @@ -1,24 +1,33 @@ - + + - + + + - + - + - + + + + + + + diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 52855a055..ed7a36994 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -10,13 +10,22 @@ import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - + var coordinator: SceneCoordinator? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + guard let windowScene = scene as? UIWindowScene else { return } + + let window = UIWindow(windowScene: windowScene) + self.window = window + + + let appContext = AppContext.shared + let sceneCoordinator = SceneCoordinator(scene: scene, sceneDelegate: self, appContext: appContext) + self.coordinator = sceneCoordinator + + sceneCoordinator.setup() + + window.makeKeyAndVisible() } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/Mastodon/ViewController.swift b/Mastodon/ViewController.swift deleted file mode 100644 index a90e181b7..000000000 --- a/Mastodon/ViewController.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ViewController.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021/1/22. -// - -import UIKit -import MastodonSDK - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - } - - -} - diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift index 5bffa018f..c555f5046 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift @@ -9,10 +9,14 @@ import Combine import Foundation public extension Mastodon.API.App { - + static func appEndpointURL(domain: String) -> URL { return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("apps") } + +} + +extension Mastodon.API.App { struct Application: Codable {