diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 980d659ed..c9e20acc8 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 8471A2C41ED4CEBF008F099E /* DataModel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8471A2B71ED4CEAD008F099E /* DataModel.framework */; }; + 8471A2C51ED4CEBF008F099E /* DataModel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8471A2B71ED4CEAD008F099E /* DataModel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 849C64641ED37A5D003D8FC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849C64631ED37A5D003D8FC0 /* AppDelegate.swift */; }; 849C64661ED37A5D003D8FC0 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849C64651ED37A5D003D8FC0 /* ViewController.swift */; }; 849C64681ED37A5D003D8FC0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 849C64671ED37A5D003D8FC0 /* Assets.xcassets */; }; @@ -31,6 +33,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 8471A2B61ED4CEAD008F099E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8471A2B21ED4CEAD008F099E /* DataModel.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 84C7AE921D68C558009FB883; + remoteInfo = DataModel; + }; + 8471A2C61ED4CEBF008F099E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8471A2B21ED4CEAD008F099E /* DataModel.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 84C7AE911D68C558009FB883; + remoteInfo = DataModel; + }; 849C64721ED37A5D003D8FC0 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 849C64581ED37A5D003D8FC0 /* Project object */; @@ -249,6 +265,7 @@ 84B06FFE1ED3818D00F0B54B /* RSTree.framework in Embed Frameworks */, 84B06FAF1ED37DBD00F0B54B /* RSCore.framework in Embed Frameworks */, 84B06FD01ED37F7D00F0B54B /* DB5.framework in Embed Frameworks */, + 8471A2C51ED4CEBF008F099E /* DataModel.framework in Embed Frameworks */, 84B06FC31ED37E9600F0B54B /* RSWeb.framework in Embed Frameworks */, 84B06F831ED37BDD00F0B54B /* RSXML.framework in Embed Frameworks */, ); @@ -258,6 +275,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 8471A2B21ED4CEAD008F099E /* DataModel.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = DataModel.xcodeproj; path = Frameworks/DataModel/DataModel.xcodeproj; sourceTree = ""; }; 849C64601ED37A5D003D8FC0 /* Evergreen.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Evergreen.app; sourceTree = BUILT_PRODUCTS_DIR; }; 849C64631ED37A5D003D8FC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Evergreen/AppDelegate.swift; sourceTree = ""; }; 849C64651ED37A5D003D8FC0 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ViewController.swift; path = Evergreen/ViewController.swift; sourceTree = ""; }; @@ -288,6 +306,7 @@ 84B06FFD1ED3818D00F0B54B /* RSTree.framework in Frameworks */, 84B06FAE1ED37DBD00F0B54B /* RSCore.framework in Frameworks */, 84B06FCF1ED37F7D00F0B54B /* DB5.framework in Frameworks */, + 8471A2C41ED4CEBF008F099E /* DataModel.framework in Frameworks */, 84B06FC21ED37E9600F0B54B /* RSWeb.framework in Frameworks */, 84B06F821ED37BDD00F0B54B /* RSXML.framework in Frameworks */, ); @@ -303,6 +322,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 8471A2B31ED4CEAD008F099E /* Products */ = { + isa = PBXGroup; + children = ( + 8471A2B71ED4CEAD008F099E /* DataModel.framework */, + ); + name = Products; + sourceTree = ""; + }; 849C64571ED37A5D003D8FC0 = { isa = PBXGroup; children = ( @@ -313,6 +340,7 @@ 849C646C1ED37A5D003D8FC0 /* Info.plist */, 849C64741ED37A5D003D8FC0 /* EvergreenTests */, 849C64611ED37A5D003D8FC0 /* Products */, + 8471A2B21ED4CEAD008F099E /* DataModel.xcodeproj */, 84B06FC61ED37F7200F0B54B /* DB5.xcodeproj */, 84B06FA21ED37DAC00F0B54B /* RSCore.xcodeproj */, 84B06F961ED37DA000F0B54B /* RSDatabase.xcodeproj */, @@ -441,6 +469,7 @@ 84B06FEC1ED3803A00F0B54B /* PBXTargetDependency */, 84B070001ED3818D00F0B54B /* PBXTargetDependency */, 84B0700D1ED3822600F0B54B /* PBXTargetDependency */, + 8471A2C71ED4CEBF008F099E /* PBXTargetDependency */, ); name = Evergreen; productName = Evergreen; @@ -500,6 +529,10 @@ productRefGroup = 849C64611ED37A5D003D8FC0 /* Products */; projectDirPath = ""; projectReferences = ( + { + ProductGroup = 8471A2B31ED4CEAD008F099E /* Products */; + ProjectRef = 8471A2B21ED4CEAD008F099E /* DataModel.xcodeproj */; + }, { ProductGroup = 84B06FC71ED37F7200F0B54B /* Products */; ProjectRef = 84B06FC61ED37F7200F0B54B /* DB5.xcodeproj */; @@ -542,6 +575,13 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ + 8471A2B71ED4CEAD008F099E /* DataModel.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = DataModel.framework; + remoteRef = 8471A2B61ED4CEAD008F099E /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 84B06F7D1ED37BCA00F0B54B /* RSXML.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; @@ -724,6 +764,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 8471A2C71ED4CEBF008F099E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = DataModel; + targetProxy = 8471A2C61ED4CEBF008F099E /* PBXContainerItemProxy */; + }; 849C64731ED37A5D003D8FC0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 849C645F1ED37A5D003D8FC0 /* Evergreen */; diff --git a/Frameworks/DataModel/AccountProtocol.swift b/Frameworks/DataModel/AccountProtocol.swift new file mode 100644 index 000000000..e7f1ded64 --- /dev/null +++ b/Frameworks/DataModel/AccountProtocol.swift @@ -0,0 +1,44 @@ +// +// AccountProtocol.swift +// Rainier +// +// Created by Brent Simmons on 4/17/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public protocol Account: class, Folder { + + var identifier: String {get} + var type: String {get} + var refreshInProgress: Bool {get} + + init(settingsFile: String, dataFolder: String, identifier: String) + + func refreshAll() + + func markArticles(_ articles: NSSet, statusKey: ArticleStatusKey, flag: Bool) + + func hasFeedWithURLString(_: String) -> Bool + + func importOPML(_: Any) + + func fetchArticles(for: [AnyObject]) -> [Article] +} + +public extension Account { + + func hasFeedWithURLString(_ urlString: String) -> Bool { + + if let _ = existingFeedWithURL(urlString) { + return true + } + return false + } + + public func postArticleStatusesDidChangeNotification(_ articles: NSSet) { + + NotificationCenter.default.post(name: .ArticleStatusesDidChange, object: self, userInfo: [articlesKey: articles]) + } +} diff --git a/Frameworks/DataModel/ArticleProtocol.swift b/Frameworks/DataModel/ArticleProtocol.swift new file mode 100644 index 000000000..c0b65f334 --- /dev/null +++ b/Frameworks/DataModel/ArticleProtocol.swift @@ -0,0 +1,68 @@ +// +// ArticleProtocol.swift +// Rainier +// +// Created by Brent Simmons on 4/23/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public protocol Article: class { + + var account: Account? {get} + var feedID: String {get} + var feed: Feed? {get} + var articleID: String {get} + var status: ArticleStatus! {get} + + var guid: String? {get} + var title: String? {get} + var body: String? {get} + var link: String? {get} + var permalink: String? {get} + var author: String? {get} + + var datePublished: Date? {get} + var logicalDatePublished: Date {get} //datePublished or something reasonable. + var dateModified: Date? {get} +} + +public extension Article { + + var feed: Feed? { + get { + return account?.existingFeedWithID(feedID) + } + } + + var logicalDatePublished: Date { + get { + if let d = datePublished { + return d + } + if let d = dateModified { + return d + } + return status.dateArrived as Date + } + } +} + +public func articleArraysAreIdentical(array1: [Article], array2: [Article]) -> Bool { + + if array1.count != array2.count { + return false + } + + var index = 0 + for oneItem in array1 { + if oneItem !== array2[index] { + return false + } + index = index + 1 + } + + return true +} + diff --git a/Frameworks/DataModel/ArticleStatusProtocol.swift b/Frameworks/DataModel/ArticleStatusProtocol.swift new file mode 100644 index 000000000..d9393acd4 --- /dev/null +++ b/Frameworks/DataModel/ArticleStatusProtocol.swift @@ -0,0 +1,43 @@ +// +// ArticleStatusProtocol.swift +// Rainier +// +// Created by Brent Simmons on 4/23/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public enum ArticleStatusKey: String { + + case read = "read" + case starred = "starred" + case userDeleted = "userDeleted" +} + +public protocol ArticleStatus { + + var read: Bool {get set} + var starred: Bool {get set} + var userDeleted: Bool {get set} + var dateArrived: Date {get} + + func boolStatusForKey(_ articleStatusKey: ArticleStatusKey) -> Bool + func setBoolStatusForKey(_ status: Bool, articleStatusKey: ArticleStatusKey) +} + +public extension ArticleStatus { + + func boolStatusForKey(_ articleStatusKey: ArticleStatusKey) -> Bool { + + switch articleStatusKey { + + case .read: + return read + case .starred: + return starred + case .userDeleted: + return userDeleted + } + } +} diff --git a/Frameworks/DataModel/BatchUpdates.swift b/Frameworks/DataModel/BatchUpdates.swift new file mode 100644 index 000000000..8fce575db --- /dev/null +++ b/Frameworks/DataModel/BatchUpdates.swift @@ -0,0 +1,76 @@ +// +// BatchUpdates.swift +// DataModel +// +// Created by Brent Simmons on 9/12/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +private final class BatchUpdatesTracker { + + private var batchUpdatesCount = 0 + + var isPerformingBatchUpdates: Bool { + get { + return batchUpdatesCount > 0 + } + } + + func incrementBatchUpdatesCount() { + + batchUpdatesCount = batchUpdatesCount + 1 + } + + func decrementBatchUpdatesCount() { + + batchUpdatesCount = batchUpdatesCount - 1 + + if batchUpdatesCount < 1 { + + if batchUpdatesCount < 0 { + assertionFailure("Batch updates count should never be below 0.") + batchUpdatesCount = 0 + } + + batchUpdatesCount = 0 + postDataModelDidPerformBatchUpdates() + } + } + + func postDataModelDidPerformBatchUpdates() { + + NotificationCenter.default.post(name: .DataModelDidPerformBatchUpdates, object: nil) + } + +} + +fileprivate let batchUpdatesTracker = BatchUpdatesTracker() + +public func dataModelIsPerformingBatchUpdates() -> Bool { + + return batchUpdatesTracker.isPerformingBatchUpdates +} + +public typealias BatchUpdatesBlock = () -> Void + +public func performDataModelBatchUpdates(_ batchUpdatesBlock: BatchUpdatesBlock) { + + startDataModelBatchUpdates() + + batchUpdatesBlock() + + endDataModelBatchUpdates() +} + +private func startDataModelBatchUpdates() { + + batchUpdatesTracker.incrementBatchUpdatesCount() +} + +private func endDataModelBatchUpdates() { + + batchUpdatesTracker.decrementBatchUpdatesCount() +} + diff --git a/Frameworks/DataModel/DataModel.xcodeproj/project.pbxproj b/Frameworks/DataModel/DataModel.xcodeproj/project.pbxproj new file mode 100644 index 000000000..0e7aaba84 --- /dev/null +++ b/Frameworks/DataModel/DataModel.xcodeproj/project.pbxproj @@ -0,0 +1,340 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 8439773E1DA07F2400F0FCBD /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8439773D1DA07F2400F0FCBD /* RSCore.framework */; }; + 8471A2CC1ED4CEEE008F099E /* AccountProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471A2C81ED4CEEE008F099E /* AccountProtocol.swift */; }; + 8471A2CD1ED4CEEE008F099E /* ArticleProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471A2C91ED4CEEE008F099E /* ArticleProtocol.swift */; }; + 8471A2CE1ED4CEEE008F099E /* ArticleStatusProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471A2CA1ED4CEEE008F099E /* ArticleStatusProtocol.swift */; }; + 8471A2CF1ED4CEEE008F099E /* BatchUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471A2CB1ED4CEEE008F099E /* BatchUpdates.swift */; }; + 8471A2D51ED4CEFA008F099E /* DisplayNameProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471A2D01ED4CEFA008F099E /* DisplayNameProviderProtocol.swift */; }; + 8471A2D61ED4CEFA008F099E /* FeedProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471A2D11ED4CEFA008F099E /* FeedProtocol.swift */; }; + 8471A2D71ED4CEFA008F099E /* FolderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471A2D21ED4CEFA008F099E /* FolderProtocol.swift */; }; + 8471A2D81ED4CEFA008F099E /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471A2D31ED4CEFA008F099E /* Notifications.swift */; }; + 8471A2D91ED4CEFA008F099E /* UnreadCountProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471A2D41ED4CEFA008F099E /* UnreadCountProviderProtocol.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8439773D1DA07F2400F0FCBD /* RSCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RSCore.framework; path = "../../../../../Library/Developer/Xcode/DerivedData/Rainier-cidsoqwawkdqqphkdtrqrojskege/Build/Products/Debug/RSCore.framework"; sourceTree = ""; }; + 8471A2C81ED4CEEE008F099E /* AccountProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountProtocol.swift; sourceTree = ""; }; + 8471A2C91ED4CEEE008F099E /* ArticleProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleProtocol.swift; sourceTree = ""; }; + 8471A2CA1ED4CEEE008F099E /* ArticleStatusProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleStatusProtocol.swift; sourceTree = ""; }; + 8471A2CB1ED4CEEE008F099E /* BatchUpdates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchUpdates.swift; sourceTree = ""; }; + 8471A2D01ED4CEFA008F099E /* DisplayNameProviderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayNameProviderProtocol.swift; sourceTree = ""; }; + 8471A2D11ED4CEFA008F099E /* FeedProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedProtocol.swift; sourceTree = ""; }; + 8471A2D21ED4CEFA008F099E /* FolderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FolderProtocol.swift; sourceTree = ""; }; + 8471A2D31ED4CEFA008F099E /* Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; + 8471A2D41ED4CEFA008F099E /* UnreadCountProviderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadCountProviderProtocol.swift; sourceTree = ""; }; + 8471A2DA1ED4CF01008F099E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 84C7AE921D68C558009FB883 /* DataModel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DataModel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 84C7AE8E1D68C558009FB883 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8439773E1DA07F2400F0FCBD /* RSCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8439773C1DA07F2400F0FCBD /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8439773D1DA07F2400F0FCBD /* RSCore.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 84C7AE881D68C558009FB883 = { + isa = PBXGroup; + children = ( + 8471A2C81ED4CEEE008F099E /* AccountProtocol.swift */, + 8471A2C91ED4CEEE008F099E /* ArticleProtocol.swift */, + 8471A2CA1ED4CEEE008F099E /* ArticleStatusProtocol.swift */, + 8471A2CB1ED4CEEE008F099E /* BatchUpdates.swift */, + 8471A2D01ED4CEFA008F099E /* DisplayNameProviderProtocol.swift */, + 8471A2D11ED4CEFA008F099E /* FeedProtocol.swift */, + 8471A2D21ED4CEFA008F099E /* FolderProtocol.swift */, + 8471A2D31ED4CEFA008F099E /* Notifications.swift */, + 8471A2D41ED4CEFA008F099E /* UnreadCountProviderProtocol.swift */, + 8471A2DA1ED4CF01008F099E /* Info.plist */, + 84C7AE931D68C558009FB883 /* Products */, + 8439773C1DA07F2400F0FCBD /* Frameworks */, + ); + sourceTree = ""; + }; + 84C7AE931D68C558009FB883 /* Products */ = { + isa = PBXGroup; + children = ( + 84C7AE921D68C558009FB883 /* DataModel.framework */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 84C7AE8F1D68C558009FB883 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 84C7AE911D68C558009FB883 /* DataModel */ = { + isa = PBXNativeTarget; + buildConfigurationList = 84C7AE9A1D68C558009FB883 /* Build configuration list for PBXNativeTarget "DataModel" */; + buildPhases = ( + 84C7AE8D1D68C558009FB883 /* Sources */, + 84C7AE8E1D68C558009FB883 /* Frameworks */, + 84C7AE8F1D68C558009FB883 /* Headers */, + 84C7AE901D68C558009FB883 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = DataModel; + productName = DataModel; + productReference = 84C7AE921D68C558009FB883 /* DataModel.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 84C7AE891D68C558009FB883 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Ranchero Software, LLC"; + TargetAttributes = { + 84C7AE911D68C558009FB883 = { + CreatedOnToolsVersion = 8.0; + LastSwiftMigration = 0800; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 84C7AE8C1D68C558009FB883 /* Build configuration list for PBXProject "DataModel" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 84C7AE881D68C558009FB883; + productRefGroup = 84C7AE931D68C558009FB883 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 84C7AE911D68C558009FB883 /* DataModel */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 84C7AE901D68C558009FB883 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 84C7AE8D1D68C558009FB883 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8471A2CC1ED4CEEE008F099E /* AccountProtocol.swift in Sources */, + 8471A2D61ED4CEFA008F099E /* FeedProtocol.swift in Sources */, + 8471A2CF1ED4CEEE008F099E /* BatchUpdates.swift in Sources */, + 8471A2CE1ED4CEEE008F099E /* ArticleStatusProtocol.swift in Sources */, + 8471A2D91ED4CEFA008F099E /* UnreadCountProviderProtocol.swift in Sources */, + 8471A2CD1ED4CEEE008F099E /* ArticleProtocol.swift in Sources */, + 8471A2D51ED4CEFA008F099E /* DisplayNameProviderProtocol.swift in Sources */, + 8471A2D81ED4CEFA008F099E /* Notifications.swift in Sources */, + 8471A2D71ED4CEFA008F099E /* FolderProtocol.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 84C7AE981D68C558009FB883 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 84C7AE991D68C558009FB883 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 84C7AE9B1D68C558009FB883 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.DataModel; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 84C7AE9C1D68C558009FB883 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.DataModel; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 84C7AE8C1D68C558009FB883 /* Build configuration list for PBXProject "DataModel" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 84C7AE981D68C558009FB883 /* Debug */, + 84C7AE991D68C558009FB883 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 84C7AE9A1D68C558009FB883 /* Build configuration list for PBXNativeTarget "DataModel" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 84C7AE9B1D68C558009FB883 /* Debug */, + 84C7AE9C1D68C558009FB883 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 84C7AE891D68C558009FB883 /* Project object */; +} diff --git a/Frameworks/DataModel/DataModel.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Frameworks/DataModel/DataModel.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..a7300f614 --- /dev/null +++ b/Frameworks/DataModel/DataModel.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Frameworks/DataModel/DisplayNameProviderProtocol.swift b/Frameworks/DataModel/DisplayNameProviderProtocol.swift new file mode 100644 index 000000000..8c99aa69d --- /dev/null +++ b/Frameworks/DataModel/DisplayNameProviderProtocol.swift @@ -0,0 +1,15 @@ +// +// DisplayNameProviderProtocol.swift +// DataModel +// +// Created by Brent Simmons on 7/28/16. +// Copyright © 2016 Ranchero Software. All rights reserved. +// + +import Foundation + +public protocol DisplayNameProvider { + + var nameForDisplay: String {get} +} + diff --git a/Frameworks/DataModel/FeedProtocol.swift b/Frameworks/DataModel/FeedProtocol.swift new file mode 100644 index 000000000..45b54cc78 --- /dev/null +++ b/Frameworks/DataModel/FeedProtocol.swift @@ -0,0 +1,45 @@ +// +// FeedProtocol.swift +// Rainier +// +// Created by Brent Simmons on 4/23/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import RSCore + +public protocol Feed: class, UnreadCountProvider, DisplayNameProvider { + + var account: Account {get} + var url: String {get} + var feedID: String {get} + var homePageURL: String? {get} + var name: String? {get} + var editedName: String? {get} + var nameForDisplay: String {get} +// var articles: NSSet {get} + + init(account: Account, url: String, feedID: String) + + // Exporting OPML. + func opmlString(indentLevel: Int) -> String +} + +public extension Feed { + + func opmlString(indentLevel: Int) -> String { + + let escapedName = nameForDisplay.rs_stringByEscapingSpecialXMLCharacters() + var escapedHomePageURL = "" + if let homePageURL = homePageURL { + escapedHomePageURL = homePageURL.rs_stringByEscapingSpecialXMLCharacters() + } + let escapedFeedURL = url.rs_stringByEscapingSpecialXMLCharacters() + + var s = "\n" + s = s.rs_string(byPrependingNumberOfTabs: indentLevel) + + return s + } +} diff --git a/Frameworks/DataModel/FolderProtocol.swift b/Frameworks/DataModel/FolderProtocol.swift new file mode 100644 index 000000000..09bafbbd5 --- /dev/null +++ b/Frameworks/DataModel/FolderProtocol.swift @@ -0,0 +1,171 @@ +// +// FolderProtocol.swift +// Rainier +// +// Created by Brent Simmons on 4/17/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import RSCore + +public typealias FolderVisitBlock = (_ obj: AnyObject) -> Bool + +public let FolderChildrenDidChangeNotification = "FolderChildNodesDidChangeNotification" + +public func FolderPostChildrenDidChangeNotification(_ folder: Folder) { + + NotificationCenter.default.post(name: NSNotification.Name(rawValue: FolderChildrenDidChangeNotification), object: folder) +} + +public protocol Folder: class, UnreadCountProvider, DisplayNameProvider { + + // TODO: get rid of children, flattenedFeeds, and some default implementations in favor of faster, specific ones. + +// var children: NSSet {get} + var account: Account? {get} + + var hasAtLeastOneFeed: Bool {get} //Recursive + var flattenedFeeds: NSSet {get} + + func objectIsChild(_ obj: AnyObject) -> Bool + func objectIsDescendant(_ obj: AnyObject) -> Bool + + func fetchArticles() -> [Article] + + // visitBlock should return true to stop visiting. + // visitObjects returns true if a visitBlock returned true. + func visitObjects(_ recurse: Bool, visitBlock: FolderVisitBlock) -> Bool + func visitChildren(visitBlock: FolderVisitBlock) -> Bool // Above with recurse = false + + func findObject(_ recurse: Bool, visitBlock: FolderVisitBlock) -> AnyObject? + + func canAddItem(_ item: AnyObject) -> Bool + func addItem(_ item: AnyObject) -> Bool // Return true even if item already exists. + func addItems(_ items: [AnyObject]) -> Bool // Return true even if some items already exist. + + func canAddFolderWithName(_ folderName: String) -> Bool // Special case: folder with name exists. Return true in that case. + func ensureFolderWithName(_ folderName: String) -> Folder? // Return folder even if item already exists. + + // Recurses + func existingFeedWithID(_ feedID: String) -> Feed? + func existingFeedWithURL(_ urlString: String) -> Feed? + + // Does not recurse. + func existingFolderWithName(_ name: String) -> Folder? + + // Doesn't add feed. Just creates instance. + func createFeedWithName(_ name: String?, editedName: String?, urlString: String) -> Feed? + + func deleteItems(_ items: [AnyObject]) + + // Exporting OPML. + func opmlString(indentLevel: Int) -> String +} + +public extension Folder { + + var hasAtLeastOneFeed: Bool { + get { + return visitObjects(true) { (oneObject) in + + return oneObject is Feed + } + } + } + + func visitChildren(visitBlock: FolderVisitBlock) -> Bool { + + return visitObjects(false, visitBlock: visitBlock) + } + + func findObject(_ recurse: Bool, visitBlock: FolderVisitBlock) -> AnyObject? { + + var foundObject: AnyObject? + + let _ = visitObjects(recurse) { (oneObject) in + + if let _ = foundObject { + return true + } + + if visitBlock(oneObject) { + foundObject = oneObject + return true + } + + return false + } + + return foundObject + } + + func objectIsChild(_ obj: AnyObject) -> Bool { + + return visitObjects(false) { (oneObject) in + return obj === oneObject + } + } + + func objectIsDescendant(_ obj: AnyObject) -> Bool { + + return visitObjects(true) { (oneObject) in + return obj === oneObject + } + } + + func existingFolderWithName(_ name: String) -> Folder? { + + let foundObject = findObject(false) { (oneObject) in + if let oneFolder = oneObject as? Folder, oneFolder.nameForDisplay == name { + return true + } + return false + } + return foundObject as! Folder? + } + + func addItems(_ items: [AnyObject]) -> Bool { + + var atLeastOneItemAdded = false + items.forEach { (oneItem) in + if addItem(oneItem) { + atLeastOneItemAdded = true + } + } + return atLeastOneItemAdded + } + + func opmlString(indentLevel: Int) -> String { + + let escapedTitle = nameForDisplay.rs_stringByEscapingSpecialXMLCharacters() + var s = "\n" + s = s.rs_string(byPrependingNumberOfTabs: indentLevel) + + var hasAtLeastOneChild = false + + let _ = visitChildren { (oneChild) -> Bool in + + hasAtLeastOneChild = true + if let oneFolder = oneChild as? Folder { + s = s + oneFolder.opmlString(indentLevel: indentLevel + 1) + } + else if let oneFeed = oneChild as? Feed { + s = s + oneFeed.opmlString(indentLevel: indentLevel + 1) + } + + return false + } + + if !hasAtLeastOneChild { + s = "\n" + s = s.rs_string(byPrependingNumberOfTabs: indentLevel) + return s + } + + s = s + NSString.rs_string(withNumberOfTabs: indentLevel) + "\n" + + return s + } +} + diff --git a/Frameworks/DataModel/Info.plist b/Frameworks/DataModel/Info.plist new file mode 100644 index 000000000..c5ede6b01 --- /dev/null +++ b/Frameworks/DataModel/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2016 Ranchero Software, LLC. All rights reserved. + NSPrincipalClass + + + diff --git a/Frameworks/DataModel/Notifications.swift b/Frameworks/DataModel/Notifications.swift new file mode 100644 index 000000000..6f3d014de --- /dev/null +++ b/Frameworks/DataModel/Notifications.swift @@ -0,0 +1,21 @@ +// +// Notifications.swift +// DataModel +// +// Created by Brent Simmons on 9/10/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public extension Notification.Name { + + public static let ArticleStatusesDidChange = Notification.Name(rawValue: "ArticleStatusesDidChange") + public static let UnreadCountDidChange = Notification.Name(rawValue: "UnreadCountDidChangeNotification") + public static let DataModelDidPerformBatchUpdates = Notification.Name(rawValue: "DataModelDidPerformBatchUpdatesDidPerformBatchUpdatesNotification") + public static let AccountRefreshProgressDidChange = Notification.Name(rawValue: "AccountRefreshProgressDidChangeNotification") +} + +public let articlesKey = "articles" +public let unreadCountKey = "unreadCount" +public let progressKey = "progress" //RSProgress diff --git a/Frameworks/DataModel/UnreadCountProviderProtocol.swift b/Frameworks/DataModel/UnreadCountProviderProtocol.swift new file mode 100644 index 000000000..3ddf09664 --- /dev/null +++ b/Frameworks/DataModel/UnreadCountProviderProtocol.swift @@ -0,0 +1,38 @@ +// +// UnreadCountProtocol.swift +// Rainier +// +// Created by Brent Simmons on 4/8/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public protocol UnreadCountProvider { + + var unreadCount: Int {get} + + func updateUnreadCount() +} + +public func calculateUnreadCount(_ children: T) -> Int { + + var updatedUnreadCount = 0 + + children.forEach { (oneChild) in + if let oneUnreadCountProvider = oneChild as? UnreadCountProvider { + updatedUnreadCount += oneUnreadCountProvider.unreadCount + } + } + + return updatedUnreadCount +} + +public extension UnreadCountProvider { + + public func postUnreadCountDidChangeNotification() { + + NotificationCenter.default.post(name: .UnreadCountDidChange, object: self, userInfo: [unreadCountKey: unreadCount]) + } + +}