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 {