diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f36f0ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +Example/Pods +Example/RealmContent.xcworkspace diff --git a/Example/Podfile b/Example/Podfile new file mode 100644 index 0000000..f4be570 --- /dev/null +++ b/Example/Podfile @@ -0,0 +1,12 @@ +use_frameworks! + +target 'RealmContent_Example' do + pod 'RealmSwift' + + pod 'RealmContent', :path => '../' + #pod 'RealmContent/Markdown', :path => '../' + + target 'RealmContent_Tests' do + inherit! :search_paths + end +end diff --git a/Example/RealmContent.xcodeproj/project.pbxproj b/Example/RealmContent.xcodeproj/project.pbxproj new file mode 100644 index 0000000..74606ea --- /dev/null +++ b/Example/RealmContent.xcodeproj/project.pbxproj @@ -0,0 +1,631 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 3624BAD47E46F4A055081D0A /* Pods_RealmContent_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F2EC4ED06831600B8DC2A1E /* Pods_RealmContent_Tests.framework */; }; + 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; + 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; + 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; + 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; + 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; + 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; }; + 9CB7EF7B1F0C0F2900E70F21 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CB7EF7A1F0C0F2900E70F21 /* User.swift */; }; + 9CB7EF7E1F0C14A100E70F21 /* DemoData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CB7EF7D1F0C14A100E70F21 /* DemoData.swift */; }; + A05247F6E9ADF72B0FB18887 /* Pods_RealmContent_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3A925AA1E0E169870735E89 /* Pods_RealmContent_Example.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 607FACC81AFB9204008FA782 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 607FACCF1AFB9204008FA782; + remoteInfo = RealmContent; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0F2EC4ED06831600B8DC2A1E /* Pods_RealmContent_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RealmContent_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2FFA066B6BE577AADABC6326 /* Pods-RealmContent_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RealmContent_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-RealmContent_Example/Pods-RealmContent_Example.release.xcconfig"; sourceTree = ""; }; + 4AA6ACB425FEB238D826B36D /* RealmContent.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = RealmContent.podspec; path = ../RealmContent.podspec; sourceTree = ""; }; + 5F7715F03D14D9868D129031 /* Pods-RealmContent_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RealmContent_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RealmContent_Tests/Pods-RealmContent_Tests.debug.xcconfig"; sourceTree = ""; }; + 607FACD01AFB9204008FA782 /* RealmContent_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RealmContent_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 607FACE51AFB9204008FA782 /* RealmContent_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RealmContent_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 607FACEB1AFB9204008FA782 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; + 7748505FC8E6BE3225268846 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; + 79587B91D264E98BB4B38592 /* Pods-RealmContent_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RealmContent_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RealmContent_Tests/Pods-RealmContent_Tests.release.xcconfig"; sourceTree = ""; }; + 91887025475E5527D73EE4E9 /* Pods-RealmContent_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RealmContent_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RealmContent_Example/Pods-RealmContent_Example.debug.xcconfig"; sourceTree = ""; }; + 9B388CBFD66930A128F34526 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; + 9CB7EF7A1F0C0F2900E70F21 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 9CB7EF7C1F0C12B100E70F21 /* RealmContent_Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RealmContent_Example.entitlements; sourceTree = ""; }; + 9CB7EF7D1F0C14A100E70F21 /* DemoData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoData.swift; sourceTree = ""; }; + C3A925AA1E0E169870735E89 /* Pods_RealmContent_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RealmContent_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 607FACCD1AFB9204008FA782 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A05247F6E9ADF72B0FB18887 /* Pods_RealmContent_Example.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 607FACE21AFB9204008FA782 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3624BAD47E46F4A055081D0A /* Pods_RealmContent_Tests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 31FDD6EB09881B803A129A81 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C3A925AA1E0E169870735E89 /* Pods_RealmContent_Example.framework */, + 0F2EC4ED06831600B8DC2A1E /* Pods_RealmContent_Tests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 607FACC71AFB9204008FA782 = { + isa = PBXGroup; + children = ( + 9CB7EF7C1F0C12B100E70F21 /* RealmContent_Example.entitlements */, + 607FACF51AFB993E008FA782 /* Podspec Metadata */, + 607FACD21AFB9204008FA782 /* Example for RealmContent */, + 607FACE81AFB9204008FA782 /* Tests */, + 607FACD11AFB9204008FA782 /* Products */, + 70A5453B443833571A71D1D3 /* Pods */, + 31FDD6EB09881B803A129A81 /* Frameworks */, + ); + sourceTree = ""; + }; + 607FACD11AFB9204008FA782 /* Products */ = { + isa = PBXGroup; + children = ( + 607FACD01AFB9204008FA782 /* RealmContent_Example.app */, + 607FACE51AFB9204008FA782 /* RealmContent_Tests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 607FACD21AFB9204008FA782 /* Example for RealmContent */ = { + isa = PBXGroup; + children = ( + 9CB7EF7F1F0C1D5400E70F21 /* Assets */, + 9CB7EF791F0C0F1700E70F21 /* Entities */, + 607FACD51AFB9204008FA782 /* AppDelegate.swift */, + 607FACD71AFB9204008FA782 /* ViewController.swift */, + 9CB7EF7D1F0C14A100E70F21 /* DemoData.swift */, + 607FACD31AFB9204008FA782 /* Supporting Files */, + ); + name = "Example for RealmContent"; + path = RealmContent; + sourceTree = ""; + }; + 607FACD31AFB9204008FA782 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 607FACD41AFB9204008FA782 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 607FACE81AFB9204008FA782 /* Tests */ = { + isa = PBXGroup; + children = ( + 607FACEB1AFB9204008FA782 /* Tests.swift */, + 607FACE91AFB9204008FA782 /* Supporting Files */, + ); + path = Tests; + sourceTree = ""; + }; + 607FACE91AFB9204008FA782 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 607FACEA1AFB9204008FA782 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { + isa = PBXGroup; + children = ( + 4AA6ACB425FEB238D826B36D /* RealmContent.podspec */, + 9B388CBFD66930A128F34526 /* README.md */, + 7748505FC8E6BE3225268846 /* LICENSE */, + ); + name = "Podspec Metadata"; + sourceTree = ""; + }; + 70A5453B443833571A71D1D3 /* Pods */ = { + isa = PBXGroup; + children = ( + 91887025475E5527D73EE4E9 /* Pods-RealmContent_Example.debug.xcconfig */, + 2FFA066B6BE577AADABC6326 /* Pods-RealmContent_Example.release.xcconfig */, + 5F7715F03D14D9868D129031 /* Pods-RealmContent_Tests.debug.xcconfig */, + 79587B91D264E98BB4B38592 /* Pods-RealmContent_Tests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 9CB7EF791F0C0F1700E70F21 /* Entities */ = { + isa = PBXGroup; + children = ( + 9CB7EF7A1F0C0F2900E70F21 /* User.swift */, + ); + name = Entities; + sourceTree = ""; + }; + 9CB7EF7F1F0C1D5400E70F21 /* Assets */ = { + isa = PBXGroup; + children = ( + 607FACD91AFB9204008FA782 /* Main.storyboard */, + 607FACDC1AFB9204008FA782 /* Images.xcassets */, + 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, + ); + name = Assets; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 607FACCF1AFB9204008FA782 /* RealmContent_Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "RealmContent_Example" */; + buildPhases = ( + 91B2174D8CB71230C6AA6C31 /* [CP] Check Pods Manifest.lock */, + 607FACCC1AFB9204008FA782 /* Sources */, + 607FACCD1AFB9204008FA782 /* Frameworks */, + 607FACCE1AFB9204008FA782 /* Resources */, + 6270B953B0F442801059BD41 /* [CP] Embed Pods Frameworks */, + 3DBC1A7D396711ACA9C631CF /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RealmContent_Example; + productName = RealmContent; + productReference = 607FACD01AFB9204008FA782 /* RealmContent_Example.app */; + productType = "com.apple.product-type.application"; + }; + 607FACE41AFB9204008FA782 /* RealmContent_Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "RealmContent_Tests" */; + buildPhases = ( + E08CE42BB644F02578E61ECC /* [CP] Check Pods Manifest.lock */, + 607FACE11AFB9204008FA782 /* Sources */, + 607FACE21AFB9204008FA782 /* Frameworks */, + 607FACE31AFB9204008FA782 /* Resources */, + 92A3FFBC474A2B332CB77CA9 /* [CP] Embed Pods Frameworks */, + 36DDE4121D2787A429C35563 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 607FACE71AFB9204008FA782 /* PBXTargetDependency */, + ); + name = RealmContent_Tests; + productName = Tests; + productReference = 607FACE51AFB9204008FA782 /* RealmContent_Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 607FACC81AFB9204008FA782 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0720; + LastUpgradeCheck = 0820; + ORGANIZATIONNAME = CocoaPods; + TargetAttributes = { + 607FACCF1AFB9204008FA782 = { + CreatedOnToolsVersion = 6.3.1; + DevelopmentTeam = 9MF8G8D9Y5; + LastSwiftMigration = 0820; + SystemCapabilities = { + com.apple.Keychain = { + enabled = 1; + }; + }; + }; + 607FACE41AFB9204008FA782 = { + CreatedOnToolsVersion = 6.3.1; + LastSwiftMigration = 0820; + TestTargetID = 607FACCF1AFB9204008FA782; + }; + }; + }; + buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "RealmContent" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 607FACC71AFB9204008FA782; + productRefGroup = 607FACD11AFB9204008FA782 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 607FACCF1AFB9204008FA782 /* RealmContent_Example */, + 607FACE41AFB9204008FA782 /* RealmContent_Tests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 607FACCE1AFB9204008FA782 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, + 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, + 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 607FACE31AFB9204008FA782 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 36DDE4121D2787A429C35563 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RealmContent_Tests/Pods-RealmContent_Tests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3DBC1A7D396711ACA9C631CF /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RealmContent_Example/Pods-RealmContent_Example-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 6270B953B0F442801059BD41 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RealmContent_Example/Pods-RealmContent_Example-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 91B2174D8CB71230C6AA6C31 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + 92A3FFBC474A2B332CB77CA9 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RealmContent_Tests/Pods-RealmContent_Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E08CE42BB644F02578E61ECC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 607FACCC1AFB9204008FA782 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, + 9CB7EF7E1F0C14A100E70F21 /* DemoData.swift in Sources */, + 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, + 9CB7EF7B1F0C0F2900E70F21 /* User.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 607FACE11AFB9204008FA782 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 607FACCF1AFB9204008FA782 /* RealmContent_Example */; + targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 607FACD91AFB9204008FA782 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 607FACDA1AFB9204008FA782 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + 607FACDF1AFB9204008FA782 /* Base */, + ); + name = LaunchScreen.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 607FACED1AFB9204008FA782 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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_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_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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_SYMBOLS_PRIVATE_EXTERN = NO; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 607FACEE1AFB9204008FA782 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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_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_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 607FACF01AFB9204008FA782 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 91887025475E5527D73EE4E9 /* Pods-RealmContent_Example.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = RealmContent_Example.entitlements; + DEVELOPMENT_TEAM = 9MF8G8D9Y5; + INFOPLIST_FILE = RealmContent/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MODULE_NAME = ExampleApp; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 607FACF11AFB9204008FA782 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2FFA066B6BE577AADABC6326 /* Pods-RealmContent_Example.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = RealmContent_Example.entitlements; + DEVELOPMENT_TEAM = 9MF8G8D9Y5; + INFOPLIST_FILE = RealmContent/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MODULE_NAME = ExampleApp; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + 607FACF31AFB9204008FA782 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5F7715F03D14D9868D129031 /* Pods-RealmContent_Tests.debug.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 607FACF41AFB9204008FA782 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 79587B91D264E98BB4B38592 /* Pods-RealmContent_Tests.release.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "RealmContent" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 607FACED1AFB9204008FA782 /* Debug */, + 607FACEE1AFB9204008FA782 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "RealmContent_Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 607FACF01AFB9204008FA782 /* Debug */, + 607FACF11AFB9204008FA782 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "RealmContent_Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 607FACF31AFB9204008FA782 /* Debug */, + 607FACF41AFB9204008FA782 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 607FACC81AFB9204008FA782 /* Project object */; +} diff --git a/Example/RealmContent.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/RealmContent.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..3085b24 --- /dev/null +++ b/Example/RealmContent.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/RealmContent.xcodeproj/project.xcworkspace/xcuserdata/marin.xcuserdatad/UserInterfaceState.xcuserstate b/Example/RealmContent.xcodeproj/project.xcworkspace/xcuserdata/marin.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..a2c021b Binary files /dev/null and b/Example/RealmContent.xcodeproj/project.xcworkspace/xcuserdata/marin.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Example/RealmContent.xcodeproj/xcshareddata/xcschemes/RealmContent-Example.xcscheme b/Example/RealmContent.xcodeproj/xcshareddata/xcschemes/RealmContent-Example.xcscheme new file mode 100644 index 0000000..0fd7fd8 --- /dev/null +++ b/Example/RealmContent.xcodeproj/xcshareddata/xcschemes/RealmContent-Example.xcscheme @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/RealmContent.xcodeproj/xcuserdata/marin.xcuserdatad/xcschemes/xcschememanagement.plist b/Example/RealmContent.xcodeproj/xcuserdata/marin.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..a1575db --- /dev/null +++ b/Example/RealmContent.xcodeproj/xcuserdata/marin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SuppressBuildableAutocreation + + 607FACCF1AFB9204008FA782 + + primary + + + 607FACE41AFB9204008FA782 + + primary + + + + + diff --git a/Example/RealmContent/AppDelegate.swift b/Example/RealmContent/AppDelegate.swift new file mode 100644 index 0000000..ac5ffcf --- /dev/null +++ b/Example/RealmContent/AppDelegate.swift @@ -0,0 +1,63 @@ +// +// AppDelegate.swift +// +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import UIKit +import RealmSwift + +/// configuration +let shouldConnectToROS = true +let host = "localhost" +let username = "test@host" +let password = "password" + +/// app delegate +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + + if shouldConnectToROS { + // syncs content from ROS + connect(host: host, username: username, password: password, completion: showMainViewController) + + } else { + // shows content from local realm + showMainViewController() + } + + return true + } + + private func showMainViewController(success: Bool = true) { + let storyboard = self.window!.rootViewController!.storyboard! + self.window!.rootViewController = storyboard.instantiateViewController(withIdentifier: "Main") + } +} + +extension AppDelegate { + // connect to a Realm Object Server + + func connect(host: String, username: String, password: String, completion: @escaping ((Bool)->Void) = {_ in}) { + let credentials = SyncCredentials.usernamePassword(username: username, password: password) + let serverUrl = URL(string: "http://\(host):9080")! + + SyncUser.logIn(with: credentials, server: serverUrl) { user, error in + guard let user = user else { + DispatchQueue.main.async { completion(false) } + return + } + + var conf = Realm.Configuration.defaultConfiguration + conf.syncConfiguration = SyncConfiguration(user: user, realmURL: URL(string: "realm://\(host):9080/~/realmcontenttest")!) + Realm.Configuration.defaultConfiguration = conf + + DispatchQueue.main.async { completion(true) } + } + } +} diff --git a/Example/RealmContent/Base.lproj/LaunchScreen.xib b/Example/RealmContent/Base.lproj/LaunchScreen.xib new file mode 100644 index 0000000..6655c7a --- /dev/null +++ b/Example/RealmContent/Base.lproj/LaunchScreen.xib @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/RealmContent/Base.lproj/Main.storyboard b/Example/RealmContent/Base.lproj/Main.storyboard new file mode 100644 index 0000000..e435e65 --- /dev/null +++ b/Example/RealmContent/Base.lproj/Main.storyboard @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/RealmContent/DemoData.swift b/Example/RealmContent/DemoData.swift new file mode 100644 index 0000000..832c7f2 --- /dev/null +++ b/Example/RealmContent/DemoData.swift @@ -0,0 +1,63 @@ +// +// ViewController+DemoData.swift +// RealmContent +// +// Created by Marin Todorov on 7/4/17. +// Copyright © 2017 CocoaPods. All rights reserved. +// + +import Foundation + +import RealmSwift +import RealmContent + +func createDemoData() { + let realm = try! Realm() + + try! realm.write { + let pages = realm.objects(ContentPage.self) + .filter("title IN %@", ["About", "Formatting Showcase", "Store"]) + pages.map { $0.elements } .forEach(realm.delete) + realm.delete(pages) + + let about = ContentPage(value: [ + "title": "About", + "priority": 10, + "mainColor": "purple", + "tag": "Info" + ]) + about.elements.append( + ContentElement(value: ["type": "p", "content": "Just a placeholder text about Realm Content"]) + ) + + let contact = ContentPage(value: [ + "title": "Formatting Showcase", + "priority": 8, + "mainColor": "purple", + "tag": "Info" + ]) + let elements: [ContentElement] = [ + ContentElement(value: ["type": "h1", "content": "Heading with h1"]), + ContentElement(value: ["type": "h2", "content": "Heading with h2"]), + ContentElement(value: ["type": "h3", "content": "Heading with h3"]), + ContentElement(value: ["type": "h4", "content": "Heading with h4"]), + ContentElement(value: ["type": "p", "content": "In publishing and graphic design, lorem ipsum is a filler text commonly used to demonstrate the graphic elements of a document or visual presentation. Replacing meaningful content with placeholder text allows designers to design the form of the content before the content itself has been produced. (source: Wikipedia)"]), + ContentElement(value: ["type": "link", "content": "Link to Wikipedia", "url": "https://en.wikipedia.org/wiki/Lorem_ipsum"]), + ContentElement(value: ["type": "img", "content": "http://realm.io/assets/img/news/2016-05-17-realm-rxswift/rx.png", "url": "https://news.realm.io/news/marin-todorov-realm-rxswift/"]), + ContentElement(value: ["type": "p", "content": "Tap on the image above to open Realm's blog 🦄 ..."]) + ] + contact.elements.append(objectsIn: elements) + + let store = ContentPage(value: [ + "title": "Store", + "priority": 0, + "mainColor": "purple", + "tag": "Shopping" + ]) + store.elements.append( + ContentElement(value: ["type": "p", "content": "just a placeholder text"]) + ) + + realm.add([about, contact, store]) + } +} diff --git a/Example/RealmContent/Images.xcassets/AppIcon.appiconset/Contents.json b/Example/RealmContent/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..b8236c6 --- /dev/null +++ b/Example/RealmContent/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,48 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/RealmContent/Info.plist b/Example/RealmContent/Info.plist new file mode 100644 index 0000000..56ab1fe --- /dev/null +++ b/Example/RealmContent/Info.plist @@ -0,0 +1,46 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSApplicationCategoryType + + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + + + diff --git a/Example/RealmContent/User.swift b/Example/RealmContent/User.swift new file mode 100644 index 0000000..8dd928b --- /dev/null +++ b/Example/RealmContent/User.swift @@ -0,0 +1,13 @@ +// +// User.swift +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import Foundation +import RealmSwift + +class User: Object { + dynamic var name = "" + dynamic var birthday = Date() +} diff --git a/Example/RealmContent/ViewController.swift b/Example/RealmContent/ViewController.swift new file mode 100644 index 0000000..2607c4b --- /dev/null +++ b/Example/RealmContent/ViewController.swift @@ -0,0 +1,76 @@ +// +// ViewController.swift +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import UIKit + +// 1) Import RealmSwift + RealmContent + +import RealmSwift +import RealmContent + +class ViewController: UIViewController { + + @IBOutlet var tableView: UITableView! + + // 2) create a content data source + let items = ContentListDataSource(style: .sectionsByTag) + + override func viewDidLoad() { + super.viewDidLoad() + + // 3) initialize the data source and set view to update + items.loadContent(from: try! Realm()) + items.updating(view: tableView) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if try! Realm().isEmpty { + createDemoData() + } + } +} + +// 4) implement table/collection data source methods as you wish... + +extension ViewController: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + return items.numberOfSections + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return items.numberOfItemsIn(section: section) + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return items.titleForSection(section: section) + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let item = items.itemAt(indexPath: indexPath) + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")! + + cell.accessoryType = .disclosureIndicator + cell.textLabel?.text = "» \(item.title ?? "...")" + + return cell + } +} + +// 5) Push or present a ContentViewController to display content + +extension ViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let item = items.itemAt(indexPath: indexPath) + let vc = ContentViewController(page: item) + + navigationController!.pushViewController(vc, animated: true) + } +} diff --git a/Example/RealmContent_Example.entitlements b/Example/RealmContent_Example.entitlements new file mode 100644 index 0000000..546b16b --- /dev/null +++ b/Example/RealmContent_Example.entitlements @@ -0,0 +1,10 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)org.cocoapods.demo.RealmContent-Example + + + diff --git a/Example/Tests/Info.plist b/Example/Tests/Info.plist new file mode 100644 index 0000000..ba72822 --- /dev/null +++ b/Example/Tests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Example/Tests/Tests.swift b/Example/Tests/Tests.swift new file mode 100644 index 0000000..dc79708 --- /dev/null +++ b/Example/Tests/Tests.swift @@ -0,0 +1,29 @@ +import UIKit +import XCTest +import RealmContent + +class Tests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testExample() { + // This is an example of a functional test case. + XCTAssert(true, "Pass") + } + + func testPerformanceExample() { + // 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/LICENSE b/LICENSE new file mode 100644 index 0000000..9606acb --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 icanzilb + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..22f49f5 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# RealmContent + +[![CI Status](http://img.shields.io/travis/icanzilb/RealmContent.svg?style=flat)](https://travis-ci.org/icanzilb/RealmContent) +[![Version](https://img.shields.io/cocoapods/v/RealmContent.svg?style=flat)](http://cocoapods.org/pods/RealmContent) +[![License](https://img.shields.io/cocoapods/l/RealmContent.svg?style=flat)](http://cocoapods.org/pods/RealmContent) +[![Platform](https://img.shields.io/cocoapods/p/RealmContent.svg?style=flat)](http://cocoapods.org/pods/RealmContent) + +## Example + +To run the example project, clone the repo, and run `pod install` from the Example directory first. + +## Requirements + +## Installation + +RealmContent is available through [CocoaPods](http://cocoapods.org). To install +it, simply add the following line to your Podfile: + +```ruby +pod "RealmContent" +``` + +## Author + +icanzilb, marin@underplot.com + +## License + +RealmContent is available under the MIT license. See the LICENSE file for more info. diff --git a/RealmContent.podspec b/RealmContent.podspec new file mode 100644 index 0000000..5996d5c --- /dev/null +++ b/RealmContent.podspec @@ -0,0 +1,39 @@ +Pod::Spec.new do |s| + s.name = 'RealmContent' + s.version = '0.1.0' + s.summary = 'Realm powered content management system' + + s.description = <<-DESC +Realm powered content management system providing built-in view controllers and views to rapidly add dynamic content to iOS apps. + DESC + + s.homepage = 'https://github.com/realm-demos/RealmContent' + # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.source = { :git => 'https://github.com/realm-demos/RealmContent.git', :tag => s.version.to_s } + s.author = { 'Realm' => 'help@realm.io' } + s.library = 'c++' + s.requires_arc = true + s.social_media_url = 'https://twitter.com/realm' + s.license = { :type => 'MIT', :file => 'LICENSE' } + + s.ios.deployment_target = '9.0' + + s.frameworks = 'UIKit' + + s.dependency 'RealmSwift' + s.dependency 'Kingfisher' + s.dependency 'NSString+Color' + + s.default_subspec = 'Core' + + s.subspec 'Core' do |cs| + cs.source_files = 'RealmContent/Classes/Classes/*', 'RealmContent/Classes/Entities/*', 'RealmContent/Classes/View/*' + end + + s.subspec 'Markdown' do |cs| + cs.dependency 'MMMarkdown' + + cs.source_files = 'RealmContent/Classes/View/Markdown/*' + end +end diff --git a/RealmContent/Assets/.gitkeep b/RealmContent/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/RealmContent/Classes/.gitkeep b/RealmContent/Classes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/RealmContent/Classes/Classes/ContentListDataSource.swift b/RealmContent/Classes/Classes/ContentListDataSource.swift new file mode 100644 index 0000000..92f5d8a --- /dev/null +++ b/RealmContent/Classes/Classes/ContentListDataSource.swift @@ -0,0 +1,230 @@ +// +// RealmContentDataSource.swift +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import Foundation +import UIKit +import RealmSwift + +/// :nodoc: +/// Internal protocol to represent datasourceable struct +protocol DataSourceCollection { + associatedtype Element + var count: Int { get } + subscript(position: Int) -> Element { get } +} + +extension Array: DataSourceCollection { } +extension Results: DataSourceCollection { } + +/** + `ContentListDataSource` is a data source class that loads content from a Realm file + and provides data to drive a table or collection view depending on the given settings + (e.g. plain list, sections, etc.) + */ +public class ContentListDataSource: NSObject { + + /// `plain` or `sectionsByTag` grouping style + public enum Style { + case plain + case sectionsByTag + } + + private let keyPriority = "priority" + private let keyTag = "tag" + private let keyElements = "elements" + + fileprivate let style: Style + private var realmConfiguration: Realm.Configuration! + + private weak var view: UIView? + + fileprivate var results: Results? + private var resultsToken: NotificationToken? + + // MARK: - class initialization + + /// creates an instance with the given grouping style + public init(style: Style = .plain) { + self.style = style + super.init() + } + + /// "loads" the list of content from the given realm + public func loadContent(from realm: Realm) { + realmConfiguration = realm.configuration + loadContent() + } + + /** + if set, automatically updates the given view with any real-time changes + - parameter view: if either a `UITableView` or `UICollectionView` will + be be updated upon any changes to the Realm's content + */ + public func updating(view: UIView) { + self.view = view + } + + // MARK: - private methods + + fileprivate struct SectionInfo { + let title: String? + let start: Int + let count: Int + } + + fileprivate var sections: [SectionInfo]? + + private func loadContent() { + let realm = try! Realm(configuration: realmConfiguration) + + switch style { + case .plain: + results = realm.objects(ContentPage.self) + .filter("\(keyElements).@count > 0") + .sorted(byKeyPath: keyPriority, ascending: false) + + resultsToken = results?.addNotificationBlock { [weak self] change in + guard let view = self?.view else { return } + if let tableView = view as? UITableView { tableView.reloadData() } + if let collectionView = view as? UICollectionView { collectionView.reloadData() } + } + + case .sectionsByTag: + results = realm.objects(ContentPage.self) + .filter("\(keyElements).@count > 0") + .sorted(by: [ + SortDescriptor(keyPath: keyTag, ascending: true), + SortDescriptor(keyPath: keyPriority, ascending: false) + ]) + resultsToken = results?.addNotificationBlock { [weak self] change in + guard let this = self else { return } + + switch change { + case .initial: + this.reloadView() + case .update(let results, let deletions, let insertions, let modifications): + if results.count > 0 || deletions.count > 0 { + this.sections = this.sections(for: results) + } + this.reloadView(changes: (deletions, insertions, modifications)) + default: break + } + } + sections = sections(for: results) + } + } + + private func reloadView(changes: ([Int], [Int], [Int])? = nil) { + guard let view = view else { return } + if let tableView = view as? UITableView { tableView.reloadData() } + if let collectionView = view as? UICollectionView { collectionView.reloadData() } + } + + private func sections(for results: Results?) -> [SectionInfo] { + //build section indexes + guard let results = results, let first = results.first else { + return [] + } + + var sections = [SectionInfo]() + var currentCategory = first.tag + var count = 0 + var start = 0 + + autoreleasepool { + for (index, page) in results.enumerated() { + if page.tag != currentCategory { + sections.append(SectionInfo(title: currentCategory, start: start, count: count)) + currentCategory = page.tag + start = index + count = 1 + } else { + count += 1 + } + } + + sections.append(SectionInfo(title: currentCategory, start: start, count: count)) + } + return sections + } + + // MARK: - public data source methods + + /// returns the list of content as an array, in case the data source is group array will be 2 dimensional + public func asArray() -> Array { + guard let results = results else { fatalError("You need to load the content before calling asArray()") } + return Array(results) + } + + /// returns the list of content as Results object. It will crash if called for a grouped in sections data source + public func asResults() -> Results { + guard let results = results else { fatalError("You need to load the content before calling asResults()") } + return results + } + + /// returns the number of sections in the data source. Always returns `1` for plain grouping style. + public var numberOfSections: Int { + switch style { + case .plain: + return 1 + case .sectionsByTag: + return sections!.count + } + } + + /** + returns the number of items for a given section + - parameter section: the section in question + - returns: the number of items in the section + */ + public func numberOfItemsIn(section: Int) -> Int { + switch style { + case .plain: + return results!.count + case .sectionsByTag: + return sections![section].count + } + } + + /** + returns the title for a given section + - parameter section: the section in question + - returns: the String title for the given section. Returns `nil` when grouping style is `plain`. + */ + public func titleForSection(section: Int) -> String? { + switch style { + case .plain: + return nil + case .sectionsByTag: + return sections![section].title + } + } + + /** + returns the `ContentPage` for given section and index + - parameter section: the section index + - parameter section: the item index inside the section + - returns: a `ContentPage` found for the given section/index + */ + public func itemAt(section: Int = -1, index: Int) -> ContentPage { + switch style { + case .plain: + return results![index] + case .sectionsByTag: + let offset = sections![section].start + return results![offset + index] + } + } + + /** + returns the `ContentPage` for given `IndexPath` + - parameter indexPath: the index path in question + - returns: a `ContentPage` found for the given `IndexPath` + */ + public func itemAt(indexPath: IndexPath) -> ContentPage { + return itemAt(section: indexPath.section, index: indexPath.row) + } +} diff --git a/RealmContent/Classes/Classes/MarkdownContentConverter.swift b/RealmContent/Classes/Classes/MarkdownContentConverter.swift new file mode 100644 index 0000000..f22fe7b --- /dev/null +++ b/RealmContent/Classes/Classes/MarkdownContentConverter.swift @@ -0,0 +1,74 @@ +// +// RealmContentDataSource.swift +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import Foundation +import RealmSwift + +/** + `MarkdownConvertorOptions` is a set of options enabling conversion features as follows: + + - `includeTitle` starts the output with the `ContentPage` title as a h1 heading + - `addNewlines` adds extra newline after each element + + */ +struct MarkdownConvertorOptions : OptionSet { + let rawValue: Int + + static let includeTitle = MarkdownConvertorOptions(rawValue: 1 << 0) + static let addNewlines = MarkdownConvertorOptions(rawValue: 1 << 1) +} + +private let newLine = "\n" + +/** + `MarkdownContentConverter` is a struct with static content-conversion functions + */ +struct MarkdownContentConverter { + + /** + Returns the markdown representation of the provided `ContentPage` object + + - parameter page: a content page with elements to convert to markdown + - parameter options: a `MarkdownConvertorOptions` set of options to enable + + - returns: a markdown `String` + */ + static func markdownFrom(page: ContentPage, options: MarkdownConvertorOptions = []) -> String { + var result = "" + + if options.contains(.includeTitle), let title = page.title { + result += "# \(title) \(newLine)" + } + + for element in page.elements { + if let kind = ContentElement.Kind(rawValue: element.type) { + switch kind { + case .p : result += "\(newLine) \(element.content)" + case .h1: result += "\(newLine) # \(element.content)" + case .h2: result += "\(newLine) ## \(element.content)" + case .h3: result += "\(newLine) ### \(element.content)" + case .h4: result += "\(newLine) #### \(element.content)" + case .img: + guard let url = element.url else { break } + if element.content.hasPrefix("http") { + result += "\(newLine) [![\(element.content)](\(url))](\(element.content))" + } else { + result += "\(newLine) ![\(element.content)](\(url))" + } + case .link: + guard let url = element.url else { break } + result += "\(newLine) [\(element.content)](\(url))" + } + if options.contains(.addNewlines) { + result += newLine + } + } + } + + return result + } + +} diff --git a/RealmContent/Classes/Entities/ContentElement.swift b/RealmContent/Classes/Entities/ContentElement.swift new file mode 100644 index 0000000..1ad76bd --- /dev/null +++ b/RealmContent/Classes/Entities/ContentElement.swift @@ -0,0 +1,28 @@ +// +// RealmContentDataSource.swift +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import Foundation +import RealmSwift + +/// represents a single element in a content page +public class ContentElement: Object { + + /// the supported element types + public enum Kind: String { + case p, img, h1, h2, h3, h4, link + } + + public dynamic var type = "p" + public dynamic var content = "" + public dynamic var url: String? + +} + +extension ContentElement { + override public var hashValue: Int { + return (31 &* content.hashValue) &+ type.hashValue + } +} diff --git a/RealmContent/Classes/Entities/ContentPage.swift b/RealmContent/Classes/Entities/ContentPage.swift new file mode 100644 index 0000000..1f3e535 --- /dev/null +++ b/RealmContent/Classes/Entities/ContentPage.swift @@ -0,0 +1,25 @@ +// +// RealmContentDataSource.swift +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import Foundation +import RealmSwift + +/// represents a single content page - news article, blog post, announcement, etc. +public class ContentPage: Object { + + public dynamic var title: String? + public let elements = List() + public dynamic var priority = 0 + public dynamic var mainColor: String? + public dynamic var lang: String? + public dynamic var tag = "" + public dynamic var id = "" + + override public static func indexedProperties() -> [String] { + return ["priority", "tag", "id"] + } + +} diff --git a/RealmContent/Classes/View/ContentViewController.swift b/RealmContent/Classes/View/ContentViewController.swift new file mode 100644 index 0000000..0b465ab --- /dev/null +++ b/RealmContent/Classes/View/ContentViewController.swift @@ -0,0 +1,220 @@ +// +// RealmContentDataSource.swift +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import Foundation +import UIKit +import SafariServices +import RealmSwift +import NSString_Color +import Kingfisher + +/** + `ContentViewController` is a view controller, which displays a given `ContentPage`'s content + */ +public class ContentViewController: UIViewController, UITableViewDataSource { + // MARK: - public properties + + /// whether to use Safari controller to open URLs + public var usesSafariController = true + + // MARK: - private properties + private let tableView = UITableView() + private var page: ContentPage! + private var lastHashes = [Int]() + + /// realm notifications + private var pageUpdatesToken: NotificationToken? + private var pageElementsUpdatesToken: NotificationToken? + + // MARK: - initialization + + /// initialize with a given content page + public init(page: ContentPage) { + self.page = page + super.init(nibName: nil, bundle: nil) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - view controller life-cycle + + public override func viewDidLoad() { + super.viewDidLoad() + title = page.title + configureTableView() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + observe(page: page) + + NotificationCenter.default.addObserver(forName: .UIContentSizeCategoryDidChange, object: .none, queue: OperationQueue.main) { [weak self] _ in + self?.tableView.reloadData() + } + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + pageUpdatesToken = nil + pageElementsUpdatesToken = nil + + NotificationCenter.default.removeObserver(self, name: .UIContentSizeCategoryDidChange, object: .none) + } + + // MARK: - private methods + + private func configureTableView() { + tableView.dataSource = self + tableView.estimatedRowHeight = 40 + tableView.rowHeight = UITableViewAutomaticDimension + tableView.separatorStyle = .none + tableView.allowsSelection = false + tableView.register(TextContentCell.self, forCellReuseIdentifier: String(describing: TextContentCell.self)) + tableView.register(ImageContentCell.self, forCellReuseIdentifier: String(describing: ImageContentCell.self)) + + view.addSubview(tableView) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + } + + private func observe(page: ContentPage) { + pageUpdatesToken?.stop() + pageElementsUpdatesToken?.stop() + + // load the page content + populateFrom(page: page) + + // enable updates + pageUpdatesToken = page.addNotificationBlock() { [weak self] change in + switch change { + case .change(let properties): + for p in properties { + switch p.name { + case "title": self?.title = p.newValue as? String + case "mainColor": self?.tableView.reloadData() + default: break + } + } + case .deleted: self?.pageUpdatesToken = nil + default: break + } + } + + pageElementsUpdatesToken = page.elements.addNotificationBlock(applyChanges) + } + + private func populateFrom(page: ContentPage) { + title = page.title + } + + // MARK: - table methods + private func fromRow(_ section: Int) -> (_ row: Int) -> IndexPath { + return { row in + return IndexPath(row: row, section: section) + } + } + + private func applyChanges(_ changes: RealmCollectionChange>) { + let section = 0 + + switch changes { + case .initial(let elements): + lastHashes = elements.map { $0.hashValue } + tableView.reloadData() + + case .update(let elements, var deletions, var insertions, let modifications): + + let deletedHashes = deletions.map { lastHashes[$0].hashValue } + let insertedHashes = insertions.map { elements[$0].hashValue } + + typealias IndexedValue = (index: Int, value: Int) + typealias Move = (from: Int, to: Int) + + let moves = insertedHashes.enumerated() + .flatMap({ (newIndex, hash) -> (from: IndexedValue, to: IndexedValue)? in + if let oldIndex = deletedHashes.index(of: hash) { + return ( + from: (index: oldIndex, value: deletions[oldIndex]), + to: (index: newIndex, value: insertions[newIndex]) + ) + } else { + return nil + } + }) + + moves.map({ $0.from.index }).forEach { deletions.remove(at: $0) } + moves.map({ $0.to.index }).forEach { insertions.remove(at: $0) } + + tableView.beginUpdates() + tableView.deleteRows(at: deletions.map(fromRow(section)), with: .automatic) + tableView.insertRows(at: insertions.map(fromRow(section)), with: .automatic) + tableView.reloadRows(at: modifications.map(fromRow(section)), with: .none) + + moves.forEach { print("move from \($0.from.value) to \($0.to.value)") } + + moves.forEach { + tableView.moveRow( + at: fromRow(section)($0.from.value), + to: fromRow(section)($0.to.value) + ) + } + + tableView.endUpdates() + + lastHashes = elements.map { $0.hashValue } + default: break + } + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return page.elements.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let element = page.elements[indexPath.row] + + guard let kind = ContentElement.Kind(rawValue: element.type) else { + return UITableViewCell() + } + + switch kind { + case .h1, .h2, .h3, .h4, .p, .link: + let cellId = String(describing: TextContentCell.self) + let cell: TextContentCell = tableView.dequeueReusableCell(withIdentifier: cellId) as! TextContentCell + cell.populate(with: element, config: TextContentCell.TextConfig(mainColor: page.mainColor)) + cell.delegate = self + return cell + + case .img: + let cellId = String(describing: ImageContentCell.self) + let cell: ImageContentCell = tableView.dequeueReusableCell(withIdentifier: cellId) as! ImageContentCell + cell.populate(with: element) { [weak self] in + self?.tableView.beginUpdates() + self?.tableView.endUpdates() + } + cell.delegate = self + return cell + } + } +} + +/// content cell delegate methods +extension ContentViewController: ContentCellDelegate { + public func openUrl(url: URL) { + guard usesSafariController, let scheme = url.scheme, scheme.hasPrefix("http") else { + UIApplication.shared.openURL(url) + return + } + + let safari = SFSafariViewController(url: url) + present(safari, animated: true, completion: nil) + } +} diff --git a/RealmContent/Classes/View/ImageContentCell.swift b/RealmContent/Classes/View/ImageContentCell.swift new file mode 100644 index 0000000..b2cdd56 --- /dev/null +++ b/RealmContent/Classes/View/ImageContentCell.swift @@ -0,0 +1,139 @@ +// +// RealmContentDataSource.swift +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import Foundation +import UIKit +import Kingfisher + +/// content cell that represents an image element in a content page +public class ImageContentCell: UITableViewCell { + + /// read-write property with the default image corner radius + public static var defaultCornerRadius: CGFloat = 6.0 + + /// the cell delegate, required + public var delegate: ContentCellDelegate! + + // MARK: - private properties + private let img = UIImageView() + private let padding: CGFloat = 2.0 + + private var heightConstraint: NSLayoutConstraint! + + private var url: URL? + + private lazy var installTap: Void = { + self.img.isUserInteractionEnabled = true + self.img.addGestureRecognizer( + UITapGestureRecognizer(target: self, action: #selector(didTap)) + ) + }() + + lazy private var placeholder: UIImage = { + return self.placeholderImage( + size: CGSize(width: self.contentView.bounds.size.width, height: 40.0) + ) + }() + + // MARK: - cell life-cycle + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + let marginGuide = contentView.layoutMarginsGuide + + img.contentMode = .scaleAspectFit + + // configure titleLabel + contentView.addSubview(img) + img.translatesAutoresizingMaskIntoConstraints = false + img.leadingAnchor.constraint(equalTo: marginGuide.leadingAnchor).isActive = true + img.topAnchor.constraint(equalTo: marginGuide.topAnchor).isActive = true + img.trailingAnchor.constraint(equalTo: marginGuide.trailingAnchor).isActive = true + img.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor).isActive = true + heightConstraint = img.heightAnchor.constraint(equalToConstant: 20) + heightConstraint.isActive = true + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func prepareForReuse() { + img.image = nil + } + + // MARK: - public methods + + /** + Populates the cell content from a `ContentElement` + - parameter element: the element object containing the cell text + - parameter relayout: a closure to call when cell height changes and table view needs a re-layout + */ + public func populate(with element: ContentElement, relayout: @escaping (()->Void)) { + _ = installTap + + if let url = URL(string: element.content) { + img.kf.setImage( + with: url, placeholder: placeholder, + options: [.backgroundDecode, .processor(RoundCornerImageProcessor(cornerRadius: ImageContentCell.defaultCornerRadius))], + completionHandler: { [weak self] (image, error, cache, url) in + guard let this = self, let image = image else { return } + if image.size.width > this.contentView.frame.width { + let newHeight = (this.contentView.frame.width * image.size.height) / image.size.width + this.heightConstraint.constant = newHeight + } else { + this.heightConstraint.constant = image.size.height + } + DispatchQueue.main.async(execute: relayout) + }) + } + + if let urlString = element.url, let url = URL(string: urlString) { + self.url = url + } else { + self.url = nil + } + } + + // MARK: - private methods + + private func placeholderImage(size: CGSize) -> UIImage { + UIGraphicsBeginImageContextWithOptions(size, true, 0) + + let ctx = UIGraphicsGetCurrentContext()! + ctx.saveGState() + + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + + let path = UIBezierPath(rect: rect) + UIColor.white.setFill() + path.fill() + + let clipPath = UIBezierPath(roundedRect: rect, cornerRadius: 6.0).cgPath + + ctx.addPath(clipPath) + ctx.setFillColor(UIColor(white: 0.95, alpha: 1.0).cgColor) + + ctx.closePath() + ctx.fillPath() + ctx.restoreGState() + + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + return image + } + + internal func didTap() { + guard let url = url else { return } + img.alpha = 0.67 + UIView.animate(withDuration: 0.33, animations: { + self.img.alpha = 1.0 + }, completion: {_ in + self.delegate.openUrl(url: url) + }) + } +} diff --git a/RealmContent/Classes/View/Markdown/MarkdownView.swift b/RealmContent/Classes/View/Markdown/MarkdownView.swift new file mode 100644 index 0000000..18c96d8 --- /dev/null +++ b/RealmContent/Classes/View/Markdown/MarkdownView.swift @@ -0,0 +1,62 @@ +// +// RealmContentDataSource.swift +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import Foundation + +import UIKit +import RealmSwift +import MMMarkdown +import WebKit + +public enum MarkdownRenderOption { + case mainColor(String) + case linkColor(String) + case customCss(String) +} + +/// a webview sub-class rendering markdown +public class MarkdownView: UIWebView { + + // options + private var mainColor = "#000000" + private var linkColor: String? = nil + private var customCss = "" + + public func render(markdown: String, options: [MarkdownRenderOption] = []) { + + for option in options { + switch option { + case .mainColor(let hex): mainColor = hex + case .linkColor(let hex): linkColor = hex + case .customCss(let css): customCss = css + } + } + + guard let mdHtml = try? MMMarkdown.htmlString(withMarkdown: markdown, extensions: MMMarkdownExtensions.gitHubFlavored) else { + loadHTMLString("", baseURL: nil) + return + } + + var fullPage = MarkdownTemplate.html.replacingOccurrences(of: "%markdown%", with: mdHtml) + fullPage = fullPage.replacingOccurrences(of: "%headingColor%", with: mainColor) + fullPage = fullPage.replacingOccurrences(of: "%linkColor%", with: (linkColor ?? mainColor)) + fullPage = fullPage.replacingOccurrences(of: "%customCss%", with: customCss) + fullPage = fullPage.replacingOccurrences(of: "%fontSize%", with: "\(UIFont.systemFontSize)px") + fullPage = fullPage.replacingOccurrences(of: "%fontName%", with: UIFont.systemFont(ofSize: UIFont.systemFontSize).fontName) + loadHTMLString(fullPage, baseURL: nil) + } +} + +private struct MarkdownTemplate { + static let html = "" + + "" + + "%markdown%" +} diff --git a/RealmContent/Classes/View/Markdown/MarkdownViewController.swift b/RealmContent/Classes/View/Markdown/MarkdownViewController.swift new file mode 100644 index 0000000..2963735 --- /dev/null +++ b/RealmContent/Classes/View/Markdown/MarkdownViewController.swift @@ -0,0 +1,135 @@ +// +// RealmContentDataSource.swift +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import Foundation +import UIKit +import WebKit +import SafariServices +import MMMarkdown +import RealmSwift +import NSString_Color + +/// a view controller, which owns a `MarkdownView` +public class MarkdownViewController: UIViewController { + + // types of content input + public enum Content { + case page(ContentPage) + case markdown(String) + } + + private var content: Content! + + // the controller's view + private let webView = MarkdownView() + + // realm notifications + private var pageUpdatesToken: NotificationToken? + private var pageElementsUpdatesToken: NotificationToken? + + // dynamically update the content + private func observe(page: ContentPage) { + pageUpdatesToken?.stop() + pageElementsUpdatesToken?.stop() + + // load the page content + populateFrom(page: page) + + // enable updates + pageUpdatesToken = page.addNotificationBlock { [weak self] change in + switch change { + case .change: self?.populateFrom(page: page) + case .deleted: self?.pageUpdatesToken = nil + default: break + } + } + + pageElementsUpdatesToken = page.elements.addNotificationBlock { [weak self] change in + switch change { + case .update: self?.populateFrom(page: page) + default: break + } + } + } + + public var usesSafariController = true + public var customCssStyle: String? = nil + + public init(content: Content) { + self.content = content + + super.init(nibName: nil, bundle: nil) + self.view = webView + //webView.navigationDelegate = self + webView.delegate = self + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func populateFrom(markdown md: String) { + var renderOptions = [MarkdownRenderOption]() + if let css = customCssStyle { + renderOptions.append(.customCss(css)) + } + webView.render(markdown: md, options: renderOptions) + } + + private func populateFrom(page: ContentPage) { + title = page.title + + let pageRef = ThreadSafeReference(to: page) + let configuration = page.realm!.configuration + + DispatchQueue.global(qos: .background).async { [weak self] in + let realm = try! Realm(configuration: configuration) + guard let page = realm.resolve(pageRef) else { return } + + let markdown = MarkdownContentConverter.markdownFrom( + page: page, options: [.addNewlines]) + + var renderOptions = [MarkdownRenderOption]() + if let color = page.mainColor { + renderOptions.append(.mainColor(color)) + } + if let css = self?.customCssStyle { + renderOptions.append(.customCss(css)) + } + DispatchQueue.main.async { [weak self] in + self?.webView.render(markdown: markdown, options: renderOptions) + } + } + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + switch content! { + case .page(let page): + observe(page: page) + populateFrom(page: page) + case .markdown(let md): + populateFrom(markdown: md) + } + } +} + +extension MarkdownViewController: UIWebViewDelegate { + public func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool { + if navigationType == .linkClicked, let url = request.url { + // website + if usesSafariController, let scheme = url.scheme, scheme.hasPrefix("http") { + let safari = SFSafariViewController(url: url) + present(safari, animated: true, completion: nil) + } else { + UIApplication.shared.openURL(url) + } + return false + } + return true + } +} diff --git a/RealmContent/Classes/View/TextContentCell.swift b/RealmContent/Classes/View/TextContentCell.swift new file mode 100644 index 0000000..0819955 --- /dev/null +++ b/RealmContent/Classes/View/TextContentCell.swift @@ -0,0 +1,144 @@ +// +// RealmContentDataSource.swift +// Created by Marin Todorov +// Copyright © 2017 - present Realm. All rights reserved. +// + +import Foundation +import UIKit +import NSString_Color + +/// delegate protocol for handling URL taps +public protocol ContentCellDelegate { + func openUrl(url: URL) +} + +/// content cell that represents a text element in a content page +public class TextContentCell: UITableViewCell { + + /// read-write property with the default cell text color + public static var defaultTextColor = UIColor.black + + /// the cell delegate, required + public var delegate: ContentCellDelegate! + + // MARK: - private properties + private let label = UILabel() + private let verticalPadding: CGFloat = 0.0 + private let horizontalPadding: CGFloat = 0.0 + + private var topConstraint: NSLayoutConstraint! + private var bottomConstraint: NSLayoutConstraint! + + private var url: URL? + + private lazy var installTap: Void = { + self.contentView.addGestureRecognizer( + UITapGestureRecognizer(target: self, action: #selector(didTap)) + ) + }() + + // MARK: - cell life-cycle + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + let marginGuide = contentView.layoutMarginsGuide + + label.isUserInteractionEnabled = false + label.textAlignment = .left + label.textColor = TextContentCell.defaultTextColor + label.font = UIFont.systemFont(ofSize: UIFont.systemFontSize) + label.lineBreakMode = .byWordWrapping + label.numberOfLines = 0 + + // configure titleLabel + contentView.addSubview(label) + + label.translatesAutoresizingMaskIntoConstraints = false + let leading = label.leadingAnchor.constraint(equalTo: marginGuide.leadingAnchor, constant: horizontalPadding) + topConstraint = label.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: verticalPadding) + let trailing = label.trailingAnchor.constraint(equalTo: marginGuide.trailingAnchor, constant: horizontalPadding) + bottomConstraint = label.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: verticalPadding) + + NSLayoutConstraint.activate([topConstraint, bottomConstraint, leading, trailing]) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func prepareForReuse() { + label.text = nil + } + + /// text cell rendering configuration struct + public struct TextConfig { + var mainColor: String? + func color() -> UIColor { + if let mainColor = mainColor, !mainColor.isEmpty { + return NSString(string: mainColor).representedColor() + } else { + return TextContentCell.defaultTextColor + } + } + } + + // MARK: - public methods + + /** + Populates the cell content from a `ContentElement` + - parameter element: the element object containing the cell text + - parameter config: the rendering configuration for the given cell + */ + public func populate(with element: ContentElement, config: TextConfig) { + _ = installTap + + // text + label.text = element.content + + // url + if let urlString = element.url, let url = URL(string: urlString) { + self.url = url + } else { + self.url = nil + } + + guard let kind = ContentElement.Kind(rawValue: element.type) else { return } + + label.textColor = TextContentCell.defaultTextColor + + // formatting + switch kind { + case .p: + label.font = UIFont.preferredFont(forTextStyle: .body) + case .h1: + label.font = UIFont.preferredFont(forTextStyle: .title1) + label.textColor = config.color() + case .h2: + label.font = UIFont.preferredFont(forTextStyle: .title2) + label.textColor = config.color() + case .h3: + label.font = UIFont.preferredFont(forTextStyle: .headline) + case .h4: + label.font = UIFont.preferredFont(forTextStyle: .subheadline) + case .link: + label.font = UIFont.preferredFont(forTextStyle: .callout) + label.textColor = config.color() + default: break + } + + } + + // MARK: - private methods + + internal func didTap() { + guard let url = url else { return } + label.alpha = 0.5 + UIView.animate(withDuration: 0.33, animations: { + self.label.alpha = 1.0 + }, completion: {_ in + self.delegate.openUrl(url: url) + }) + } +} diff --git a/_Pods.xcodeproj b/_Pods.xcodeproj new file mode 120000 index 0000000..3c5a8e7 --- /dev/null +++ b/_Pods.xcodeproj @@ -0,0 +1 @@ +Example/Pods/Pods.xcodeproj \ No newline at end of file