├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Combine-Realm.podspec ├── CombineRealm.png ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── Models.swift │ ├── SceneDelegate.swift │ └── ViewController.swift ├── LICENSE.md ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── CombineRealm │ ├── Add.swift │ ├── Delete.swift │ └── Publishers.swift └── Tests ├── CombineRealmTests ├── CombineRealmLinkingObjectTests.swift ├── CombineRealmListTests.swift ├── CombineRealmObjectTests.swift ├── CombineRealmResultsTests.swift ├── CombineRealmTests.swift ├── CombineTests.swift ├── Models.swift ├── Utilities.swift └── XCTestManifests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Combine-Realm.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "Combine-Realm" 4 | s.version = "2.0.1" 5 | s.summary = "A Combine wrapper of Realm's notifications and write bindings" 6 | 7 | s.description = <<-DESC 8 | This is a Combine extension that provides an easy to use way to use Realm's natively reactive collection types as a Publishers 9 | DESC 10 | 11 | s.homepage = "https://github.com/CombineCommunity/CombineRealm.git" 12 | s.license = 'MIT' 13 | s.author = { "Istvan Kreisz" => "kreiszdev@gmail.com", "Combine Community" => "cocoapods@combine.community" } 14 | s.source = { :git => "https://github.com/CombineCommunity/CombineRealm.git", :tag => s.version.to_s } 15 | s.source_files = 'Sources/CombineRealm/*' 16 | 17 | s.requires_arc = true 18 | 19 | s.ios.deployment_target = '13.0' 20 | s.osx.deployment_target = '10.15' 21 | s.tvos.deployment_target = '13.0' 22 | s.watchos.deployment_target = '6.0' 23 | 24 | s.frameworks = 'Combine' 25 | 26 | s.swift_version = "5.1" 27 | 28 | s.dependency 'RealmSwift', '~> 5' 29 | 30 | end 31 | -------------------------------------------------------------------------------- /CombineRealm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CombineCommunity/CombineRealm/8812f3472790c7a0467c4f917179ab5ae02212ce/CombineRealm.png -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5D450BDB24F2A0C0004A2648 /* CombineRealm in Frameworks */ = {isa = PBXBuildFile; productRef = 5D450BDA24F2A0C0004A2648 /* CombineRealm */; }; 11 | 5D6C5C4423ECA6BD00853061 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6C5C4323ECA6BD00853061 /* AppDelegate.swift */; }; 12 | 5D6C5C4623ECA6BD00853061 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6C5C4523ECA6BD00853061 /* SceneDelegate.swift */; }; 13 | 5D6C5C4823ECA6BD00853061 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6C5C4723ECA6BD00853061 /* ViewController.swift */; }; 14 | 5D6C5C4B23ECA6BD00853061 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5D6C5C4923ECA6BD00853061 /* Main.storyboard */; }; 15 | 5D6C5C4D23ECA6BE00853061 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5D6C5C4C23ECA6BE00853061 /* Assets.xcassets */; }; 16 | 5D6C5C5023ECA6BE00853061 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5D6C5C4E23ECA6BE00853061 /* LaunchScreen.storyboard */; }; 17 | 5D6C5C6723ECD1A800853061 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6C5C6623ECD1A800853061 /* Models.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 5D02515823F217C000CB304A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 22 | 5D02515E23F217F400CB304A /* CombineRealmTest-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CombineRealmTest-Bridging-Header.h"; sourceTree = ""; }; 23 | 5D02515F23F217F500CB304A /* CombineRealmResultsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CombineRealmResultsTests.swift; path = ../../Tests/CombineRealmTests/CombineRealmResultsTests.swift; sourceTree = ""; }; 24 | 5D02516023F217F500CB304A /* CombineRealmObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CombineRealmObjectTests.swift; path = ../../Tests/CombineRealmTests/CombineRealmObjectTests.swift; sourceTree = ""; }; 25 | 5D02516123F217F500CB304A /* CombineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CombineTests.swift; path = ../../Tests/CombineRealmTests/CombineTests.swift; sourceTree = ""; }; 26 | 5D02516223F217F500CB304A /* Models.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Models.swift; path = ../../Tests/CombineRealmTests/Models.swift; sourceTree = ""; }; 27 | 5D02516323F217F500CB304A /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Utilities.swift; path = ../../Tests/CombineRealmTests/Utilities.swift; sourceTree = ""; }; 28 | 5D02516423F217F500CB304A /* CombineRealmLinkingObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CombineRealmLinkingObjectTests.swift; path = ../../Tests/CombineRealmTests/CombineRealmLinkingObjectTests.swift; sourceTree = ""; }; 29 | 5D02516523F217F500CB304A /* CombineRealmTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CombineRealmTests.swift; path = ../../Tests/CombineRealmTests/CombineRealmTests.swift; sourceTree = ""; }; 30 | 5D02516623F217F500CB304A /* CombineRealmListTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CombineRealmListTests.swift; path = ../../Tests/CombineRealmTests/CombineRealmListTests.swift; sourceTree = ""; }; 31 | 5D6C5C4023ECA6BD00853061 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | 5D6C5C4323ECA6BD00853061 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33 | 5D6C5C4523ECA6BD00853061 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 34 | 5D6C5C4723ECA6BD00853061 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 35 | 5D6C5C4A23ECA6BD00853061 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 36 | 5D6C5C4C23ECA6BE00853061 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | 5D6C5C4F23ECA6BE00853061 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 38 | 5D6C5C5123ECA6BE00853061 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | 5D6C5C6623ECD1A800853061 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; 40 | /* End PBXFileReference section */ 41 | 42 | /* Begin PBXFrameworksBuildPhase section */ 43 | 5D6C5C3D23ECA6BD00853061 /* Frameworks */ = { 44 | isa = PBXFrameworksBuildPhase; 45 | buildActionMask = 2147483647; 46 | files = ( 47 | 5D450BDB24F2A0C0004A2648 /* CombineRealm in Frameworks */, 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | 5D02515523F217C000CB304A /* CombineRealmTest */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 5D02516423F217F500CB304A /* CombineRealmLinkingObjectTests.swift */, 58 | 5D02516623F217F500CB304A /* CombineRealmListTests.swift */, 59 | 5D02516023F217F500CB304A /* CombineRealmObjectTests.swift */, 60 | 5D02515F23F217F500CB304A /* CombineRealmResultsTests.swift */, 61 | 5D02516523F217F500CB304A /* CombineRealmTests.swift */, 62 | 5D02516123F217F500CB304A /* CombineTests.swift */, 63 | 5D02516223F217F500CB304A /* Models.swift */, 64 | 5D02516323F217F500CB304A /* Utilities.swift */, 65 | 5D02515823F217C000CB304A /* Info.plist */, 66 | 5D02515E23F217F400CB304A /* CombineRealmTest-Bridging-Header.h */, 67 | ); 68 | path = CombineRealmTest; 69 | sourceTree = ""; 70 | }; 71 | 5D6C5C3723ECA6BD00853061 = { 72 | isa = PBXGroup; 73 | children = ( 74 | 5D6C5C4223ECA6BD00853061 /* Example */, 75 | 5D02515523F217C000CB304A /* CombineRealmTest */, 76 | 5D6C5C4123ECA6BD00853061 /* Products */, 77 | ); 78 | sourceTree = ""; 79 | }; 80 | 5D6C5C4123ECA6BD00853061 /* Products */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 5D6C5C4023ECA6BD00853061 /* Example.app */, 84 | ); 85 | name = Products; 86 | sourceTree = ""; 87 | }; 88 | 5D6C5C4223ECA6BD00853061 /* Example */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 5D6C5C4323ECA6BD00853061 /* AppDelegate.swift */, 92 | 5D6C5C4523ECA6BD00853061 /* SceneDelegate.swift */, 93 | 5D6C5C4723ECA6BD00853061 /* ViewController.swift */, 94 | 5D6C5C6623ECD1A800853061 /* Models.swift */, 95 | 5D6C5C4923ECA6BD00853061 /* Main.storyboard */, 96 | 5D6C5C4C23ECA6BE00853061 /* Assets.xcassets */, 97 | 5D6C5C4E23ECA6BE00853061 /* LaunchScreen.storyboard */, 98 | 5D6C5C5123ECA6BE00853061 /* Info.plist */, 99 | ); 100 | path = Example; 101 | sourceTree = ""; 102 | }; 103 | /* End PBXGroup section */ 104 | 105 | /* Begin PBXNativeTarget section */ 106 | 5D6C5C3F23ECA6BD00853061 /* Example */ = { 107 | isa = PBXNativeTarget; 108 | buildConfigurationList = 5D6C5C5423ECA6BE00853061 /* Build configuration list for PBXNativeTarget "Example" */; 109 | buildPhases = ( 110 | 5D6C5C3C23ECA6BD00853061 /* Sources */, 111 | 5D6C5C3D23ECA6BD00853061 /* Frameworks */, 112 | 5D6C5C3E23ECA6BD00853061 /* Resources */, 113 | ); 114 | buildRules = ( 115 | ); 116 | dependencies = ( 117 | ); 118 | name = Example; 119 | packageProductDependencies = ( 120 | 5D450BDA24F2A0C0004A2648 /* CombineRealm */, 121 | ); 122 | productName = Example; 123 | productReference = 5D6C5C4023ECA6BD00853061 /* Example.app */; 124 | productType = "com.apple.product-type.application"; 125 | }; 126 | /* End PBXNativeTarget section */ 127 | 128 | /* Begin PBXProject section */ 129 | 5D6C5C3823ECA6BD00853061 /* Project object */ = { 130 | isa = PBXProject; 131 | attributes = { 132 | LastSwiftUpdateCheck = 1130; 133 | LastUpgradeCheck = 1130; 134 | ORGANIZATIONNAME = "István Kreisz"; 135 | TargetAttributes = { 136 | 5D6C5C3F23ECA6BD00853061 = { 137 | CreatedOnToolsVersion = 11.3.1; 138 | }; 139 | }; 140 | }; 141 | buildConfigurationList = 5D6C5C3B23ECA6BD00853061 /* Build configuration list for PBXProject "Example" */; 142 | compatibilityVersion = "Xcode 9.3"; 143 | developmentRegion = en; 144 | hasScannedForEncodings = 0; 145 | knownRegions = ( 146 | en, 147 | Base, 148 | ); 149 | mainGroup = 5D6C5C3723ECA6BD00853061; 150 | packageReferences = ( 151 | 5D450BD924F2A0C0004A2648 /* XCRemoteSwiftPackageReference "CombineRealm" */, 152 | ); 153 | productRefGroup = 5D6C5C4123ECA6BD00853061 /* Products */; 154 | projectDirPath = ""; 155 | projectRoot = ""; 156 | targets = ( 157 | 5D6C5C3F23ECA6BD00853061 /* Example */, 158 | ); 159 | }; 160 | /* End PBXProject section */ 161 | 162 | /* Begin PBXResourcesBuildPhase section */ 163 | 5D6C5C3E23ECA6BD00853061 /* Resources */ = { 164 | isa = PBXResourcesBuildPhase; 165 | buildActionMask = 2147483647; 166 | files = ( 167 | 5D6C5C5023ECA6BE00853061 /* LaunchScreen.storyboard in Resources */, 168 | 5D6C5C4D23ECA6BE00853061 /* Assets.xcassets in Resources */, 169 | 5D6C5C4B23ECA6BD00853061 /* Main.storyboard in Resources */, 170 | ); 171 | runOnlyForDeploymentPostprocessing = 0; 172 | }; 173 | /* End PBXResourcesBuildPhase section */ 174 | 175 | /* Begin PBXSourcesBuildPhase section */ 176 | 5D6C5C3C23ECA6BD00853061 /* Sources */ = { 177 | isa = PBXSourcesBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | 5D6C5C4823ECA6BD00853061 /* ViewController.swift in Sources */, 181 | 5D6C5C4423ECA6BD00853061 /* AppDelegate.swift in Sources */, 182 | 5D6C5C4623ECA6BD00853061 /* SceneDelegate.swift in Sources */, 183 | 5D6C5C6723ECD1A800853061 /* Models.swift in Sources */, 184 | ); 185 | runOnlyForDeploymentPostprocessing = 0; 186 | }; 187 | /* End PBXSourcesBuildPhase section */ 188 | 189 | /* Begin PBXVariantGroup section */ 190 | 5D6C5C4923ECA6BD00853061 /* Main.storyboard */ = { 191 | isa = PBXVariantGroup; 192 | children = ( 193 | 5D6C5C4A23ECA6BD00853061 /* Base */, 194 | ); 195 | name = Main.storyboard; 196 | sourceTree = ""; 197 | }; 198 | 5D6C5C4E23ECA6BE00853061 /* LaunchScreen.storyboard */ = { 199 | isa = PBXVariantGroup; 200 | children = ( 201 | 5D6C5C4F23ECA6BE00853061 /* Base */, 202 | ); 203 | name = LaunchScreen.storyboard; 204 | sourceTree = ""; 205 | }; 206 | /* End PBXVariantGroup section */ 207 | 208 | /* Begin XCBuildConfiguration section */ 209 | 5D6C5C5223ECA6BE00853061 /* Debug */ = { 210 | isa = XCBuildConfiguration; 211 | buildSettings = { 212 | ALWAYS_SEARCH_USER_PATHS = NO; 213 | CLANG_ANALYZER_NONNULL = YES; 214 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 215 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 216 | CLANG_CXX_LIBRARY = "libc++"; 217 | CLANG_ENABLE_MODULES = YES; 218 | CLANG_ENABLE_OBJC_ARC = YES; 219 | CLANG_ENABLE_OBJC_WEAK = YES; 220 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 221 | CLANG_WARN_BOOL_CONVERSION = YES; 222 | CLANG_WARN_COMMA = YES; 223 | CLANG_WARN_CONSTANT_CONVERSION = YES; 224 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 225 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 226 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 227 | CLANG_WARN_EMPTY_BODY = YES; 228 | CLANG_WARN_ENUM_CONVERSION = YES; 229 | CLANG_WARN_INFINITE_RECURSION = YES; 230 | CLANG_WARN_INT_CONVERSION = YES; 231 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 232 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 233 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 234 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 235 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 236 | CLANG_WARN_STRICT_PROTOTYPES = YES; 237 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 238 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 239 | CLANG_WARN_UNREACHABLE_CODE = YES; 240 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 241 | COPY_PHASE_STRIP = NO; 242 | DEBUG_INFORMATION_FORMAT = dwarf; 243 | ENABLE_STRICT_OBJC_MSGSEND = YES; 244 | ENABLE_TESTABILITY = YES; 245 | GCC_C_LANGUAGE_STANDARD = gnu11; 246 | GCC_DYNAMIC_NO_PIC = NO; 247 | GCC_NO_COMMON_BLOCKS = YES; 248 | GCC_OPTIMIZATION_LEVEL = 0; 249 | GCC_PREPROCESSOR_DEFINITIONS = ( 250 | "DEBUG=1", 251 | "$(inherited)", 252 | ); 253 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 254 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 255 | GCC_WARN_UNDECLARED_SELECTOR = YES; 256 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 257 | GCC_WARN_UNUSED_FUNCTION = YES; 258 | GCC_WARN_UNUSED_VARIABLE = YES; 259 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 260 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 261 | MTL_FAST_MATH = YES; 262 | ONLY_ACTIVE_ARCH = YES; 263 | SDKROOT = iphoneos; 264 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 265 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 266 | }; 267 | name = Debug; 268 | }; 269 | 5D6C5C5323ECA6BE00853061 /* Release */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | ALWAYS_SEARCH_USER_PATHS = NO; 273 | CLANG_ANALYZER_NONNULL = YES; 274 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 275 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 276 | CLANG_CXX_LIBRARY = "libc++"; 277 | CLANG_ENABLE_MODULES = YES; 278 | CLANG_ENABLE_OBJC_ARC = YES; 279 | CLANG_ENABLE_OBJC_WEAK = YES; 280 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 281 | CLANG_WARN_BOOL_CONVERSION = YES; 282 | CLANG_WARN_COMMA = YES; 283 | CLANG_WARN_CONSTANT_CONVERSION = YES; 284 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 286 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 287 | CLANG_WARN_EMPTY_BODY = YES; 288 | CLANG_WARN_ENUM_CONVERSION = YES; 289 | CLANG_WARN_INFINITE_RECURSION = YES; 290 | CLANG_WARN_INT_CONVERSION = YES; 291 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 292 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 293 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 294 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 295 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 296 | CLANG_WARN_STRICT_PROTOTYPES = YES; 297 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 298 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 299 | CLANG_WARN_UNREACHABLE_CODE = YES; 300 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 301 | COPY_PHASE_STRIP = NO; 302 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 303 | ENABLE_NS_ASSERTIONS = NO; 304 | ENABLE_STRICT_OBJC_MSGSEND = YES; 305 | GCC_C_LANGUAGE_STANDARD = gnu11; 306 | GCC_NO_COMMON_BLOCKS = YES; 307 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 308 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 309 | GCC_WARN_UNDECLARED_SELECTOR = YES; 310 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 311 | GCC_WARN_UNUSED_FUNCTION = YES; 312 | GCC_WARN_UNUSED_VARIABLE = YES; 313 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 314 | MTL_ENABLE_DEBUG_INFO = NO; 315 | MTL_FAST_MATH = YES; 316 | SDKROOT = iphoneos; 317 | SWIFT_COMPILATION_MODE = wholemodule; 318 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 319 | VALIDATE_PRODUCT = YES; 320 | }; 321 | name = Release; 322 | }; 323 | 5D6C5C5523ECA6BE00853061 /* Debug */ = { 324 | isa = XCBuildConfiguration; 325 | buildSettings = { 326 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 327 | CODE_SIGN_STYLE = Automatic; 328 | DEVELOPMENT_TEAM = TA366CU42K; 329 | INFOPLIST_FILE = Example/Info.plist; 330 | LD_RUNPATH_SEARCH_PATHS = ( 331 | "$(inherited)", 332 | "@executable_path/Frameworks", 333 | ); 334 | PRODUCT_BUNDLE_IDENTIFIER = istvankreisz.Example; 335 | PRODUCT_NAME = "$(TARGET_NAME)"; 336 | SWIFT_VERSION = 5.0; 337 | TARGETED_DEVICE_FAMILY = "1,2"; 338 | }; 339 | name = Debug; 340 | }; 341 | 5D6C5C5623ECA6BE00853061 /* Release */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 345 | CODE_SIGN_STYLE = Automatic; 346 | DEVELOPMENT_TEAM = TA366CU42K; 347 | INFOPLIST_FILE = Example/Info.plist; 348 | LD_RUNPATH_SEARCH_PATHS = ( 349 | "$(inherited)", 350 | "@executable_path/Frameworks", 351 | ); 352 | PRODUCT_BUNDLE_IDENTIFIER = istvankreisz.Example; 353 | PRODUCT_NAME = "$(TARGET_NAME)"; 354 | SWIFT_VERSION = 5.0; 355 | TARGETED_DEVICE_FAMILY = "1,2"; 356 | }; 357 | name = Release; 358 | }; 359 | /* End XCBuildConfiguration section */ 360 | 361 | /* Begin XCConfigurationList section */ 362 | 5D6C5C3B23ECA6BD00853061 /* Build configuration list for PBXProject "Example" */ = { 363 | isa = XCConfigurationList; 364 | buildConfigurations = ( 365 | 5D6C5C5223ECA6BE00853061 /* Debug */, 366 | 5D6C5C5323ECA6BE00853061 /* Release */, 367 | ); 368 | defaultConfigurationIsVisible = 0; 369 | defaultConfigurationName = Release; 370 | }; 371 | 5D6C5C5423ECA6BE00853061 /* Build configuration list for PBXNativeTarget "Example" */ = { 372 | isa = XCConfigurationList; 373 | buildConfigurations = ( 374 | 5D6C5C5523ECA6BE00853061 /* Debug */, 375 | 5D6C5C5623ECA6BE00853061 /* Release */, 376 | ); 377 | defaultConfigurationIsVisible = 0; 378 | defaultConfigurationName = Release; 379 | }; 380 | /* End XCConfigurationList section */ 381 | 382 | /* Begin XCRemoteSwiftPackageReference section */ 383 | 5D450BD924F2A0C0004A2648 /* XCRemoteSwiftPackageReference "CombineRealm" */ = { 384 | isa = XCRemoteSwiftPackageReference; 385 | repositoryURL = "https://github.com/CombineCommunity/CombineRealm.git"; 386 | requirement = { 387 | kind = upToNextMajorVersion; 388 | minimumVersion = 2.0.0; 389 | }; 390 | }; 391 | /* End XCRemoteSwiftPackageReference section */ 392 | 393 | /* Begin XCSwiftPackageProductDependency section */ 394 | 5D450BDA24F2A0C0004A2648 /* CombineRealm */ = { 395 | isa = XCSwiftPackageProductDependency; 396 | package = 5D450BD924F2A0C0004A2648 /* XCRemoteSwiftPackageReference "CombineRealm" */; 397 | productName = CombineRealm; 398 | }; 399 | /* End XCSwiftPackageProductDependency section */ 400 | }; 401 | rootObject = 5D6C5C3823ECA6BD00853061 /* Project object */; 402 | } 403 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CombineRealm", 6 | "repositoryURL": "https://github.com/CombineCommunity/CombineRealm.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "9c8f4478f0d603afba2706882ca3212432b5aaee", 10 | "version": "2.0.0" 11 | } 12 | }, 13 | { 14 | "package": "Realm", 15 | "repositoryURL": "https://github.com/realm/realm-cocoa.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "99c7d89791dfe8730c145461d59969eb4c56c557", 19 | "version": "5.3.5" 20 | } 21 | }, 22 | { 23 | "package": "RealmCore", 24 | "repositoryURL": "https://github.com/realm/realm-core", 25 | "state": { 26 | "branch": null, 27 | "revision": "b7c49bb107cfc933016792e78fc5daa2aca71a14", 28 | "version": "6.0.19" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by István Kreisz on 06/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | let realm = try! Realm(configuration: Realm.Configuration(deleteRealmIfMigrationNeeded: true)) 17 | try! realm.write { 18 | realm.deleteAll() 19 | } 20 | return true 21 | } 22 | 23 | // MARK: UISceneSession Lifecycle 24 | 25 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Example/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Example/Example/Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Models.swift 3 | // Example 4 | // 5 | // Created by István Kreisz on 06/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | 12 | let formatter: DateFormatter = { 13 | let f = DateFormatter() 14 | f.timeStyle = .long 15 | return f 16 | }() 17 | 18 | class Color: Object { 19 | @objc dynamic var time: TimeInterval = Date().timeIntervalSinceReferenceDate 20 | @objc dynamic var colorR = Double.random(in: 0...1.0) 21 | @objc dynamic var colorG = Double.random(in: 0...1.0) 22 | @objc dynamic var colorB = Double.random(in: 0...1.0) 23 | 24 | var color: UIColor { 25 | return UIColor(red: CGFloat(colorR), green: CGFloat(colorG), blue: CGFloat(colorB), alpha: 1.0) 26 | } 27 | } 28 | 29 | class TickCounter: Object { 30 | @objc dynamic var id = UUID().uuidString 31 | @objc dynamic var ticks: Int = 0 32 | override static func primaryKey() -> String? { return "id" } 33 | } 34 | -------------------------------------------------------------------------------- /Example/Example/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Example 4 | // 5 | // Created by István Kreisz on 06/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | guard let _ = (scene as? UIWindowScene) else { return } 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Example/Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by István Kreisz on 06/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RealmSwift 11 | import Combine 12 | import CombineRealm 13 | 14 | class MainViewController: UITableViewController { 15 | 16 | var subscriptions = Set() 17 | 18 | var colors: Results! 19 | 20 | let addedColors = PassthroughSubject() 21 | let deletedColors = PassthroughSubject() 22 | 23 | let tickUpdated = PassthroughSubject() 24 | 25 | let footer: UIStackView = { 26 | let label1 = UILabel() 27 | let label2 = UILabel() 28 | label1.textAlignment = .center 29 | label2.textAlignment = .center 30 | let stackView = UIStackView(arrangedSubviews: [label1, label2]) 31 | stackView.distribution = .fillEqually 32 | stackView.axis = .horizontal 33 | return stackView 34 | }() 35 | 36 | lazy var ticker: TickCounter = { 37 | let realm = try! Realm() 38 | let ticker = TickCounter() 39 | try! realm.write { 40 | realm.add(ticker) 41 | } 42 | return ticker 43 | }() 44 | 45 | @IBAction func addTapped(_ sender: Any) { 46 | addedColors.send(Color()) 47 | } 48 | 49 | @IBAction func tickTapped(_ sender: Any) { 50 | tickUpdated.send() 51 | } 52 | 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | 56 | let realm = try! Realm() 57 | colors = realm.objects(Color.self).sorted(byKeyPath: "time", ascending: false) 58 | 59 | tickUpdated 60 | .sink { [unowned self] in 61 | let realm = try! Realm() 62 | try! realm.write { 63 | self.ticker.ticks += 1 64 | } 65 | } 66 | .store(in: &subscriptions) 67 | 68 | // Observing collection changes 69 | RealmPublishers.collection(from: colors) 70 | .map { results in "colors: \(results.count)" } 71 | .sink(receiveCompletion: { _ in 72 | print("Completed") 73 | }, receiveValue: { results in 74 | self.title = results 75 | }) 76 | .store(in: &subscriptions) 77 | 78 | // Observing changesets 79 | RealmPublishers.changeset(from: colors) 80 | .sink(receiveCompletion: { _ in 81 | print("Completed") 82 | }, receiveValue: { [unowned self] _, changes in 83 | if let changes = changes { 84 | self.tableView.applyChangeset(changes) 85 | } else { 86 | self.tableView.reloadData() 87 | } 88 | }) 89 | .store(in: &subscriptions) 90 | 91 | // Adding to realm 92 | addedColors 93 | .addToRealm() 94 | .store(in: &subscriptions) 95 | 96 | // Deleting from realm 97 | deletedColors 98 | .deleteFromRealm() 99 | .store(in: &subscriptions) 100 | 101 | // Observing a single object 102 | RealmPublishers.propertyChanges(object: ticker) 103 | .filter { $0.name == "ticks" } 104 | .map { "\($0.newValue!) ticks" } 105 | .sink(receiveCompletion: { _ in }, 106 | receiveValue: { [unowned self] ticks in 107 | (self.footer.arrangedSubviews[0] as! UILabel).text = ticks 108 | }) 109 | .store(in: &subscriptions) 110 | 111 | // Observing all database changes 112 | RealmPublishers.from(realm: realm) 113 | .map { _ in } 114 | .scan(0, { result, _ in 115 | return result + 1 116 | }) 117 | .map { "\($0) changes" } 118 | .sink(receiveCompletion: { _ in }, 119 | receiveValue: { count in 120 | (self.footer.arrangedSubviews[1] as! UILabel).text = count 121 | }) 122 | .store(in: &subscriptions) 123 | } 124 | } 125 | 126 | extension MainViewController { 127 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 128 | return colors.count 129 | } 130 | 131 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 132 | let color = colors[indexPath.row] 133 | 134 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")! 135 | cell.textLabel?.text = formatter.string(from: Date(timeIntervalSinceReferenceDate: color.time)) 136 | cell.backgroundColor = color.color 137 | return cell 138 | } 139 | 140 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 141 | return "Delete objects by tapping them" 142 | } 143 | } 144 | 145 | extension MainViewController { 146 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 147 | deletedColors.send(colors[indexPath.row]) 148 | } 149 | 150 | override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 151 | return footer 152 | } 153 | } 154 | 155 | extension UITableView { 156 | func applyChangeset(_ changes: RealmChangeset) { 157 | beginUpdates() 158 | deleteRows(at: changes.deleted.map { IndexPath(row: $0, section: 0) }, with: .automatic) 159 | insertRows(at: changes.inserted.map { IndexPath(row: $0, section: 0) }, with: .automatic) 160 | reloadRows(at: changes.updated.map { IndexPath(row: $0, section: 0) }, with: .automatic) 161 | endUpdates() 162 | } 163 | } 164 | 165 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Combine Open Source 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Realm", 6 | "repositoryURL": "https://github.com/realm/realm-cocoa.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "99c7d89791dfe8730c145461d59969eb4c56c557", 10 | "version": "5.3.5" 11 | } 12 | }, 13 | { 14 | "package": "RealmCore", 15 | "repositoryURL": "https://github.com/realm/realm-core", 16 | "state": { 17 | "branch": null, 18 | "revision": "b7c49bb107cfc933016792e78fc5daa2aca71a14", 19 | "version": "6.0.19" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CombineRealm", 8 | platforms: [ 9 | .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6) 10 | ], 11 | products: [ 12 | .library( 13 | name: "CombineRealm", 14 | targets: ["CombineRealm"]), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/realm/realm-cocoa.git", .upToNextMajor(from: "5.0.0")) 18 | ], 19 | targets: [ 20 | .target( 21 | name: "CombineRealm", 22 | dependencies: ["Realm", "RealmSwift"], 23 | path: "Sources" 24 | ), 25 | .testTarget( 26 | name: "CombineRealmTests", 27 | dependencies: ["CombineRealm"], 28 | path: "Tests" 29 | ) 30 | ], 31 | swiftLanguageVersions: [.v5] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CombineRealm 2 | 3 | ![CombineRealm Logo](CombineRealm.png) 4 | 5 | [![Version](https://img.shields.io/cocoapods/v/Combine-Realm.svg?style=flat)](http://cocoapods.org/pods/Combine-Realm) 6 | [![License](https://img.shields.io/cocoapods/l/Combine-Realm.svg?style=flat)](http://cocoapods.org/pods/Combine-Realm) 7 | ![Platform](https://img.shields.io/badge/platforms-iOS%2013%20&%20macOS%2010.15%20&%20tvOS%2013%20&%20watchOS%206-success.svg) 8 | 9 | This library is a thin wrapper around __RealmSwift__ ([Realm Docs](https://realm.io/docs/swift/latest/)), inspired by the RxSwift Community's [RxRealm](https://github.com/RxSwiftCommunity/RxRealm) library. 10 | 11 | ## Usage 12 | 13 | **Table of contents:** 14 | 15 | 1. [Observing object collections](https://github.com/istvan-kreisz/CombineRealm#observing-object-collections) 16 | 2. [Observing a single object](https://github.com/istvan-kreisz/CombineRealm#observing-a-single-object) 17 | 3. [Observing a realm instance](https://github.com/istvan-kreisz/CombineRealm#observing-a-realm-instance) 18 | 4. [Write transactions](https://github.com/istvan-kreisz/CombineRealm#write-transactions) 19 | 5. [Delete transactions](https://github.com/istvan-kreisz/CombineRealm#delete-transactions) 20 | 6. [Example app](https://github.com/istvan-kreisz/CombineRealm#example-app) 21 | 22 | ### Observing object collections 23 | 24 | CombineRealm can be used to create `Publisher`s from objects of type `Results`, `List`, `LinkingObjects` or `AnyRealmCollection`. These types are typically used to load and observe object collections from the Realm Mobile Database. 25 | 26 | #### `RealmPublishers.collection(from:synchronousStart:)` 27 | 28 | Emits an event each time the collection changes: 29 | 30 | ```swift 31 | let realm = try! Realm() 32 | let colors = realm.objects(Color.self) 33 | 34 | RealmPublishers.collection(from: colors) 35 | .map { colors in "colors: \(colors.count)" } 36 | .sink(receiveCompletion: { _ in 37 | print("Completed") 38 | }, receiveValue: { result in 39 | print(result) 40 | }) 41 | ``` 42 | 43 | The above prints out "colors: X" each time a `Color` instance is added or removed from the database. If you set `synchronousStart` to `true` (the default value), the first element will be emitted synchronously - e.g. when you're binding UI it might not be possible for an asynchronous notification to come through. 44 | 45 | #### `RealmPublishers.array(from:synchronousStart:)` 46 | Upon each change fetches a snapshot of the Realm collection and converts it to an array value (for example if you want to use array methods on the collection): 47 | 48 | ```swift 49 | let realm = try! Realm() 50 | let colors = realm.objects(Color.self) 51 | 52 | RealmPublishers.array(from: colors) 53 | .map { colors in colors.prefix(3) } 54 | .sink(receiveCompletion: { _ in 55 | print("Completed") 56 | }, receiveValue: { colors in 57 | print(colors) 58 | }) 59 | ``` 60 | 61 | #### `RealmPublishers.changeset(from:synchronousStart:)` 62 | Emits every time the collection changes and provides the exact indexes that have been deleted, inserted or updated along with the appropriate `AnyRealmCollection` value: 63 | 64 | ```swift 65 | let realm = try! Realm() 66 | let colors = realm.objects(Color.self) 67 | 68 | RealmPublishers.changeset(from: colors) 69 | .sink(receiveCompletion: { _ in 70 | print("Completed") 71 | }, receiveValue: { results, changes in 72 | if let changes = changes { 73 | // it's an update 74 | print(results) 75 | print("deleted: \(changes.deleted)") 76 | print("inserted: \(changes.inserted)") 77 | print("updated: \(changes.updated)") 78 | } else { 79 | // it's the initial data 80 | print(results) 81 | } 82 | }) 83 | ``` 84 | 85 | #### `RealmPublishers.arrayWithChangeset(from:synchronousStart:)` 86 | 87 | Emits every time the collection changes and provides the exact indexes that have been deleted, inserted or updated along with the `Array` value: 88 | 89 | ```swift 90 | let realm = try! Realm() 91 | let colors = realm.objects(Color.self)) 92 | 93 | RealmPublishers.arrayWithChangeset(from: colors) 94 | .sink(receiveCompletion: { _ in 95 | print("Completed") 96 | }, receiveValue: { array, changes in 97 | if let changes = changes { 98 | // it's an update 99 | print(array) 100 | print("deleted: \(changes.deleted)") 101 | print("inserted: \(changes.inserted)") 102 | print("updated: \(changes.updated)") 103 | } else { 104 | // it's the initial data 105 | print(array) 106 | } 107 | }) 108 | ``` 109 | 110 | ### Observing a single object 111 | 112 | #### `RealmPublishers.from(object:emitInitialValue:properties:)` 113 | 114 | Emits every time any of the properties of the observed object change. 115 | 116 | It will by default emit the object's initial state as its first value. You can disable this behavior by using the `emitInitialValue` parameter and setting it to `false`. 117 | 118 | ```swift 119 | RealmPublishers.from(object: color) 120 | .sink(receiveCompletion: { _ in 121 | print("Completed") 122 | }) { color in 123 | print(color) 124 | } 125 | ``` 126 | 127 | You can set which property changes you'd like to observe: 128 | 129 | ```swift 130 | Observable.from(object: ticker, properties: ["red", "green", "blue"]) 131 | ``` 132 | 133 | ### Observing a realm instance 134 | 135 | #### `RealmPublishers.from(realm:)` 136 | 137 | Emits every time the realm changes: any create & update & delete operation happens in it. It provides the realm instance along with the realm change notification. 138 | 139 | ```swift 140 | let realm = try! Realm() 141 | 142 | RealmPublishers.from(realm: realm) 143 | .sink(receiveCompletion: { _ in 144 | print("Completed") 145 | }) { realm, notification in 146 | print("Something happened!") 147 | } 148 | ``` 149 | 150 | ### Write transactions 151 | 152 | #### `addToRealm()` 153 | 154 | Writes object(s) to the default Realm: `Realm(configuration: .defaultConfiguration)`. 155 | 156 | ```swift 157 | let realm = try! Realm() 158 | let colors = realm.objects(Color.self)) 159 | 160 | RealmPublishers.array(from: colors) 161 | .addToRealm() 162 | ``` 163 | 164 | #### `addToRealm(configuration:updatePolicy:onError:)` 165 | 166 | Writes object(s) to a **custom** Realm. If you want to switch threads and not use the default Realm, provide a `Realm.Configuration`. You an also provide an error handler for the observer to be called if either creating the realm reference or the write transaction raise an error: 167 | 168 | NOTE: All 3 arguments are optional, check the function definition for the default values 169 | 170 | ```swift 171 | let realm = try! Realm() 172 | let colors = realm.objects(Color.self)) 173 | 174 | RealmPublishers.array(from: colors) 175 | .addToRealm(configuration: .defaultCOnfiguration, updatePolicy: .error, onError: { 176 | print($0) 177 | }) 178 | ``` 179 | 180 | ### Delete transactions 181 | 182 | #### `deleteFromRealm()` 183 | 184 | Deletes object(s) from the object(s)'s realm: 185 | 186 | ```swift 187 | let realm = try! Realm() 188 | let colors = realm.objects(Color.self)) 189 | 190 | RealmPublishers.array(from: colors) 191 | .deleteFromRealm() 192 | ``` 193 | 194 | #### `deleteFromRealm(onError:)` 195 | 196 | Deletes object(s) from the object(s)'s realm. You an also provide an error handler for the observer to be called if either creating the realm reference or the write transaction raise an error: 197 | 198 | ```swift 199 | let realm = try! Realm() 200 | let colors = realm.objects(Color.self)) 201 | 202 | RealmPublishers.array(from: colors) 203 | .deleteFromRealm(onError: { 204 | print($0) 205 | }) 206 | ``` 207 | 208 | ### Example app 209 | 210 | To run the example project, clone the repo, navigate to the __Example__ folder and open the `Example.xcodeproj` file. 211 | 212 | To ensure that you're using the latest version of CombineRealm, in Xcode select `Update to Latest Package Versions` in the `File/Swift Packages/Add Package Dependency...` menu. 213 | 214 | The app uses CombineRealm to observe changes in and write to Realm. 215 | 216 | ## Testing 217 | 218 | To inspect the library's Unit tests, check out the files in `Tests/CombineRealmTests`. 219 | To run the tests, go to the root directory of the repo and run the command: 220 | 221 | ```swift 222 | swift test 223 | ``` 224 | 225 | ## Installation 226 | 227 | ### CocoaPods 228 | 229 | Add the following line to your Podfile and run `pod install`: 230 | 231 | ```ruby 232 | pod 'Combine-Realm' 233 | ``` 234 | Since import statements in Xcode can't contain dashes, the correct way to import the library is: 235 | 236 | ```swift 237 | import Combine_Realm 238 | ``` 239 | 240 | ### Swift Package Manager 241 | 242 | - In Xcode select `File/Swift Packages/Add Package Dependency...` 243 | - Paste `https://github.com/istvan-kreisz/CombineRealm.git` into the repository URL textfield. 244 | 245 | ### Future ideas 246 | 247 | - Add CI tests 248 | - Add Carthage support 249 | - Your ideas? 250 | 251 | ## Author 252 | 253 | __Istvan Kreisz__ 254 | 255 | [kreiszdev@gmail.com](mailto:kreiszdev@gmail.com) 256 | 257 | [@IKreisz](https://twitter.com/IKreisz) 258 | 259 | ## License 260 | 261 | CombineReachability is available under the MIT license. See the LICENSE file for more info. 262 | -------------------------------------------------------------------------------- /Sources/CombineRealm/Add.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Add.swift 3 | // CombineRealm 4 | // 5 | // Created by István Kreisz on 05/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import RealmSwift 12 | 13 | 14 | class Add: Subscriber, Cancellable { 15 | 16 | public let combineIdentifier = CombineIdentifier() 17 | 18 | private let configuration: Realm.Configuration 19 | 20 | private let updatePolicy: Realm.UpdatePolicy 21 | private let onError: ((Swift.Error) -> Void)? 22 | private var subscription: Subscription? 23 | 24 | init(configuration: Realm.Configuration, updatePolicy: Realm.UpdatePolicy, onError: ((Swift.Error) -> Void)?) { 25 | self.configuration = configuration 26 | self.updatePolicy = updatePolicy 27 | self.onError = onError 28 | } 29 | 30 | func receive(subscription: Subscription) { 31 | self.subscription = subscription 32 | subscription.request(.unlimited) 33 | } 34 | 35 | func receive(_ input: Input) -> Subscribers.Demand { 36 | do { 37 | let realm = try Realm(configuration: configuration) 38 | try realm.write { 39 | addToRealm(realm, input: input, updatePolicy: updatePolicy) 40 | } 41 | } catch let error { 42 | onError?(error) 43 | } 44 | return .unlimited 45 | } 46 | 47 | func addToRealm(_ realm: Realm, input: Input, updatePolicy: Realm.UpdatePolicy) { 48 | preconditionFailure("Subclasses must override this method") 49 | } 50 | 51 | func receive(completion: Subscribers.Completion) { 52 | subscription = nil 53 | } 54 | 55 | func cancel() { 56 | subscription?.cancel() 57 | subscription = nil 58 | } 59 | } 60 | 61 | final class AddOne: Add { 62 | override func addToRealm(_ realm: Realm, input: Input, updatePolicy: Realm.UpdatePolicy) { 63 | realm.add(input, update: updatePolicy) 64 | } 65 | } 66 | 67 | final class AddMany: Add where Input.Iterator.Element: Object { 68 | override func addToRealm(_ realm: Realm, input: Input, updatePolicy: Realm.UpdatePolicy) { 69 | realm.add(input, update: updatePolicy) 70 | } 71 | } 72 | 73 | public extension Publisher where Output: Object, Failure: Error { 74 | 75 | /** 76 | Subscribes publisher to subscriber which adds objects to a Realm. The objects are added to the default realm instance `Realm()`. 77 | 78 | - returns: `AnyCancellable` 79 | */ 80 | func addToRealm() -> AnyCancellable { 81 | return addToRealm(configuration: .defaultConfiguration) 82 | } 83 | 84 | /** 85 | Subscribes publisher to subscriber which adds objects to a Realm. 86 | 87 | - parameter configuration (by default uses `Realm.Configuration.defaultConfiguration`) to use to get a Realm for the write operations 88 | - returns: `AnyCancellable` 89 | */ 90 | func addToRealm(configuration: Realm.Configuration = .defaultConfiguration) -> AnyCancellable { 91 | return addToRealm(configuration: configuration, updatePolicy: .error) 92 | } 93 | 94 | /** 95 | Subscribes publisher to subscriber which adds objects to a Realm. 96 | 97 | - parameter configuration (by default uses `Realm.Configuration.defaultConfiguration`) to use to get a Realm for the write operations 98 | - parameter updatePolicy - update according to `Realm.UpdatePolicy` 99 | - parameter onError - closure to implement custom error handling 100 | - returns: `AnyCancellable` 101 | */ 102 | func addToRealm(configuration: Realm.Configuration = .defaultConfiguration, updatePolicy: Realm.UpdatePolicy = .error, onError: ((Swift.Error) -> Void)? = nil) -> AnyCancellable { 103 | let subscriber = AddOne(configuration: configuration, updatePolicy: updatePolicy, onError: onError) 104 | self.subscribe(subscriber) 105 | return AnyCancellable(subscriber) 106 | } 107 | } 108 | 109 | public extension Publisher where Output: Sequence, Failure: Error, Output.Iterator.Element: Object { 110 | 111 | /** 112 | Subscribes publisher to subscriber which adds objects in sequence to a Realm. The objects are added to the default realm instance `Realm()`. 113 | 114 | - returns: `AnyCancellable` 115 | */ 116 | func addToRealm() -> AnyCancellable { 117 | return addToRealm(configuration: .defaultConfiguration) 118 | } 119 | 120 | /** 121 | Subscribes publisher to subscriber which adds objects in sequence to a Realm. 122 | 123 | - parameter configuration (by default uses `Realm.Configuration.defaultConfiguration`) to use to get a Realm for the write operations 124 | - returns: `AnyCancellable` 125 | */ 126 | func addToRealm(configuration: Realm.Configuration = .defaultConfiguration) -> AnyCancellable { 127 | return addToRealm(configuration: configuration, updatePolicy: .error) 128 | } 129 | 130 | /** 131 | Subscribes publisher to subscriber which adds objects in sequence to a Realm. 132 | 133 | - parameter configuration (by default uses `Realm.Configuration.defaultConfiguration`) to use to get a Realm for the write operations 134 | - parameter updatePolicy - update according to `Realm.UpdatePolicy` 135 | - parameter onError - closure to implement custom error handling 136 | - returns: `AnyCancellable` 137 | */ 138 | func addToRealm(configuration: Realm.Configuration = .defaultConfiguration, updatePolicy: Realm.UpdatePolicy = .error, onError: ((Swift.Error) -> Void)? = nil) -> AnyCancellable { 139 | let subscriber = AddMany(configuration: configuration, updatePolicy: updatePolicy, onError: onError) 140 | self.subscribe(subscriber) 141 | return AnyCancellable(subscriber) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/CombineRealm/Delete.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Delete.swift 3 | // CombineRealm 4 | // 5 | // Created by István Kreisz on 04/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import RealmSwift 12 | 13 | 14 | class Delete: Subscriber, Cancellable { 15 | 16 | public let combineIdentifier = CombineIdentifier() 17 | 18 | private let onError: ((Swift.Error) -> Void)? 19 | private var subscription: Subscription? 20 | 21 | init(onError: ((Swift.Error) -> Void)?) { 22 | self.onError = onError 23 | } 24 | 25 | func receive(subscription: Subscription) { 26 | self.subscription = subscription 27 | subscription.request(.unlimited) 28 | } 29 | 30 | func receive(_ input: Input) -> Subscribers.Demand { 31 | do { 32 | let realm = try realmInstance(from: input) 33 | try realm.write { [weak self] in 34 | self?.deleteFromRealm(realm, input: input) 35 | } 36 | } catch let error { 37 | onError?(error) 38 | } 39 | return .unlimited 40 | } 41 | 42 | func realmInstance(from input: Input) throws -> Realm { 43 | preconditionFailure("Subclasses must override this method") 44 | } 45 | 46 | func deleteFromRealm(_ realm: Realm, input: Input) { 47 | preconditionFailure("Subclasses must override this method") 48 | } 49 | 50 | func receive(completion: Subscribers.Completion) { 51 | subscription = nil 52 | } 53 | 54 | func cancel() { 55 | subscription?.cancel() 56 | subscription = nil 57 | } 58 | } 59 | 60 | final class DeleteOne: Delete { 61 | override func realmInstance(from input: Input) throws -> Realm { 62 | guard let realm = input.realm else { 63 | throw CombineRealmError.unknown 64 | } 65 | return realm 66 | } 67 | 68 | override func deleteFromRealm(_ realm: Realm, input: Input) { 69 | realm.delete(input) 70 | } 71 | } 72 | 73 | final class DeleteMany: Delete where Input.Iterator.Element: Object { 74 | override func realmInstance(from input: Input) throws -> Realm { 75 | guard var generator = input.makeIterator() as Input.Iterator?, 76 | let first = generator.next(), 77 | let realm = first.realm else { 78 | throw CombineRealmError.unknown 79 | } 80 | return realm 81 | } 82 | 83 | override func deleteFromRealm(_ realm: Realm, input: Input) { 84 | realm.delete(input) 85 | } 86 | } 87 | 88 | public extension Publisher where Output: Object, Failure: Error { 89 | 90 | /** 91 | Subscribes publisher to subscriber which deletes objects from a Realm. The objects are deleted from the default realm instance `Realm()`. 92 | 93 | - returns: `AnyCancellable` 94 | */ 95 | func deleteFromRealm() -> AnyCancellable { 96 | return deleteFromRealm(onError: nil) 97 | } 98 | 99 | /** 100 | Subscribes publisher to subscriber which deletes objects from a Realm. 101 | 102 | - parameter onError - closure to implement custom error handling 103 | - returns: `AnyCancellable` 104 | */ 105 | func deleteFromRealm(onError: ((Swift.Error) -> Void)? = nil) -> AnyCancellable { 106 | let subscriber = DeleteOne(onError: onError) 107 | self.subscribe(subscriber) 108 | return AnyCancellable(subscriber) 109 | } 110 | } 111 | 112 | public extension Publisher where Output: Sequence, Failure: Error, Output.Iterator.Element: Object { 113 | 114 | /** 115 | Subscribes publisher to subscriber which deletes objects from a Realm. The objects are deleted from the default realm instance `Realm()`. 116 | 117 | - returns: `AnyCancellable` 118 | */ 119 | func deleteFromRealm() -> AnyCancellable { 120 | return deleteFromRealm(onError: nil) 121 | } 122 | 123 | /** 124 | Subscribes publisher to subscriber which deletes objects from a Realm. 125 | 126 | - parameter onError - closure to implement custom error handling 127 | - returns: `AnyCancellable` 128 | */ 129 | func deleteFromRealm(onError: ((Swift.Error) -> Void)? = nil) -> AnyCancellable { 130 | let subscriber = DeleteMany(onError: onError) 131 | self.subscribe(subscriber) 132 | return AnyCancellable(subscriber) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/CombineRealm/Publishers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineRealm.swift 3 | // CombineRealm 4 | // 5 | // Created by István Kreisz on 02/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import RealmSwift 12 | 13 | 14 | public enum CombineRealmError: Error { 15 | case objectDeleted 16 | case unknown 17 | } 18 | 19 | // MARK: Realm Collections type extensions 20 | 21 | /** 22 | `NotificationEmitter` is a protocol to allow for Realm's collections to be handled in a generic way. 23 | 24 | All collections already include a `addNotificationBlock(_:)` method - making them conform to `NotificationEmitter` just makes it easier to add Combine methods to them. 25 | 26 | The methods of essence in this protocol are `observe(...)`, which allow for observing for changes on Realm's collections. 27 | */ 28 | public protocol NotificationEmitter { 29 | associatedtype ElementType: RealmCollectionValue 30 | 31 | /** 32 | Returns a `NotificationToken`, which while retained enables change notifications for the current collection. 33 | 34 | - returns: `NotificationToken` - retain this value to keep notifications being emitted for the current collection. 35 | */ 36 | func observe(on queue: DispatchQueue?, _ block: @escaping (RealmCollectionChange) -> Void) -> NotificationToken 37 | 38 | func toArray() -> [ElementType] 39 | 40 | func toAnyCollection() -> AnyRealmCollection 41 | } 42 | 43 | extension List: NotificationEmitter { 44 | 45 | public func toAnyCollection() -> AnyRealmCollection { 46 | return AnyRealmCollection(self) 47 | } 48 | 49 | public typealias ElementType = Element 50 | public func toArray() -> [Element] { 51 | return Array(self) 52 | } 53 | } 54 | 55 | extension AnyRealmCollection: NotificationEmitter { 56 | public func toAnyCollection() -> AnyRealmCollection { 57 | return AnyRealmCollection(self) 58 | } 59 | 60 | public typealias ElementType = Element 61 | public func toArray() -> [Element] { 62 | return Array(self) 63 | } 64 | } 65 | 66 | extension Results: NotificationEmitter { 67 | public func toAnyCollection() -> AnyRealmCollection { 68 | return AnyRealmCollection(self) 69 | } 70 | 71 | public typealias ElementType = Element 72 | public func toArray() -> [Element] { 73 | return Array(self) 74 | } 75 | } 76 | 77 | extension LinkingObjects: NotificationEmitter { 78 | public func toAnyCollection() -> AnyRealmCollection { 79 | return AnyRealmCollection(self) 80 | } 81 | 82 | public typealias ElementType = Element 83 | public func toArray() -> [Element] { 84 | return Array(self) 85 | } 86 | } 87 | 88 | /** 89 | `RealmChangeset` is a struct that contains the data about a single realm change set. 90 | 91 | It includes the insertions, modifications, and deletions indexes in the data set that the current notification is about. 92 | */ 93 | public struct RealmChangeset { 94 | /// the indexes in the collection that were deleted 95 | public let deleted: [Int] 96 | 97 | /// the indexes in the collection that were inserted 98 | public let inserted: [Int] 99 | 100 | /// the indexes in the collection that were modified 101 | public let updated: [Int] 102 | } 103 | 104 | struct RealmPublisher: Publisher { 105 | 106 | public typealias Output = Output 107 | public typealias Failure = Failure 108 | 109 | private let handler: (AnySubscriber) -> NotificationToken 110 | private let initialValue: Output? 111 | 112 | init(initialValue: Output? = nil, handler: @escaping (AnySubscriber) -> NotificationToken) { 113 | self.handler = handler 114 | self.initialValue = initialValue 115 | } 116 | 117 | func receive(subscriber: S) where S: Subscriber, S.Failure == Failure, S.Input == Output { 118 | subscriber.receive(subscription: RealmSubscription(subscriber: subscriber, initialValue: initialValue, handler: handler)) 119 | } 120 | } 121 | 122 | final class RealmSubscription: Subscription { 123 | 124 | private var subscriber: AnySubscriber? 125 | private var token: NotificationToken? 126 | private var handler: (AnySubscriber) -> NotificationToken 127 | private var initialValue: Output? 128 | 129 | init(subscriber: S, initialValue: Output?, handler: @escaping (AnySubscriber) -> NotificationToken) 130 | where S: Subscriber, 131 | Failure == S.Failure, 132 | Output == S.Input { 133 | self.subscriber = AnySubscriber(subscriber) 134 | self.initialValue = initialValue 135 | self.handler = handler 136 | } 137 | 138 | func request(_ demand: Subscribers.Demand) { 139 | if let subscriber = subscriber, token == nil { 140 | token = handler(subscriber) 141 | if let initialValue = initialValue { 142 | _ = subscriber.receive(initialValue) 143 | } 144 | } 145 | } 146 | 147 | func cancel() { 148 | token?.invalidate() 149 | subscriber = nil 150 | } 151 | } 152 | 153 | public enum RealmPublishers { 154 | 155 | /** 156 | Returns an `AnyPublisher` that emits each time the collection data changes. 157 | The publisher emits an initial value upon subscription. 158 | 159 | - parameter from: A Realm collection of type `Output`: either `Results`, `List`, `LinkingObjects` or `AnyRealmCollection`. 160 | - parameter synchronousStart: whether the resulting `Publisher` should emit its first element synchronously (e.g. better for UI bindings) 161 | 162 | - returns: `AnyPublisher`, e.g. when called on `Results` it will return `AnyPublisher, Error>`, on a `List` it will return `AnyPublisher, Error>`, etc. 163 | */ 164 | public static func collection(from collection: Output, 165 | synchronousStart: Bool = true) 166 | -> AnyPublisher { 167 | 168 | let initialValue: Output? = synchronousStart ? collection : nil 169 | return RealmPublisher(initialValue: initialValue) { subscriber in 170 | return collection.observe(on: nil) { changeset in 171 | let value: Output 172 | 173 | switch changeset { 174 | case let .initial(latestValue): 175 | guard !synchronousStart else { return } 176 | value = latestValue 177 | case .update(let latestValue, _, _, _): 178 | value = latestValue 179 | case let .error(error): 180 | subscriber.receive(completion: .failure(error)) 181 | return 182 | } 183 | _ = subscriber.receive(value) 184 | } 185 | } 186 | .eraseToAnyPublisher() 187 | } 188 | 189 | /** 190 | Returns an `AnyPublisher, Error>` that emits each time the collection data changes. The publisher emits an initial value upon subscription. 191 | The result emits an array containing all objects from the source collection. 192 | 193 | This method emits an `Array` containing all the realm collection objects, this means they all live in the memory. If you're using this method to observe large collections you might hit memory warnings. 194 | 195 | - parameter from: A Realm collection of type `Output`: either `Results`, `List`, `LinkingObjects` or `AnyRealmCollection`. 196 | - parameter synchronousStart: whether the resulting `Publisher` should emit its first element synchronously (e.g. better for UI bindings) 197 | 198 | - returns: `AnyPublisher, Error>`, e.g. when called on `Results` it will return `AnyPublisher, Error>`, on a `List` it will return `AnyPublisher, Error>`, etc. 199 | */ 200 | public static func array(from collection: Output, 201 | synchronousStart: Bool = true) 202 | -> AnyPublisher<[Output.ElementType], Error> { 203 | 204 | return RealmPublishers.collection(from: collection, synchronousStart: synchronousStart) 205 | .map { $0.toArray() } 206 | .eraseToAnyPublisher() 207 | } 208 | 209 | /** 210 | Returns an `AnyPublisher<(Output, RealmChangeset?), Error>` that emits each time the collection data changes. The publisher emits an initial value upon subscription. 211 | 212 | When the publisher emits for the first time (if the initial notification is not coalesced with an update) the second tuple value will be `nil`. 213 | 214 | Each following emit will include a `RealmChangeset` with the indexes inserted, deleted or modified. 215 | 216 | - parameter from: A Realm collection of type `Output`: either `Results`, `List`, `LinkingObjects` or `AnyRealmCollection`. 217 | - parameter synchronousStart: whether the resulting `Publisher` should emit its first element synchronously (e.g. better for UI bindings) 218 | 219 | - returns: `AnyPublisher<(Output, RealmChangeset?), Error>`, e.g. when called on `Results` it will return `AnyPublisher<(Results, RealmChangeset?), Error>`, on a `List` it will return `AnyPublisher<(List, RealmChangeset?), Error>`, etc. 220 | */ 221 | public static func changeset(from collection: Output, 222 | synchronousStart: Bool = true) 223 | -> AnyPublisher<(AnyRealmCollection, RealmChangeset?), Error> { 224 | 225 | let initialValue: (AnyRealmCollection, RealmChangeset?)? = synchronousStart ? (collection.toAnyCollection(), nil) : nil 226 | return RealmPublisher<(AnyRealmCollection, RealmChangeset?), Error>(initialValue: initialValue) { subscriber in 227 | return collection.toAnyCollection().observe(on: nil) { changeset in 228 | switch changeset { 229 | case let .initial(value): 230 | guard !synchronousStart else { return } 231 | _ = subscriber.receive((value, nil)) 232 | case let .update(value, deletes, inserts, updates): 233 | _ = subscriber.receive((value, RealmChangeset(deleted: deletes, inserted: inserts, updated: updates))) 234 | case let .error(error): 235 | subscriber.receive(completion: .failure(error)) 236 | return 237 | } 238 | } 239 | } 240 | .eraseToAnyPublisher() 241 | } 242 | 243 | /** 244 | Returns an `AnyPublisher<(Array, RealmChangeset?), Error>` that emits each time the collection data changes. The publisher emits an initial value upon subscription. 245 | 246 | This method emits an `Array` containing all the realm collection objects, this means they all live in the memory. If you're using this method to observe large collections you might hit memory warnings. 247 | 248 | When the observable emits for the first time (if the initial notification is not coalesced with an update) the second tuple value will be `nil`. 249 | 250 | Each following emit will include a `RealmChangeset` with the indexes inserted, deleted or modified. 251 | 252 | - parameter from: A Realm collection of type `Output`: either `Results`, `List`, `LinkingObjects` or `AnyRealmCollection`. 253 | - parameter synchronousStart: whether the resulting `Publisher` should emit its first element synchronously (e.g. better for UI bindings) 254 | 255 | - returns: `AnyPublisher<(Array, RealmChangeset?), Error>`, e.g. when called on `Results` it will return `AnyPublisher<(Array, RealmChangeset?), Error>`, on a `List` it will return `AnyPublisher<(Array, RealmChangeset?), Error>`, etc. 256 | */ 257 | public static func arrayWithChangeset(from collection: Output, 258 | synchronousStart: Bool = true) 259 | -> AnyPublisher<([Output.ElementType], RealmChangeset?), Error> { 260 | 261 | return RealmPublishers.changeset(from: collection) 262 | .map { ($0.toArray(), $1) } 263 | .eraseToAnyPublisher() 264 | } 265 | 266 | /** 267 | Returns an `AnyPublisher<(Realm, Realm.Notification), Error>` that emits each time the Realm emits a notification. 268 | 269 | The Publisher you will get emits a tuple made out of: 270 | 271 | * the realm that emitted the event 272 | * the notification type: this can be either `.didChange` which occurs after a refresh or a write transaction ends, 273 | or `.refreshRequired` which happens when a write transaction occurs from a different thread on the same realm file 274 | 275 | For more information look up: [Realm.Notification](https://realm.io/docs/swift/latest/#notifications) 276 | 277 | - parameter realm: A Realm instance 278 | - returns: `AnyPublisher<(Realm, Realm.Notification), Error>`, which you can subscribe to 279 | */ 280 | public static func from(realm: Realm) -> AnyPublisher<(Realm, Realm.Notification), Error> { 281 | 282 | return RealmPublisher<(Realm, Realm.Notification), Error> { subscriber in 283 | return realm.observe { (notification: Realm.Notification, realm: Realm) in 284 | _ = subscriber.receive((realm, notification)) 285 | } 286 | } 287 | .eraseToAnyPublisher() 288 | } 289 | 290 | /** 291 | Returns an `AnyPublisher` that emits each time the object changes. The publisher emits an initial value upon subscription. 292 | 293 | - parameter object: A Realm Object to observe 294 | - parameter emitInitialValue: whether the resulting `Publisher` should emit its first element synchronously (e.g. better for UI bindings) 295 | - parameter properties: changes to which properties would triger emitting a .next event 296 | - returns: `AnyPublisher` will emit any time the observed object changes + one initial emit upon subscription 297 | */ 298 | 299 | public static func from(object: O, 300 | emitInitialValue: Bool = true, 301 | properties: [String]? = nil) 302 | -> AnyPublisher { 303 | 304 | let initialValue: O? = emitInitialValue ? object : nil 305 | return RealmPublisher(initialValue: initialValue) { subscriber in 306 | return object.observe(on: nil) { change in 307 | switch change { 308 | case let .change(_, changedProperties): 309 | if let properties = properties, !changedProperties.contains(where: { return properties.contains($0.name) }) { 310 | // if change property isn't an observed one, just return 311 | return 312 | } 313 | _ = subscriber.receive(object) 314 | case .deleted: 315 | subscriber.receive(completion: .failure(CombineRealmError.objectDeleted)) 316 | case let .error(error): 317 | subscriber.receive(completion: .failure(error)) 318 | } 319 | } 320 | } 321 | .eraseToAnyPublisher() 322 | } 323 | 324 | /** 325 | Returns an `AnyPublisher` that emits the object `PropertyChange`. 326 | 327 | - parameter object: A Realm Object to observe 328 | - returns: `AnyPublisher` will emit any time a change is detected on the object 329 | */ 330 | 331 | public static func propertyChanges(object: O) -> AnyPublisher { 332 | 333 | return RealmPublisher { subscriber in 334 | return object.observe(on: nil) { change in 335 | switch change { 336 | case let .change(_, changes): 337 | for change in changes { 338 | _ = subscriber.receive(change) 339 | } 340 | case .deleted: 341 | subscriber.receive(completion: .failure(CombineRealmError.objectDeleted)) 342 | case let .error(error): 343 | subscriber.receive(completion: .failure(error)) 344 | } 345 | } 346 | } 347 | .eraseToAnyPublisher() 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /Tests/CombineRealmTests/CombineRealmLinkingObjectTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineRealmLinkingObjectTests.swift 3 | // CombinTests 4 | // 5 | // Created by István Kreisz on 08/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | @testable import CombineRealm 10 | import Combine 11 | import RealmSwift 12 | import XCTest 13 | 14 | 15 | class CombineRealmLinkingObjectTests: XCTestCase { 16 | 17 | var subscriptions = Set() 18 | 19 | let realm = inMemoryRealm() 20 | 21 | override func setUp() { 22 | try! realm.write { 23 | realm.deleteAll() 24 | } 25 | } 26 | 27 | func testLinkingObjectsType() { 28 | var results = [Int]() 29 | let exp = expectation(description: "") 30 | let configuration = realm.configuration 31 | 32 | let message = Message("first") 33 | try! realm.write { 34 | realm.add(message) 35 | } 36 | 37 | RealmPublishers.array(from: message.mentions) 38 | .map { $0.count } 39 | .prefix(3) 40 | .sink(receiveCompletion: { _ in 41 | exp.fulfill() 42 | }, receiveValue: { 43 | results.append($0) 44 | }) 45 | .store(in: &subscriptions) 46 | 47 | let user1 = User("user1") 48 | user1.lastMessage = message 49 | 50 | try! realm.write { 51 | realm.add(user1) 52 | } 53 | 54 | DispatchQueue.global(qos: .background).sync { 55 | let realm = try! Realm(configuration: configuration) 56 | let user1 = realm.objects(User.self).first! 57 | try! realm.write { 58 | realm.delete(user1) 59 | } 60 | } 61 | 62 | wait(for: [exp], timeout: 10) 63 | 64 | XCTAssertEqual(results[0], 0) 65 | XCTAssertEqual(results[1], 1) 66 | XCTAssertEqual(results[2], 0) 67 | } 68 | 69 | func testLinkingObjectsTypeChangeset() { 70 | var results = [String]() 71 | let exp = expectation(description: "") 72 | let configuration = realm.configuration 73 | 74 | let message = Message("first") 75 | try! realm.write { 76 | realm.add(message) 77 | } 78 | 79 | RealmPublishers.changeset(from: message.mentions) 80 | .map(stringifyChanges) 81 | .prefix(3) 82 | .sink(receiveCompletion: { _ in 83 | exp.fulfill() 84 | }, receiveValue: { 85 | results.append($0) 86 | }) 87 | .store(in: &subscriptions) 88 | 89 | let user1 = User("user1") 90 | user1.lastMessage = message 91 | 92 | try! realm.write { 93 | realm.add(user1) 94 | } 95 | 96 | DispatchQueue.global(qos: .background).sync { 97 | let realm = try! Realm(configuration: configuration) 98 | let user1 = realm.objects(User.self).first! 99 | try! realm.write { 100 | realm.delete(user1) 101 | } 102 | } 103 | 104 | wait(for: [exp], timeout: 0.1) 105 | 106 | XCTAssertEqual(results[0], "count:0") 107 | XCTAssertEqual(results[1], "count:1 inserted:[0] deleted:[] updated:[]") 108 | XCTAssertEqual(results[2], "count:0 inserted:[] deleted:[0] updated:[]") 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Tests/CombineRealmTests/CombineRealmListTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineRealmListTests.swift 3 | // CombinTests 4 | // 5 | // Created by István Kreisz on 08/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | @testable import CombineRealm 10 | import Combine 11 | import RealmSwift 12 | import XCTest 13 | 14 | 15 | class CombineRealmListTests: XCTestCase { 16 | 17 | var subscriptions = Set() 18 | 19 | let realm = inMemoryRealm() 20 | 21 | override func setUp() { 22 | try! realm.write { 23 | realm.deleteAll() 24 | } 25 | } 26 | 27 | func testListType() { 28 | var results = [Int]() 29 | let exp = expectation(description: "") 30 | let configuration = realm.configuration 31 | 32 | let message = Message("first") 33 | try! realm.write { 34 | realm.add(message) 35 | } 36 | 37 | RealmPublishers.array(from: message.recipients) 38 | .map { $0.count } 39 | .prefix(3) 40 | .sink(receiveCompletion: { _ in 41 | exp.fulfill() 42 | }, receiveValue: { 43 | results.append($0) 44 | }) 45 | .store(in: &subscriptions) 46 | 47 | try! realm.write { 48 | message.recipients.append(User("user1")) 49 | } 50 | 51 | DispatchQueue.global(qos: .background).sync { 52 | let realm = try! Realm(configuration: configuration) 53 | let message = realm.objects(Message.self).first! 54 | try! realm.write { 55 | message.recipients.remove(at: 0) 56 | } 57 | } 58 | 59 | wait(for: [exp], timeout: 0.1) 60 | 61 | XCTAssertEqual(results[0], 0) 62 | XCTAssertEqual(results[1], 1) 63 | XCTAssertEqual(results[2], 0) 64 | } 65 | 66 | func testListTypeChangeset() { 67 | var results = [String]() 68 | let exp = expectation(description: "") 69 | let configuration = realm.configuration 70 | 71 | let message = Message("first") 72 | try! realm.write { 73 | realm.add(message) 74 | } 75 | 76 | RealmPublishers.changeset(from: message.recipients) 77 | .map(stringifyChanges) 78 | .prefix(3) 79 | .sink(receiveCompletion: { _ in 80 | exp.fulfill() 81 | }, receiveValue: { 82 | results.append($0) 83 | }) 84 | .store(in: &subscriptions) 85 | 86 | try! realm.write { 87 | message.recipients.append(User("user1")) 88 | } 89 | 90 | DispatchQueue.global(qos: .background).sync { 91 | let realm = try! Realm(configuration: configuration) 92 | let message = realm.objects(Message.self).first! 93 | try! realm.write { 94 | message.recipients.remove(at: 0) 95 | } 96 | } 97 | 98 | wait(for: [exp], timeout: 0.1) 99 | 100 | XCTAssertEqual(results[0], "count:0") 101 | XCTAssertEqual(results[1], "count:1 inserted:[0] deleted:[] updated:[]") 102 | XCTAssertEqual(results[2], "count:0 inserted:[] deleted:[0] updated:[]") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tests/CombineRealmTests/CombineRealmObjectTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineRealmObjectTests.swift 3 | // CombinTests 4 | // 5 | // Created by István Kreisz on 08/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | @testable import CombineRealm 10 | import RealmSwift 11 | import Combine 12 | import XCTest 13 | 14 | 15 | class CombineRealmObjectTests: XCTestCase { 16 | 17 | var subscriptions = Set() 18 | 19 | let realm = inMemoryRealm() 20 | 21 | override func setUp() { 22 | try! realm.write { 23 | realm.deleteAll() 24 | } 25 | } 26 | 27 | func testObjectChangeNotifications() { 28 | var results = [String]() 29 | let exp1 = expectation(description: "") 30 | let exp2 = expectation(description: "") 31 | 32 | let configuration = realm.configuration 33 | 34 | let idValue = 1024 35 | let object = UniqueObject(idValue) 36 | 37 | try! realm.write { 38 | realm.add(object) 39 | } 40 | 41 | RealmPublishers.from(object: object) 42 | .map { $0.name } 43 | .sink(receiveCompletion: { completion in 44 | if case .failure = completion { 45 | exp2.fulfill() 46 | } 47 | }, receiveValue: { 48 | results.append($0) 49 | if results.count == 3 { 50 | exp1.fulfill() 51 | } 52 | }) 53 | .store(in: &subscriptions) 54 | 55 | try! realm.write { 56 | object.name = "test1" 57 | } 58 | 59 | DispatchQueue.global(qos: .background).sync { 60 | let realm = try! Realm(configuration: configuration) 61 | try! realm.write { 62 | realm.objects(UniqueObject.self).filter("id == %@", idValue).first!.name = "test2" 63 | } 64 | } 65 | 66 | wait(for: [exp1], timeout: 0.1) 67 | 68 | XCTAssertEqual(results, ["", "test1", "test2"]) 69 | 70 | try! realm.write { 71 | realm.delete(object) 72 | } 73 | 74 | wait(for: [exp2], timeout: 0.1) 75 | } 76 | 77 | func testObjectEmitsInitialChange() { 78 | var result = false 79 | let exp = expectation(description: "") 80 | 81 | let object = UniqueObject(1024) 82 | try! realm.write { 83 | realm.add(object) 84 | } 85 | 86 | RealmPublishers.from(object: object, emitInitialValue: true) 87 | .prefix(1) 88 | .sink(receiveCompletion: { _ in 89 | exp.fulfill() 90 | }, receiveValue: { _ in 91 | result = true 92 | }) 93 | .store(in: &subscriptions) 94 | 95 | wait(for: [exp], timeout: 0.1) 96 | 97 | XCTAssert(result) 98 | } 99 | 100 | func testObjectDoesntEmitInitialValue() { 101 | var result = false 102 | let exp = expectation(description: "") 103 | 104 | let object = UniqueObject(1024) 105 | try! realm.write { 106 | realm.add(object) 107 | } 108 | 109 | RealmPublishers.from(object: object, emitInitialValue: false) 110 | .prefix(1) 111 | .sink(receiveCompletion: { _ in 112 | exp.fulfill() 113 | }, receiveValue: { _ in 114 | result = true 115 | }) 116 | .store(in: &subscriptions) 117 | 118 | _ = XCTWaiter.wait(for: [exp], timeout: 0.1) 119 | 120 | XCTAssertFalse(result) 121 | } 122 | 123 | func testObjectPropertyChangeNotifications() { 124 | var results = [String]() 125 | let exp1 = expectation(description: "") 126 | let exp2 = expectation(description: "") 127 | 128 | let configuration = realm.configuration 129 | 130 | let idValue = 1024 131 | let object = UniqueObject(idValue) 132 | 133 | try! realm.write { 134 | realm.add(object) 135 | } 136 | 137 | RealmPublishers.propertyChanges(object: object) 138 | .map { "\($0.name):\($0.newValue!)" } 139 | .sink(receiveCompletion: { completion in 140 | if case .failure = completion { 141 | exp2.fulfill() 142 | } 143 | }, receiveValue: { 144 | results.append($0) 145 | if results.count == 2 { 146 | exp1.fulfill() 147 | } 148 | }) 149 | .store(in: &subscriptions) 150 | 151 | try! realm.write { 152 | object.name = "test1" 153 | } 154 | 155 | DispatchQueue.global(qos: .background).sync { 156 | let realm = try! Realm(configuration: configuration) 157 | try! realm.write { 158 | realm.objects(UniqueObject.self).filter("id == %@", idValue).first!.name = "test2" 159 | } 160 | } 161 | 162 | wait(for: [exp1], timeout: 0.1) 163 | 164 | XCTAssertEqual(results, ["name:test1", "name:test2"]) 165 | 166 | try! realm.write { 167 | realm.delete(object) 168 | } 169 | 170 | wait(for: [exp2], timeout: 0.1) 171 | } 172 | 173 | func testObjectChangeNotificationsForProperties() { 174 | var results = [String]() 175 | let exp1 = expectation(description: "") 176 | let exp2 = expectation(description: "") 177 | 178 | let configuration = realm.configuration 179 | 180 | let idValue = 1024 181 | let object = UniqueObject(idValue) 182 | 183 | try! realm.write { 184 | realm.add(object) 185 | } 186 | 187 | RealmPublishers.from(object: object, emitInitialValue: false, properties: ["name"]) 188 | .map { "\($0.name)" } 189 | .sink(receiveCompletion: { completion in 190 | if case .failure = completion { 191 | exp2.fulfill() 192 | } 193 | }, receiveValue: { 194 | results.append($0) 195 | if results.count == 2 { 196 | exp1.fulfill() 197 | } 198 | }) 199 | .store(in: &subscriptions) 200 | 201 | try! realm.write { 202 | object.name = "test1" 203 | } 204 | 205 | DispatchQueue.global(qos: .background).sync { 206 | let realm = try! Realm(configuration: configuration) 207 | try! realm.write { 208 | realm.objects(UniqueObject.self).filter("id == %@", idValue).first!.name = "test2" 209 | } 210 | } 211 | 212 | wait(for: [exp1], timeout: 0.1) 213 | 214 | XCTAssertEqual(results, ["test1", "test2"]) 215 | 216 | try! realm.write { 217 | realm.delete(object) 218 | } 219 | 220 | wait(for: [exp2], timeout: 0.1) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /Tests/CombineRealmTests/CombineRealmResultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineRealmTests.swift 3 | // CombinTests 4 | // 5 | // Created by István Kreisz on 08/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | @testable import CombineRealm 10 | import RealmSwift 11 | import Combine 12 | import XCTest 13 | 14 | 15 | class CombineRealmResultsTests: XCTestCase { 16 | 17 | var subscriptions = Set() 18 | 19 | let realm = inMemoryRealm() 20 | 21 | override func setUp() { 22 | try! realm.write { 23 | realm.deleteAll() 24 | } 25 | } 26 | 27 | func testEmittedResultsValues() { 28 | var results = [[String]]() 29 | let exp = expectation(description: "") 30 | 31 | RealmPublishers.collection(from: realm.objects(Message.self)) 32 | .map { Array($0.map { $0.text }) } 33 | .dropFirst() 34 | .prefix(3) 35 | .sink(receiveCompletion: { _ in 36 | exp.fulfill() 37 | }, receiveValue: { 38 | results.append($0) 39 | }) 40 | .store(in: &subscriptions) 41 | 42 | let first = addMessage(realm, text: "first(Results)") 43 | addMessage(realm, text: "second(Results)") 44 | deleteMessage(realm, message: first) 45 | 46 | wait(for: [exp], timeout: 0.1) 47 | 48 | XCTAssertEqual(results[0], ["first(Results)"]) 49 | XCTAssertEqual(results[1], ["first(Results)", "second(Results)"]) 50 | XCTAssertEqual(results[2], ["second(Results)"]) 51 | } 52 | 53 | func testEmittedArrayValues() { 54 | var results = [[String]]() 55 | let exp = expectation(description: "") 56 | 57 | RealmPublishers.array(from: realm.objects(Message.self)) 58 | .map { $0.map { $0.text } } 59 | .dropFirst() 60 | .prefix(3) 61 | .sink(receiveCompletion: { _ in 62 | exp.fulfill() 63 | }, receiveValue: { 64 | results.append($0) 65 | }) 66 | .store(in: &subscriptions) 67 | 68 | let first = addMessage(realm, text: "first(Results)") 69 | addMessage(realm, text: "second(Results)") 70 | deleteMessage(realm, message: first) 71 | 72 | wait(for: [exp], timeout: 0.1) 73 | 74 | XCTAssertEqual(results[0], ["first(Results)"]) 75 | XCTAssertEqual(results[1], ["first(Results)", "second(Results)"]) 76 | XCTAssertEqual(results[2], ["second(Results)"]) 77 | } 78 | 79 | func testEmittedChangeset() { 80 | var results = [String]() 81 | let exp = expectation(description: "") 82 | 83 | // initial data 84 | addMessage(realm, text: "first(Changeset)") 85 | 86 | RealmPublishers.changeset(from: realm.objects(Message.self).sorted(byKeyPath: "text")) 87 | .map(stringifyChanges) 88 | .prefix(4) 89 | .sink(receiveCompletion: { _ in 90 | exp.fulfill() 91 | }, receiveValue: { 92 | results.append($0) 93 | }) 94 | .store(in: &subscriptions) 95 | 96 | // insert 97 | addMessage(realm, text: "second(Changeset)") 98 | 99 | // update 100 | try! realm.write { 101 | realm.objects(Message.self).filter("text='second(Changeset)'").first!.text = "third(Changeset)" 102 | } 103 | 104 | // coalesced + delete 105 | try! realm.write { 106 | realm.add(Message("zzzzz(Changeset)")) 107 | realm.delete(realm.objects(Message.self).filter("text='first(Changeset)'").first!) 108 | } 109 | 110 | wait(for: [exp], timeout: 0.1) 111 | 112 | XCTAssertEqual(results[0], "count:1") 113 | XCTAssertEqual(results[1], "count:2 inserted:[1] deleted:[] updated:[]") 114 | XCTAssertEqual(results[2], "count:2 inserted:[] deleted:[] updated:[1]") 115 | XCTAssertEqual(results[3], "count:2 inserted:[1] deleted:[0] updated:[]") 116 | } 117 | 118 | func testEmittedArrayChangeset() { 119 | var changesetResults = [String]() 120 | var arrayResults = [[String]]() 121 | let exp = expectation(description: "") 122 | 123 | // initial data 124 | addMessage(realm, text: "first(Changeset)") 125 | 126 | RealmPublishers.arrayWithChangeset(from: realm.objects(Message.self).sorted(byKeyPath: "text")) 127 | .map { (arg) -> (String, [String]) in 128 | let (result, changes) = arg 129 | if let changes = changes { 130 | return ("count:\(result.count) inserted:\(changes.inserted) deleted:\(changes.deleted) updated:\(changes.updated)", result.map { $0.text }) 131 | } else { 132 | return ("count:\(result.count)", result.map { $0.text }) 133 | } 134 | } 135 | .prefix(4) 136 | .sink(receiveCompletion: { _ in 137 | exp.fulfill() 138 | }, receiveValue: { 139 | changesetResults.append($0.0) 140 | arrayResults.append($0.1) 141 | }) 142 | .store(in: &subscriptions) 143 | 144 | // insert 145 | addMessage(realm, text: "second(Changeset)") 146 | 147 | // update 148 | try! realm.write { 149 | realm.objects(Message.self).filter("text='second(Changeset)'").first!.text = "third(Changeset)" 150 | } 151 | 152 | // coalesced + delete 153 | try! realm.write { 154 | realm.add(Message("zzzzz(Changeset)")) 155 | realm.delete(realm.objects(Message.self).filter("text='first(Changeset)'").first!) 156 | } 157 | 158 | wait(for: [exp], timeout: 0.1) 159 | 160 | XCTAssertEqual(changesetResults[0], "count:1") 161 | XCTAssertEqual(changesetResults[1], "count:2 inserted:[1] deleted:[] updated:[]") 162 | XCTAssertEqual(changesetResults[2], "count:2 inserted:[] deleted:[] updated:[1]") 163 | XCTAssertEqual(changesetResults[3], "count:2 inserted:[1] deleted:[0] updated:[]") 164 | 165 | XCTAssertEqual(arrayResults[0], ["first(Changeset)"]) 166 | XCTAssertEqual(arrayResults[1], ["first(Changeset)", "second(Changeset)"]) 167 | XCTAssertEqual(arrayResults[2], ["first(Changeset)", "third(Changeset)"]) 168 | XCTAssertEqual(arrayResults[3], ["third(Changeset)", "zzzzz(Changeset)"]) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Tests/CombineRealmTests/CombineRealmTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineRealmTests.swift 3 | // CombinTests 4 | // 5 | // Created by István Kreisz on 08/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | @testable import CombineRealm 10 | import RealmSwift 11 | import Combine 12 | import XCTest 13 | 14 | 15 | class CombineRealmTests: XCTestCase { 16 | 17 | var subscriptions = Set() 18 | 19 | let realm = inMemoryRealm() 20 | 21 | override func setUp() { 22 | try! realm.write { 23 | realm.deleteAll() 24 | } 25 | } 26 | 27 | func testRealmDidChangeNotifications() { 28 | var results = [Realm.Notification]() 29 | let exp = expectation(description: "") 30 | 31 | let configuration = realm.configuration 32 | 33 | RealmPublishers.from(realm: realm) 34 | .prefix(2) 35 | .sink(receiveCompletion: { _ in 36 | exp.fulfill() 37 | }, receiveValue: { 38 | results.append($0.1) 39 | }) 40 | .store(in: &subscriptions) 41 | 42 | try! realm.write { 43 | realm.add(Message("first")) 44 | } 45 | 46 | DispatchQueue.global(qos: .background).sync { 47 | let realm = try! Realm(configuration: configuration) 48 | try! realm.write { 49 | realm.add(Message("second")) 50 | } 51 | } 52 | 53 | wait(for: [exp], timeout: 0.1) 54 | 55 | XCTAssertEqual(results[0], .didChange) 56 | XCTAssertEqual(results[1], .didChange) 57 | } 58 | 59 | func testRealmRefreshRequiredNotifications() { 60 | let realmId = UUID().uuidString 61 | let realm = inMemoryRealm(id: realmId, autorefresh: false) 62 | var results = [Realm.Notification]() 63 | let exp = expectation(description: "") 64 | 65 | RealmPublishers.from(realm: realm) 66 | .prefix(2) 67 | .sink(receiveCompletion: { _ in 68 | exp.fulfill() 69 | }, receiveValue: { 70 | results.append($0.1) 71 | }) 72 | .store(in: &subscriptions) 73 | 74 | try! realm.write { 75 | realm.add(Message("first")) 76 | } 77 | 78 | DispatchQueue.global(qos: .background).async { 79 | let realm = inMemoryRealm(id: realmId) 80 | try! realm.write { 81 | realm.add(Message("second")) 82 | } 83 | } 84 | 85 | wait(for: [exp], timeout: 1.0) 86 | 87 | XCTAssertEqual(results[0], .didChange) 88 | XCTAssertEqual(results[1], .refreshRequired) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Tests/CombineRealmTests/CombineTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineTests.swift 3 | // CombineTests 4 | // 5 | // Created by István Kreisz on 07/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | @testable import CombineRealm 10 | import RealmSwift 11 | import Combine 12 | import XCTest 13 | 14 | 15 | class CombineRealmWriteTests: XCTestCase { 16 | 17 | var subscriptions = Set() 18 | 19 | let realm = inMemoryRealm() 20 | 21 | override func setUp() { 22 | try! realm.write { 23 | realm.deleteAll() 24 | } 25 | } 26 | 27 | func testAddObject() { 28 | var results = [String]() 29 | let exp = expectation(description: "") 30 | 31 | RealmPublishers.array(from: realm.objects(Message.self)) 32 | .map { $0.map { $0.text } } 33 | .dropFirst() 34 | .prefix(1) 35 | .sink(receiveCompletion: { _ in 36 | exp.fulfill() 37 | }, receiveValue: { 38 | results = $0 39 | }) 40 | .store(in: &subscriptions) 41 | 42 | Just(Message("1")) 43 | .addToRealm(configuration: realm.configuration) 44 | .store(in: &subscriptions) 45 | 46 | wait(for: [exp], timeout: 0.1) 47 | 48 | XCTAssertEqual(results[0], "1") 49 | } 50 | 51 | func testAddSequence() { 52 | var results = [String]() 53 | let exp = expectation(description: "") 54 | 55 | RealmPublishers.array(from: realm.objects(Message.self)) 56 | .map { $0.map { $0.text } } 57 | .dropFirst() 58 | .prefix(1) 59 | .sink(receiveCompletion: { _ in 60 | exp.fulfill() 61 | }, receiveValue: { 62 | results = $0 63 | }) 64 | .store(in: &subscriptions) 65 | 66 | 67 | Just([Message("1"), Message("2")]) 68 | .addToRealm(configuration: realm.configuration) 69 | .store(in: &subscriptions) 70 | 71 | wait(for: [exp], timeout: 0.1) 72 | 73 | XCTAssertEqual(results[0], "1") 74 | XCTAssertEqual(results[1], "2") 75 | } 76 | 77 | func testAddUpdateObjects() { 78 | var results = [[Int]]() 79 | let exp = expectation(description: "") 80 | 81 | RealmPublishers.array(from: realm.objects(UniqueObject.self).sorted(byKeyPath: "id")) 82 | .map { $0.map { $0.id } } 83 | .dropFirst() 84 | .prefix(2) 85 | .sink(receiveCompletion: { _ in 86 | exp.fulfill() 87 | }, receiveValue: { 88 | results.append($0) 89 | }) 90 | .store(in: &subscriptions) 91 | 92 | Just([UniqueObject(1), UniqueObject(2)]) 93 | .addToRealm(configuration: realm.configuration) 94 | .store(in: &subscriptions) 95 | Just([UniqueObject(1), UniqueObject(3)]) 96 | .addToRealm(configuration: realm.configuration, updatePolicy: .all) 97 | .store(in: &subscriptions) 98 | 99 | wait(for: [exp], timeout: 0.1) 100 | 101 | XCTAssertEqual(results[0], [1, 2]) 102 | XCTAssertEqual(results[1], [1, 2, 3]) 103 | } 104 | 105 | func testDeleteItem() { 106 | var results = [[Int]]() 107 | let exp = expectation(description: "") 108 | 109 | RealmPublishers.array(from: realm.objects(UniqueObject.self).sorted(byKeyPath: "id")) 110 | .map { $0.map { $0.id } } 111 | .dropFirst() 112 | .prefix(3) 113 | .sink(receiveCompletion: { _ in 114 | exp.fulfill() 115 | }, receiveValue: { 116 | results.append($0) 117 | }) 118 | .store(in: &subscriptions) 119 | 120 | let object1 = UniqueObject(1) 121 | let object2 = UniqueObject(2) 122 | try! realm.write { 123 | realm.add([object1, object2]) 124 | } 125 | 126 | Just(object1) 127 | .deleteFromRealm() 128 | .store(in: &subscriptions) 129 | Just(object2) 130 | .deleteFromRealm() 131 | .store(in: &subscriptions) 132 | 133 | wait(for: [exp], timeout: 0.1) 134 | 135 | XCTAssertEqual(results[0], [1, 2]) 136 | XCTAssertEqual(results[1], [2]) 137 | XCTAssertEqual(results[2], []) 138 | } 139 | 140 | 141 | func testDeleteItems() { 142 | var results = [[Int]]() 143 | let exp = expectation(description: "") 144 | 145 | RealmPublishers.array(from: realm.objects(UniqueObject.self).sorted(byKeyPath: "id")) 146 | .map { $0.map { $0.id } } 147 | .dropFirst() 148 | .prefix(3) 149 | .sink(receiveCompletion: { _ in 150 | exp.fulfill() 151 | }, receiveValue: { 152 | results.append($0) 153 | }) 154 | .store(in: &subscriptions) 155 | 156 | let object1 = UniqueObject(1) 157 | let object2 = UniqueObject(2) 158 | let object3 = UniqueObject(3) 159 | let object4 = UniqueObject(4) 160 | 161 | try! realm.write { 162 | realm.add([object1, object2, object3, object4]) 163 | } 164 | 165 | Just([object1, object2]) 166 | .deleteFromRealm() 167 | .store(in: &subscriptions) 168 | 169 | Just([object3, object4]) 170 | .deleteFromRealm() 171 | .store(in: &subscriptions) 172 | 173 | wait(for: [exp], timeout: 0.1) 174 | 175 | XCTAssertEqual(results[0], [1, 2, 3, 4]) 176 | XCTAssertEqual(results[1], [3, 4]) 177 | XCTAssertEqual(results[2], []) 178 | } 179 | 180 | func testAddObjectsFromDifferentThreads() { 181 | let realmId = UUID().uuidString 182 | let realm = inMemoryRealm(id: realmId) 183 | let configuration = realm.configuration 184 | 185 | var results = [Int]() 186 | let exp = expectation(description: "") 187 | 188 | RealmPublishers.array(from: realm.objects(UniqueObject.self).sorted(byKeyPath: "id")) 189 | .map { $0.map { $0.id } } 190 | .filter { $0.count == 6 } 191 | .prefix(1) 192 | .sink(receiveCompletion: { _ in 193 | exp.fulfill() 194 | }, receiveValue: { 195 | results = $0 196 | }) 197 | .store(in: &subscriptions) 198 | 199 | Just(UniqueObject(1)) 200 | .addToRealm(configuration: configuration) 201 | .store(in: &subscriptions) 202 | 203 | // write on background thread 204 | DispatchQueue.global(qos: .background).sync { 205 | Just(UniqueObject(2)) 206 | .addToRealm(configuration: configuration) 207 | .store(in: &self.subscriptions) 208 | } 209 | 210 | // write on main scheduler 211 | DispatchQueue.global(qos: .background).sync { 212 | Just(UniqueObject(3)) 213 | .receive(on: DispatchQueue.main) 214 | .addToRealm(configuration: configuration) 215 | .store(in: &self.subscriptions) 216 | } 217 | 218 | // write on bg scheduler 219 | DispatchQueue.main.async { 220 | Just(UniqueObject(4)) 221 | .receive(on: DispatchQueue.global(qos: .background)) 222 | .addToRealm(configuration: configuration) 223 | .store(in: &self.subscriptions) 224 | } 225 | 226 | // subscribe on main, write in bg 227 | DispatchQueue.main.async { 228 | Just([UniqueObject(5), UniqueObject(6)]) 229 | .receive(on: DispatchQueue.global(qos: .background)) 230 | .addToRealm(configuration: configuration) 231 | .store(in: &self.subscriptions) 232 | } 233 | 234 | wait(for: [exp], timeout: 0.1) 235 | 236 | XCTAssertEqual(results, [1, 2, 3, 4, 5, 6]) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Tests/CombineRealmTests/Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Models.swift 3 | // CombinTests 4 | // 5 | // Created by István Kreisz on 08/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | 13 | // MARK: Message 14 | class Message: Object { 15 | @objc dynamic var text = "" 16 | 17 | let recipients = List() 18 | let mentions = LinkingObjects(fromType: User.self, property: "lastMessage") 19 | 20 | convenience init(_ text: String) { 21 | self.init() 22 | self.text = text 23 | } 24 | } 25 | 26 | // MARK: User 27 | class User: Object { 28 | @objc dynamic var name = "" 29 | @objc dynamic var lastMessage: Message? 30 | 31 | convenience init(_ name: String) { 32 | self.init() 33 | self.name = name 34 | } 35 | } 36 | 37 | // MARK: UniqueObject 38 | class UniqueObject: Object { 39 | @objc dynamic var id = 0 40 | @objc dynamic var name = "" 41 | 42 | convenience init(_ id: Int) { 43 | self.init() 44 | self.id = id 45 | } 46 | 47 | override class func primaryKey() -> String? { 48 | return "id" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/CombineRealmTests/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Models.swift 3 | // CombineTests 4 | // 5 | // Created by István Kreisz on 07/02/2020. 6 | // Copyright (c) Combine Community. All rights reserved. 7 | // 8 | 9 | @testable import CombineRealm 10 | import RealmSwift 11 | import Foundation 12 | 13 | 14 | enum WriteError: Error { 15 | case def 16 | } 17 | 18 | func inMemoryRealm(id: String = UUID().uuidString, autorefresh: Bool = true) -> Realm { 19 | var conf = Realm.Configuration() 20 | conf.inMemoryIdentifier = id 21 | let realm = try! Realm(configuration: conf) 22 | realm.autorefresh = autorefresh 23 | return realm 24 | } 25 | 26 | func stringifyChanges(_ arg: (AnyRealmCollection, RealmChangeset?)) -> String { 27 | let (result, changes) = arg 28 | if let changes = changes { 29 | return "count:\(result.count) inserted:\(changes.inserted) deleted:\(changes.deleted) updated:\(changes.updated)" 30 | } else { 31 | return "count:\(result.count)" 32 | } 33 | } 34 | 35 | @discardableResult func addMessage(_ realm: Realm, text: String) -> Message { 36 | let message = Message(text) 37 | try! realm.write { 38 | realm.add(message) 39 | } 40 | return message 41 | } 42 | 43 | func deleteMessage(_ realm: Realm, message: Message) { 44 | try! realm.write { 45 | realm.delete(message) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/CombineRealmTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(CombineRealmTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import CombineRealmTests 4 | 5 | //var tests = [XCTestCaseEntry]() 6 | //tests += CombineRealmTests.allTests() 7 | //XCTMain(tests) 8 | --------------------------------------------------------------------------------