From 1f21830228cc676099e411ed4c9174234b6cc720 Mon Sep 17 00:00:00 2001 From: Marin Todorov Date: Tue, 4 Jul 2017 21:12:34 +0200 Subject: [PATCH] initial comit --- .gitignore | 2 + Example/Podfile | 12 + .../RealmContent.xcodeproj/project.pbxproj | 631 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 10626 bytes .../xcschemes/RealmContent-Example.xcscheme | 115 ++++ .../xcschemes/xcschememanagement.plist | 19 + Example/RealmContent/AppDelegate.swift | 63 ++ .../RealmContent/Base.lproj/LaunchScreen.xib | 45 ++ .../RealmContent/Base.lproj/Main.storyboard | 109 +++ Example/RealmContent/DemoData.swift | 63 ++ .../AppIcon.appiconset/Contents.json | 48 ++ Example/RealmContent/Info.plist | 46 ++ Example/RealmContent/User.swift | 13 + Example/RealmContent/ViewController.swift | 76 +++ Example/RealmContent_Example.entitlements | 10 + Example/Tests/Info.plist | 24 + Example/Tests/Tests.swift | 29 + LICENSE | 19 + README.md | 29 + RealmContent.podspec | 39 ++ RealmContent/Assets/.gitkeep | 0 RealmContent/Classes/.gitkeep | 0 .../Classes/ContentListDataSource.swift | 230 +++++++ .../Classes/MarkdownContentConverter.swift | 74 ++ .../Classes/Entities/ContentElement.swift | 28 + .../Classes/Entities/ContentPage.swift | 25 + .../Classes/View/ContentViewController.swift | 220 ++++++ .../Classes/View/ImageContentCell.swift | 139 ++++ .../Classes/View/Markdown/MarkdownView.swift | 62 ++ .../Markdown/MarkdownViewController.swift | 135 ++++ .../Classes/View/TextContentCell.swift | 144 ++++ _Pods.xcodeproj | 1 + 33 files changed, 2457 insertions(+) create mode 100644 .gitignore create mode 100644 Example/Podfile create mode 100644 Example/RealmContent.xcodeproj/project.pbxproj create mode 100644 Example/RealmContent.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Example/RealmContent.xcodeproj/project.xcworkspace/xcuserdata/marin.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Example/RealmContent.xcodeproj/xcshareddata/xcschemes/RealmContent-Example.xcscheme create mode 100644 Example/RealmContent.xcodeproj/xcuserdata/marin.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Example/RealmContent/AppDelegate.swift create mode 100644 Example/RealmContent/Base.lproj/LaunchScreen.xib create mode 100644 Example/RealmContent/Base.lproj/Main.storyboard create mode 100644 Example/RealmContent/DemoData.swift create mode 100644 Example/RealmContent/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Example/RealmContent/Info.plist create mode 100644 Example/RealmContent/User.swift create mode 100644 Example/RealmContent/ViewController.swift create mode 100644 Example/RealmContent_Example.entitlements create mode 100644 Example/Tests/Info.plist create mode 100644 Example/Tests/Tests.swift create mode 100644 LICENSE create mode 100644 README.md create mode 100644 RealmContent.podspec create mode 100644 RealmContent/Assets/.gitkeep create mode 100644 RealmContent/Classes/.gitkeep create mode 100644 RealmContent/Classes/Classes/ContentListDataSource.swift create mode 100644 RealmContent/Classes/Classes/MarkdownContentConverter.swift create mode 100644 RealmContent/Classes/Entities/ContentElement.swift create mode 100644 RealmContent/Classes/Entities/ContentPage.swift create mode 100644 RealmContent/Classes/View/ContentViewController.swift create mode 100644 RealmContent/Classes/View/ImageContentCell.swift create mode 100644 RealmContent/Classes/View/Markdown/MarkdownView.swift create mode 100644 RealmContent/Classes/View/Markdown/MarkdownViewController.swift create mode 100644 RealmContent/Classes/View/TextContentCell.swift create mode 120000 _Pods.xcodeproj 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 0000000000000000000000000000000000000000..a2c021b9f3d458cd2036a578a24111c29d50717d GIT binary patch literal 10626 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 WQSv3prQKA9Nw3@;u~a6x?Ee9}LoSm5 literal 0 HcmV?d00001 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