├── .gitattributes ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ ├── CoreRender-Package.xcscheme │ ├── CoreRender.xcscheme │ ├── CoreRenderObjC.xcscheme │ ├── CoreRenderObjCTests.xcscheme │ └── CoreRenderTests.xcscheme ├── Demo ├── CoreRenderDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── CoreRenderDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── ContentView.swift │ ├── DemoWidget.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── ViewController.swift ├── Package.swift ├── README.md ├── Sources ├── CoreRender │ ├── Bridge.swift │ ├── CommonNodeTypes.swift │ └── Gestures.swift └── CoreRenderObjC │ ├── CRContext.h │ ├── CRContext.mm │ ├── CRCoordinator+Private.h │ ├── CRCoordinator.h │ ├── CRCoordinator.mm │ ├── CRHostingView.h │ ├── CRHostingView.mm │ ├── CRMacros.h │ ├── CRNode.h │ ├── CRNode.mm │ ├── CRNodeBridge.h │ ├── CRNodeBridge.mm │ ├── CRNodeBuilder+Modifiers.h │ ├── CRNodeBuilder+Modifiers.mm │ ├── CRNodeBuilder.h │ ├── CRNodeBuilder.mm │ ├── CRNodeHierarchy.h │ ├── CRNodeHierarchy.mm │ ├── CRNodeLayoutSpec.h │ ├── CRNodeLayoutSpec.mm │ ├── CRUmbrellaHeader.h │ ├── UIView+CRNode.h │ ├── UIView+CRNode.mm │ ├── YGLayout.h │ ├── YGLayout.mm │ ├── Yoga.c │ ├── Yoga.h │ └── include │ └── CoreRenderObjC.h ├── Tests ├── CoreRenderObjCTests │ └── CRNodeTests.m ├── CoreRenderTests │ ├── BridgeTest.swift │ └── XCTestManifests.swift └── LinuxMain.swift ├── docs └── assets │ ├── carbon_4.png │ ├── logo_new.png │ └── screen_2.png └── format_objc.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | *.c linguist-language=ObjectiveC++ 2 | *.mm linguist-language=ObjectiveC++ 3 | *.mm linguist-language=ObjectiveC++ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | .DS_Store 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | # CocoaPods 34 | # 35 | # We recommend against adding the Pods directory to your .gitignore. However 36 | # you should judge for yourself, the pros and cons are mentioned at: 37 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 38 | # 39 | # Pods/ 40 | 41 | # Carthage 42 | # 43 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 44 | # Carthage/Checkouts 45 | 46 | Carthage/Build 47 | 48 | # fastlane 49 | # 50 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 51 | # screenshots whenever they are needed. 52 | # For more information about the recommended setup visit: 53 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 54 | 55 | fastlane/report.xml 56 | fastlane/Preview.html 57 | fastlane/screenshots/**/*.png 58 | fastlane/test_output 59 | 60 | # Code Injection 61 | # 62 | # After new code Injection tools there's a generated folder /iOSInjectionProject 63 | # https://github.com/johnno1962/injectionforxcode 64 | 65 | iOSInjectionProject/ 66 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/CoreRender-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 66 | 67 | 72 | 73 | 75 | 81 | 82 | 83 | 85 | 91 | 92 | 93 | 94 | 95 | 105 | 106 | 112 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/CoreRender.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/CoreRenderObjC.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/CoreRenderObjCTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/CoreRenderTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Demo/CoreRenderDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1622F3D223687938007C7E00 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1622F3D123687938007C7E00 /* AppDelegate.swift */; }; 11 | 1622F3D623687938007C7E00 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1622F3D523687938007C7E00 /* ContentView.swift */; }; 12 | 1622F3D82368793A007C7E00 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1622F3D72368793A007C7E00 /* Assets.xcassets */; }; 13 | 1622F3DB2368793A007C7E00 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1622F3DA2368793A007C7E00 /* Preview Assets.xcassets */; }; 14 | 1622F3DE2368793A007C7E00 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1622F3DC2368793A007C7E00 /* LaunchScreen.storyboard */; }; 15 | 1622F3E623687988007C7E00 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1622F3E523687988007C7E00 /* ViewController.swift */; }; 16 | 16E97B6223687B7E00CEFB67 /* CoreRenderObjC in Frameworks */ = {isa = PBXBuildFile; productRef = 16E97B6123687B7E00CEFB67 /* CoreRenderObjC */; }; 17 | 16E97B6423687B7E00CEFB67 /* CoreRender in Frameworks */ = {isa = PBXBuildFile; productRef = 16E97B6323687B7E00CEFB67 /* CoreRender */; }; 18 | 16E97B6623687C3D00CEFB67 /* DemoWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E97B6523687C3D00CEFB67 /* DemoWidget.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 1622F3CE23687938007C7E00 /* CoreRenderDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CoreRenderDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 1622F3D123687938007C7E00 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | 1622F3D523687938007C7E00 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 25 | 1622F3D72368793A007C7E00 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | 1622F3DA2368793A007C7E00 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 27 | 1622F3DD2368793A007C7E00 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | 1622F3DF2368793A007C7E00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 1622F3E523687988007C7E00 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 30 | 16E97B6523687C3D00CEFB67 /* DemoWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoWidget.swift; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 1622F3CB23687938007C7E00 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | 16E97B6223687B7E00CEFB67 /* CoreRenderObjC in Frameworks */, 39 | 16E97B6423687B7E00CEFB67 /* CoreRender in Frameworks */, 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXFrameworksBuildPhase section */ 44 | 45 | /* Begin PBXGroup section */ 46 | 1622F3C523687938007C7E00 = { 47 | isa = PBXGroup; 48 | children = ( 49 | 1622F3D023687938007C7E00 /* CoreRenderDemo */, 50 | 1622F3CF23687938007C7E00 /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | 1622F3CF23687938007C7E00 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 1622F3CE23687938007C7E00 /* CoreRenderDemo.app */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | 1622F3D023687938007C7E00 /* CoreRenderDemo */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 1622F3D123687938007C7E00 /* AppDelegate.swift */, 66 | 16E97B6523687C3D00CEFB67 /* DemoWidget.swift */, 67 | 1622F3D523687938007C7E00 /* ContentView.swift */, 68 | 1622F3E523687988007C7E00 /* ViewController.swift */, 69 | 1622F3D72368793A007C7E00 /* Assets.xcassets */, 70 | 1622F3DC2368793A007C7E00 /* LaunchScreen.storyboard */, 71 | 1622F3DF2368793A007C7E00 /* Info.plist */, 72 | 1622F3D92368793A007C7E00 /* Preview Content */, 73 | ); 74 | path = CoreRenderDemo; 75 | sourceTree = ""; 76 | }; 77 | 1622F3D92368793A007C7E00 /* Preview Content */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | 1622F3DA2368793A007C7E00 /* Preview Assets.xcassets */, 81 | ); 82 | path = "Preview Content"; 83 | sourceTree = ""; 84 | }; 85 | /* End PBXGroup section */ 86 | 87 | /* Begin PBXNativeTarget section */ 88 | 1622F3CD23687938007C7E00 /* CoreRenderDemo */ = { 89 | isa = PBXNativeTarget; 90 | buildConfigurationList = 1622F3E22368793A007C7E00 /* Build configuration list for PBXNativeTarget "CoreRenderDemo" */; 91 | buildPhases = ( 92 | 1622F3CA23687938007C7E00 /* Sources */, 93 | 1622F3CB23687938007C7E00 /* Frameworks */, 94 | 1622F3CC23687938007C7E00 /* Resources */, 95 | ); 96 | buildRules = ( 97 | ); 98 | dependencies = ( 99 | ); 100 | name = CoreRenderDemo; 101 | packageProductDependencies = ( 102 | 16E97B6123687B7E00CEFB67 /* CoreRenderObjC */, 103 | 16E97B6323687B7E00CEFB67 /* CoreRender */, 104 | ); 105 | productName = CoreRenderDemo; 106 | productReference = 1622F3CE23687938007C7E00 /* CoreRenderDemo.app */; 107 | productType = "com.apple.product-type.application"; 108 | }; 109 | /* End PBXNativeTarget section */ 110 | 111 | /* Begin PBXProject section */ 112 | 1622F3C623687938007C7E00 /* Project object */ = { 113 | isa = PBXProject; 114 | attributes = { 115 | LastSwiftUpdateCheck = 1110; 116 | LastUpgradeCheck = 1110; 117 | ORGANIZATIONNAME = "Alex Usbergo"; 118 | TargetAttributes = { 119 | 1622F3CD23687938007C7E00 = { 120 | CreatedOnToolsVersion = 11.1; 121 | }; 122 | }; 123 | }; 124 | buildConfigurationList = 1622F3C923687938007C7E00 /* Build configuration list for PBXProject "CoreRenderDemo" */; 125 | compatibilityVersion = "Xcode 9.3"; 126 | developmentRegion = en; 127 | hasScannedForEncodings = 0; 128 | knownRegions = ( 129 | en, 130 | Base, 131 | ); 132 | mainGroup = 1622F3C523687938007C7E00; 133 | packageReferences = ( 134 | 16E97B6023687B7E00CEFB67 /* XCRemoteSwiftPackageReference "CoreRender" */, 135 | ); 136 | productRefGroup = 1622F3CF23687938007C7E00 /* Products */; 137 | projectDirPath = ""; 138 | projectRoot = ""; 139 | targets = ( 140 | 1622F3CD23687938007C7E00 /* CoreRenderDemo */, 141 | ); 142 | }; 143 | /* End PBXProject section */ 144 | 145 | /* Begin PBXResourcesBuildPhase section */ 146 | 1622F3CC23687938007C7E00 /* Resources */ = { 147 | isa = PBXResourcesBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | 1622F3DE2368793A007C7E00 /* LaunchScreen.storyboard in Resources */, 151 | 1622F3DB2368793A007C7E00 /* Preview Assets.xcassets in Resources */, 152 | 1622F3D82368793A007C7E00 /* Assets.xcassets in Resources */, 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXResourcesBuildPhase section */ 157 | 158 | /* Begin PBXSourcesBuildPhase section */ 159 | 1622F3CA23687938007C7E00 /* Sources */ = { 160 | isa = PBXSourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | 1622F3D223687938007C7E00 /* AppDelegate.swift in Sources */, 164 | 1622F3E623687988007C7E00 /* ViewController.swift in Sources */, 165 | 16E97B6623687C3D00CEFB67 /* DemoWidget.swift in Sources */, 166 | 1622F3D623687938007C7E00 /* ContentView.swift in Sources */, 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXSourcesBuildPhase section */ 171 | 172 | /* Begin PBXVariantGroup section */ 173 | 1622F3DC2368793A007C7E00 /* LaunchScreen.storyboard */ = { 174 | isa = PBXVariantGroup; 175 | children = ( 176 | 1622F3DD2368793A007C7E00 /* Base */, 177 | ); 178 | name = LaunchScreen.storyboard; 179 | sourceTree = ""; 180 | }; 181 | /* End PBXVariantGroup section */ 182 | 183 | /* Begin XCBuildConfiguration section */ 184 | 1622F3E02368793A007C7E00 /* Debug */ = { 185 | isa = XCBuildConfiguration; 186 | buildSettings = { 187 | ALWAYS_SEARCH_USER_PATHS = NO; 188 | CLANG_ANALYZER_NONNULL = YES; 189 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 190 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 191 | CLANG_CXX_LIBRARY = "libc++"; 192 | CLANG_ENABLE_MODULES = YES; 193 | CLANG_ENABLE_OBJC_ARC = YES; 194 | CLANG_ENABLE_OBJC_WEAK = YES; 195 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 196 | CLANG_WARN_BOOL_CONVERSION = YES; 197 | CLANG_WARN_COMMA = YES; 198 | CLANG_WARN_CONSTANT_CONVERSION = YES; 199 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 200 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 201 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 202 | CLANG_WARN_EMPTY_BODY = YES; 203 | CLANG_WARN_ENUM_CONVERSION = YES; 204 | CLANG_WARN_INFINITE_RECURSION = YES; 205 | CLANG_WARN_INT_CONVERSION = YES; 206 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 207 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 208 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 209 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 210 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 211 | CLANG_WARN_STRICT_PROTOTYPES = YES; 212 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 213 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 214 | CLANG_WARN_UNREACHABLE_CODE = YES; 215 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 216 | COPY_PHASE_STRIP = NO; 217 | DEBUG_INFORMATION_FORMAT = dwarf; 218 | ENABLE_STRICT_OBJC_MSGSEND = YES; 219 | ENABLE_TESTABILITY = YES; 220 | GCC_C_LANGUAGE_STANDARD = gnu11; 221 | GCC_DYNAMIC_NO_PIC = NO; 222 | GCC_NO_COMMON_BLOCKS = YES; 223 | GCC_OPTIMIZATION_LEVEL = 0; 224 | GCC_PREPROCESSOR_DEFINITIONS = ( 225 | "DEBUG=1", 226 | "$(inherited)", 227 | ); 228 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 229 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 230 | GCC_WARN_UNDECLARED_SELECTOR = YES; 231 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 232 | GCC_WARN_UNUSED_FUNCTION = YES; 233 | GCC_WARN_UNUSED_VARIABLE = YES; 234 | IPHONEOS_DEPLOYMENT_TARGET = 13.1; 235 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 236 | MTL_FAST_MATH = YES; 237 | ONLY_ACTIVE_ARCH = YES; 238 | SDKROOT = iphoneos; 239 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 240 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 241 | }; 242 | name = Debug; 243 | }; 244 | 1622F3E12368793A007C7E00 /* Release */ = { 245 | isa = XCBuildConfiguration; 246 | buildSettings = { 247 | ALWAYS_SEARCH_USER_PATHS = NO; 248 | CLANG_ANALYZER_NONNULL = YES; 249 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 250 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 251 | CLANG_CXX_LIBRARY = "libc++"; 252 | CLANG_ENABLE_MODULES = YES; 253 | CLANG_ENABLE_OBJC_ARC = YES; 254 | CLANG_ENABLE_OBJC_WEAK = YES; 255 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 256 | CLANG_WARN_BOOL_CONVERSION = YES; 257 | CLANG_WARN_COMMA = YES; 258 | CLANG_WARN_CONSTANT_CONVERSION = YES; 259 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 260 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 261 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 262 | CLANG_WARN_EMPTY_BODY = YES; 263 | CLANG_WARN_ENUM_CONVERSION = YES; 264 | CLANG_WARN_INFINITE_RECURSION = YES; 265 | CLANG_WARN_INT_CONVERSION = YES; 266 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 267 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 268 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 269 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 270 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 271 | CLANG_WARN_STRICT_PROTOTYPES = YES; 272 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 273 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 274 | CLANG_WARN_UNREACHABLE_CODE = YES; 275 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 276 | COPY_PHASE_STRIP = NO; 277 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 278 | ENABLE_NS_ASSERTIONS = NO; 279 | ENABLE_STRICT_OBJC_MSGSEND = YES; 280 | GCC_C_LANGUAGE_STANDARD = gnu11; 281 | GCC_NO_COMMON_BLOCKS = YES; 282 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 283 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 284 | GCC_WARN_UNDECLARED_SELECTOR = YES; 285 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 286 | GCC_WARN_UNUSED_FUNCTION = YES; 287 | GCC_WARN_UNUSED_VARIABLE = YES; 288 | IPHONEOS_DEPLOYMENT_TARGET = 13.1; 289 | MTL_ENABLE_DEBUG_INFO = NO; 290 | MTL_FAST_MATH = YES; 291 | SDKROOT = iphoneos; 292 | SWIFT_COMPILATION_MODE = wholemodule; 293 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 294 | VALIDATE_PRODUCT = YES; 295 | }; 296 | name = Release; 297 | }; 298 | 1622F3E32368793A007C7E00 /* Debug */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 302 | CODE_SIGN_STYLE = Automatic; 303 | DEVELOPMENT_ASSET_PATHS = "\"CoreRenderDemo/Preview Content\""; 304 | DEVELOPMENT_TEAM = 9Z67YL2L6Z; 305 | ENABLE_PREVIEWS = YES; 306 | INFOPLIST_FILE = CoreRenderDemo/Info.plist; 307 | LD_RUNPATH_SEARCH_PATHS = ( 308 | "$(inherited)", 309 | "@executable_path/Frameworks", 310 | ); 311 | PRODUCT_BUNDLE_IDENTIFIER = io.coreRender.CoreRenderDemo; 312 | PRODUCT_NAME = "$(TARGET_NAME)"; 313 | SWIFT_VERSION = 5.0; 314 | TARGETED_DEVICE_FAMILY = "1,2"; 315 | }; 316 | name = Debug; 317 | }; 318 | 1622F3E42368793A007C7E00 /* Release */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 322 | CODE_SIGN_STYLE = Automatic; 323 | DEVELOPMENT_ASSET_PATHS = "\"CoreRenderDemo/Preview Content\""; 324 | DEVELOPMENT_TEAM = 9Z67YL2L6Z; 325 | ENABLE_PREVIEWS = YES; 326 | INFOPLIST_FILE = CoreRenderDemo/Info.plist; 327 | LD_RUNPATH_SEARCH_PATHS = ( 328 | "$(inherited)", 329 | "@executable_path/Frameworks", 330 | ); 331 | PRODUCT_BUNDLE_IDENTIFIER = io.coreRender.CoreRenderDemo; 332 | PRODUCT_NAME = "$(TARGET_NAME)"; 333 | SWIFT_VERSION = 5.0; 334 | TARGETED_DEVICE_FAMILY = "1,2"; 335 | }; 336 | name = Release; 337 | }; 338 | /* End XCBuildConfiguration section */ 339 | 340 | /* Begin XCConfigurationList section */ 341 | 1622F3C923687938007C7E00 /* Build configuration list for PBXProject "CoreRenderDemo" */ = { 342 | isa = XCConfigurationList; 343 | buildConfigurations = ( 344 | 1622F3E02368793A007C7E00 /* Debug */, 345 | 1622F3E12368793A007C7E00 /* Release */, 346 | ); 347 | defaultConfigurationIsVisible = 0; 348 | defaultConfigurationName = Release; 349 | }; 350 | 1622F3E22368793A007C7E00 /* Build configuration list for PBXNativeTarget "CoreRenderDemo" */ = { 351 | isa = XCConfigurationList; 352 | buildConfigurations = ( 353 | 1622F3E32368793A007C7E00 /* Debug */, 354 | 1622F3E42368793A007C7E00 /* Release */, 355 | ); 356 | defaultConfigurationIsVisible = 0; 357 | defaultConfigurationName = Release; 358 | }; 359 | /* End XCConfigurationList section */ 360 | 361 | /* Begin XCRemoteSwiftPackageReference section */ 362 | 16E97B6023687B7E00CEFB67 /* XCRemoteSwiftPackageReference "CoreRender" */ = { 363 | isa = XCRemoteSwiftPackageReference; 364 | repositoryURL = "https://github.com/alexdrone/CoreRender"; 365 | requirement = { 366 | branch = master; 367 | kind = branch; 368 | }; 369 | }; 370 | /* End XCRemoteSwiftPackageReference section */ 371 | 372 | /* Begin XCSwiftPackageProductDependency section */ 373 | 16E97B6123687B7E00CEFB67 /* CoreRenderObjC */ = { 374 | isa = XCSwiftPackageProductDependency; 375 | package = 16E97B6023687B7E00CEFB67 /* XCRemoteSwiftPackageReference "CoreRender" */; 376 | productName = CoreRenderObjC; 377 | }; 378 | 16E97B6323687B7E00CEFB67 /* CoreRender */ = { 379 | isa = XCSwiftPackageProductDependency; 380 | package = 16E97B6023687B7E00CEFB67 /* XCRemoteSwiftPackageReference "CoreRender" */; 381 | productName = CoreRender; 382 | }; 383 | /* End XCSwiftPackageProductDependency section */ 384 | }; 385 | rootObject = 1622F3C623687938007C7E00 /* Project object */; 386 | } 387 | -------------------------------------------------------------------------------- /Demo/CoreRenderDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/CoreRenderDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/CoreRenderDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CoreRender", 6 | "repositoryURL": "https://github.com/alexdrone/CoreRender", 7 | "state": { 8 | "branch": "master", 9 | "revision": "f037c40be86d247577a3c7bde3264b5ea2140a20", 10 | "version": null 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Demo/CoreRenderDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | 4 | @UIApplicationMain 5 | class AppDelegate: UIResponder, UIApplicationDelegate { 6 | func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 9 | return true 10 | } 11 | 12 | func application( 13 | _ application: UIApplication, 14 | configurationForConnecting connectingSceneSession: UISceneSession, 15 | options: UIScene.ConnectionOptions 16 | ) -> UISceneConfiguration { 17 | // Called when a new scene session is being created. 18 | // Use this method to select a configuration to create the new scene with. 19 | return UISceneConfiguration( 20 | name: "Default Configuration", 21 | sessionRole: connectingSceneSession.role) 22 | } 23 | 24 | func application( 25 | _ application: UIApplication, 26 | didDiscardSceneSessions sceneSessions: Set) { 27 | // Called when the user discards a scene session. 28 | // If any sessions were discarded while the application was not running, this will be called 29 | // shortly after application:didFinishLaunchingWithOptions. 30 | // Use this method to release any resources that were specific to the discarded scenes, 31 | //as they will not return. 32 | } 33 | } 34 | 35 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 36 | 37 | var useSwiftUI = false 38 | var window: UIWindow? 39 | 40 | func scene( 41 | _ scene: UIScene, 42 | willConnectTo session: UISceneSession, 43 | options connectionOptions: UIScene.ConnectionOptions) { 44 | 45 | if (useSwiftUI) { 46 | let contentView = ContentView() 47 | // Use a UIHostingCoordinator as window root view coordinator. 48 | if let windowScene = scene as? UIWindowScene { 49 | let window = UIWindow(windowScene: windowScene) 50 | window.rootViewController = UIHostingController(rootView: contentView) 51 | self.window = window 52 | window.makeKeyAndVisible() 53 | } 54 | } else { 55 | // Use a normal ViewCoordinator as window root view coordinator. 56 | if let windowScene = scene as? UIWindowScene { 57 | let window = UIWindow(windowScene: windowScene) 58 | window.rootViewController = ViewCoordinator() 59 | self.window = window 60 | window.makeKeyAndVisible() 61 | } 62 | } 63 | } 64 | 65 | func sceneDidDisconnect(_ scene: UIScene) { 66 | } 67 | 68 | func sceneDidBecomeActive(_ scene: UIScene) { 69 | } 70 | 71 | func sceneWillResignActive(_ scene: UIScene) { 72 | } 73 | 74 | func sceneWillEnterForeground(_ scene: UIScene) { 75 | } 76 | 77 | func sceneDidEnterBackground(_ scene: UIScene) { 78 | } 79 | } 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /Demo/CoreRenderDemo/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 | } -------------------------------------------------------------------------------- /Demo/CoreRenderDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/CoreRenderDemo/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 | -------------------------------------------------------------------------------- /Demo/CoreRenderDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import CoreRender 3 | import CoreRenderObjC 4 | 5 | struct ContentView: View { 6 | var body: some View { 7 | Text("Hello World") 8 | } 9 | } 10 | 11 | struct ContentView_Previews: PreviewProvider { 12 | static var previews: some View { 13 | ContentView() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Demo/CoreRenderDemo/DemoWidget.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreRender 3 | import CoreRenderObjC 4 | 5 | // MARK: - Coordinator 6 | 7 | class DemoWidgetCoordinator: Coordinator { 8 | var count: UInt = 0 9 | var isRotated: Bool = false 10 | 11 | @objc dynamic func increase() { 12 | count += 1 13 | setNeedsReconcile() 14 | } 15 | 16 | override func onLayout() { 17 | // Override this to manually override the layout of some of the views in the view hierarchy. 18 | // e.g. 19 | // view(withKey: Const.increaseButtonKey)?.frame = ... 20 | } 21 | } 22 | 23 | // MARK: - Body 24 | 25 | func makeDemoWidget(context: Context, coordinator: DemoWidgetCoordinator) -> OpaqueNodeBuilder { 26 | VStackNode { 27 | LabelNode(text: "\(coordinator.count)") 28 | .font(UIFont.systemFont(ofSize: 24, weight: .black)) 29 | .textAlignment(.center) 30 | .textColor(.darkText) 31 | .background(.secondarySystemBackground) 32 | .width(Const.size + 8 * CGFloat(coordinator.count)) 33 | .height(Const.size) 34 | .margin(Const.margin) 35 | .cornerRadius(Const.cornerRadius) 36 | LabelNode(text: ">> TAP HERE TO SPIN THE BUTTON >>") 37 | .font(UIFont.systemFont(ofSize: 12, weight: .bold)) 38 | .textAlignment(.center) 39 | .textColor(.systemOrange) 40 | .height(Const.size) 41 | .margin(Const.margin) 42 | .userInteractionEnabled(true) 43 | .onTouchUpInside { _ in 44 | coordinator.doSomeFunkyStuff() 45 | } 46 | HStackNode { 47 | ButtonNode(key: Const.increaseButtonKey) 48 | .text("TAP HERE TO INCREASE COUNT") 49 | .font(UIFont.systemFont(ofSize: 12, weight: .bold)) 50 | .setTarget( 51 | coordinator, action: #selector(DemoWidgetCoordinator.increase), for: .touchUpInside) 52 | .background(.systemTeal) 53 | .padding(Const.margin * 2) 54 | .cornerRadius(Const.cornerRadius) 55 | EmptyNode() 56 | } 57 | } 58 | .alignItems(.center) 59 | .matchHostingViewWidth(withMargin: 0) 60 | } 61 | 62 | // MARK: - Manual View Manipulation Example 63 | 64 | extension DemoWidgetCoordinator { 65 | // Example of manual access to the underlying view hierarchy. 66 | // Transitions can be performed in the node description as well, this is just an 67 | // example of manual view hierarchy manipulation. 68 | func doSomeFunkyStuff() { 69 | let transform = isRotated 70 | ? CGAffineTransform.identity 71 | : CGAffineTransform.init(rotationAngle: .pi) 72 | isRotated.toggle() 73 | UIView.animate(withDuration: 1) { 74 | self.view(withKey: Const.increaseButtonKey)?.transform = transform 75 | } 76 | } 77 | } 78 | 79 | // MARK: - Constants 80 | 81 | struct Const { 82 | static let increaseButtonKey = "button_increase" 83 | static let size: CGFloat = 48.0 84 | static let cornerRadius: CGFloat = 8.0 85 | static let margin: CGFloat = 4.0 86 | } 87 | -------------------------------------------------------------------------------- /Demo/CoreRenderDemo/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 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Demo/CoreRenderDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/CoreRenderDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import CoreRender 3 | import CoreRenderObjC 4 | 5 | class ViewCoordinator: UIViewController { 6 | var hostingView: HostingView! 7 | let context = Context() 8 | 9 | override func loadView() { 10 | hostingView = HostingView(context: context, with: [.useSafeAreaInsets]) { context in 11 | Component(context: context) { context, coordinator in 12 | makeDemoWidget(context: context, coordinator: coordinator) 13 | }.builder() 14 | } 15 | self.view = hostingView 16 | } 17 | 18 | override func viewDidLayoutSubviews() { 19 | super.viewDidLayoutSubviews() 20 | hostingView.setNeedsLayout() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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: "CoreRender", 8 | platforms: [ 9 | .iOS(.v10) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library( 14 | name: "CoreRenderObjC", 15 | targets: ["CoreRenderObjC"]), 16 | .library( 17 | name: "CoreRender", 18 | targets: ["CoreRender"]), 19 | ], 20 | dependencies: [ 21 | // Dependencies declare other packages that this package depends on. 22 | // .package(url: /* package url */, from: "1.0.0"), 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 26 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 27 | .target( 28 | name: "CoreRenderObjC", 29 | dependencies: []), 30 | .target( 31 | name: "CoreRender", 32 | dependencies: ["CoreRenderObjC"]), 33 | .testTarget( 34 | name: "CoreRenderObjCTests", 35 | dependencies: ["CoreRenderObjC"]), 36 | .testTarget( 37 | name: "CoreRenderTests", 38 | dependencies: ["CoreRenderObjC", "CoreRender"]), 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Moved to https://github.com/alexdrone/Render 2 | -------------------------------------------------------------------------------- /Sources/CoreRender/Bridge.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import CoreRenderObjC 4 | 5 | // MARK: - Component 6 | 7 | /// A piece of user interface. 8 | /// This is a transient object that represent the description of a particulat subtree at a 9 | /// given state. 10 | /// 11 | /// You create custom views by declaring types that conform to the `Component` 12 | /// protocol. Implement the required `body` property to provide the content 13 | /// and behavior for your custom view. 14 | public struct Component: OpaqueNodeBuilderConvertible { 15 | private let context: Context 16 | private let key: String 17 | private let props: [AnyProp] 18 | private let body: (Context, C) -> OpaqueNodeBuilder 19 | 20 | public init( 21 | context: Context, 22 | key: String = NSStringFromClass(C.self), 23 | props: [AnyProp] = [], 24 | body: @escaping (Context, C) -> OpaqueNodeBuilder 25 | ) { 26 | self.context = context 27 | self.key = key 28 | self.props = props 29 | self.body = body 30 | } 31 | 32 | /// Forward the call to `build` to return the root node for this hierarchy. 33 | public func builder() -> OpaqueNodeBuilder { 34 | makeComponent(type: C.self, context: context, key: key, props: props, body: body) 35 | } 36 | } 37 | 38 | /// Pure function builder for `Component`. 39 | public func makeComponent( 40 | type: C.Type, 41 | context: Context, 42 | key: String = NSStringFromClass(C.self), 43 | props: [AnyProp] = [], 44 | body: (Context, C) -> OpaqueNodeBuilder 45 | ) -> OpaqueNodeBuilder { 46 | let reuseIdentifier = NSStringFromClass(C.self) 47 | let coordinator = context.coordinator(CoordinatorDescriptor(type: C.self, key: key)) as! C 48 | for setter in props { 49 | setter.apply(coordinator: coordinator) 50 | } 51 | return body(context, coordinator) 52 | .withReuseIdentifier(reuseIdentifier) 53 | .withCoordinator(coordinator) 54 | } 55 | 56 | // MARK: - OpaqueNodeBuilderConvertible 57 | 58 | public protocol OpaqueNodeBuilderConvertible { 59 | func builder() -> OpaqueNodeBuilder 60 | } 61 | 62 | extension OpaqueNodeBuilder: OpaqueNodeBuilderConvertible { 63 | public func builder() -> OpaqueNodeBuilder { self } 64 | } 65 | 66 | // MARK: - Function builders 67 | 68 | /// Node builder. 69 | /// 70 | /// - `withReuseIdentifier`: The reuse identifier for this node is its hierarchy. 71 | /// Identifiers help Render understand which items have changed. 72 | /// A custom *reuseIdentifier* is mandatory if the node has a custom creation closure. 73 | /// - `withKey`: A unique key for the component/node (necessary if the associated 74 | /// component is stateful). 75 | /// - `withViewInit`: Custom view initialization closure. 76 | /// - `withLayoutSpec`: This closure is invoked whenever the layout is performed. 77 | /// Configure your backing view by using the *UILayout* object (e.g.): 78 | /// ``` 79 | /// ... { spec in 80 | /// spec.set(\UIView.backgroundColor, value: .green) 81 | /// spec.set(\UIView.layer.borderWidth, value: 1) 82 | /// ``` 83 | /// You can also access to the view directly (this is less performant because the infrastructure 84 | /// can't keep tracks of these view changes, but necessary when coping with more complex view 85 | /// configuration methods). 86 | /// ``` 87 | /// ... { spec in 88 | /// spec.view.backgroundColor = .green 89 | /// spec.view.setTitle("FOO", for: .normal) 90 | /// ``` 91 | /// - `withCoordinatorDescriptor:initialState.props`: Associates a coordinator to this node. 92 | /// - `build`: Builds the concrete node. 93 | public func Node( 94 | _ type: V.Type = V.self, 95 | @ContentBuilder builder: () -> ChildrenBuilder = ChildrenBuilder.default 96 | ) -> NodeBuilder { 97 | let children = builder().children.compactMap { $0 as? ConcreteNode } 98 | return NodeBuilder(type: type).withChildren(children) 99 | } 100 | 101 | @_functionBuilder 102 | public struct ContentBuilder { 103 | public static func buildBlock( 104 | _ nodes: OpaqueNodeBuilderConvertible... 105 | ) -> ChildrenBuilder { 106 | return ChildrenBuilder(children: nodes.map { $0.builder().build() }) 107 | } 108 | } 109 | 110 | /// Intermediate structure used as a return type from @_ContentBuilder. 111 | public struct ChildrenBuilder { 112 | /// Default (no children). 113 | public static let none = ChildrenBuilder(children: []) 114 | /// Returns an empty builder. 115 | public static let `default`: () -> ChildrenBuilder = { 116 | return ChildrenBuilder.none 117 | } 118 | /// The wrapped childrens. 119 | let children: [AnyNode] 120 | } 121 | 122 | // MARK: - Props 123 | 124 | public protocol AnyProp { 125 | /// Setup the coordinator with the given prop. 126 | func apply(coordinator: Coordinator) 127 | } 128 | 129 | /// Any custom-defined property in the coordinator, that is not internal state. 130 | public struct Prop: AnyProp { 131 | public typealias CoordinatorType = C 132 | public let keyPath: ReferenceWritableKeyPath 133 | public let value: V 134 | 135 | public init(_ keyPath: ReferenceWritableKeyPath, _ value: V) { 136 | self.keyPath = keyPath 137 | self.value = value 138 | } 139 | /// Setup the coordinator with the given prop. 140 | public func apply(coordinator: Coordinator) { 141 | guard let coordinator = coordinator as? C else { return } 142 | coordinator[keyPath: keyPath] = value 143 | } 144 | } 145 | 146 | /// Any custom-defined configuration closure for the coordinator. 147 | public struct BlockProp { 148 | public let block: (C) -> Void 149 | 150 | public init(_ block: @escaping (C) -> Void) { 151 | self.block = block 152 | } 153 | /// Setup the coordinator with the given prop. 154 | public func apply(coordinator: Coordinator) { 155 | guard let coordinator = coordinator as? C else { return } 156 | block(coordinator) 157 | } 158 | } 159 | 160 | // MARK: - Property setters 161 | 162 | /// Sets the value of a desired keypath using typesafe writable reference keypaths. 163 | /// - parameter spec: The *LayoutSpec* object that is currently handling the view configuration. 164 | /// - parameter keyPath: The target keypath. 165 | /// - parameter value: The new desired value. 166 | /// - parameter animator: Optional property animator for this change. 167 | public func withProperty( 168 | in spec: LayoutSpec, 169 | keyPath: ReferenceWritableKeyPath, 170 | value: T, 171 | animator: UIViewPropertyAnimator? = nil 172 | ) -> Void { 173 | guard let kvc = keyPath._kvcKeyPathString else { 174 | print("\(keyPath) is not a KVC property.") 175 | return 176 | } 177 | spec.set(kvc, value: value, animator: animator); 178 | } 179 | 180 | public func withProperty( 181 | in spec: LayoutSpec, 182 | keyPath: ReferenceWritableKeyPath, 183 | value: T, 184 | animator: UIViewPropertyAnimator? = nil 185 | ) -> Void { 186 | guard let kvc = keyPath._kvcKeyPathString else { 187 | print("\(keyPath) is not a KVC property.") 188 | return 189 | } 190 | let nsValue = NSNumber(value: value.rawValue) 191 | spec.set(kvc, value: nsValue, animator: animator) 192 | } 193 | 194 | // MARK: - Alias types 195 | 196 | // Drops the YG prefix. 197 | public typealias FlexDirection = YGFlexDirection 198 | public typealias Align = YGAlign 199 | public typealias Edge = YGEdge 200 | public typealias Wrap = YGWrap 201 | public typealias Display = YGDisplay 202 | public typealias Overflow = YGOverflow 203 | 204 | public typealias LayoutOptions = CRNodeLayoutOptions 205 | 206 | // Ensure that Yoga's C-enums are accessibly through KeyPathRefs. 207 | public protocol WritableKeyPathBoxableEnum { 208 | var rawValue: Int32 { get } 209 | } 210 | 211 | extension YGFlexDirection: WritableKeyPathBoxableEnum { } 212 | extension YGAlign: WritableKeyPathBoxableEnum { } 213 | extension YGEdge: WritableKeyPathBoxableEnum { } 214 | extension YGWrap: WritableKeyPathBoxableEnum { } 215 | extension YGDisplay: WritableKeyPathBoxableEnum { } 216 | extension YGOverflow: WritableKeyPathBoxableEnum { } 217 | -------------------------------------------------------------------------------- /Sources/CoreRender/CommonNodeTypes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import CoreRenderObjC 4 | 5 | // MARK: - Common Nodes 6 | 7 | public func ViewNode( 8 | @ContentBuilder builder: () -> ChildrenBuilder = ChildrenBuilder.default 9 | ) -> NodeBuilder { 10 | Node(UIView.self, builder: builder) 11 | } 12 | 13 | public func HStackNode ( 14 | @ContentBuilder builder: () -> ChildrenBuilder = ChildrenBuilder.default 15 | ) -> NodeBuilder { 16 | Node(UIView.self, builder: builder).withLayoutSpec { spec in 17 | guard let yoga = spec.view?.yoga else { return } 18 | yoga.flexDirection = .row 19 | yoga.justifyContent = .flexStart 20 | yoga.alignItems = .flexStart 21 | yoga.flex() 22 | } 23 | } 24 | 25 | public func VStackNode( 26 | @ContentBuilder builder: () -> ChildrenBuilder = ChildrenBuilder.default 27 | ) -> NodeBuilder { 28 | Node(UIView.self, builder: builder).withLayoutSpec { spec in 29 | guard let yoga = spec.view?.yoga else { return } 30 | yoga.flexDirection = .column 31 | yoga.justifyContent = .flexStart 32 | yoga.alignItems = .flexStart 33 | yoga.flex() 34 | } 35 | } 36 | 37 | public func LabelNode( 38 | text: String, 39 | @ContentBuilder builder: () -> ChildrenBuilder = ChildrenBuilder.default 40 | ) -> NodeBuilder { 41 | Node(UILabel.self, builder: builder).withLayoutSpec { spec in 42 | guard let view = spec.view else { return } 43 | view.text = text 44 | } 45 | } 46 | 47 | public func ButtonNode( 48 | key: String, 49 | @ContentBuilder builder: () -> ChildrenBuilder = ChildrenBuilder.default 50 | ) -> NodeBuilder { 51 | Node(UIButton.self, builder: builder).withKey(key) 52 | } 53 | 54 | public func EmptyNode() -> NullNodeBuilder { 55 | return NullNodeBuilder() 56 | } 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /Sources/CoreRender/Gestures.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import CoreRenderObjC 4 | 5 | // MARK: - NodeBuilder 6 | 7 | extension OpaqueNodeBuilder { 8 | public func onTouchUpInside(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 9 | withLayoutSpec { spec in spec.view?.onTouchUpInside(handler) } 10 | } 11 | 12 | public func onTouchDown(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 13 | withLayoutSpec { spec in spec.view?.onTouchDown(handler) } 14 | } 15 | 16 | public func onDoubleTap(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 17 | withLayoutSpec { spec in spec.view?.onDoubleTap(handler) } 18 | } 19 | 20 | public func onLongPress(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 21 | withLayoutSpec { spec in spec.view?.onLongPress(handler) } 22 | } 23 | 24 | public func onSwipeLeft(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 25 | withLayoutSpec { spec in spec.view?.onSwipeLeft(handler) } 26 | } 27 | 28 | public func onSwipeRight(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 29 | withLayoutSpec { spec in spec.view?.onSwipeRight(handler) } 30 | } 31 | 32 | public func onSwipeUp(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 33 | withLayoutSpec { spec in spec.view?.onSwipeUp(handler) } 34 | } 35 | 36 | public func onSwipeDown(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 37 | withLayoutSpec { spec in spec.view?.onSwipeDown(handler) } 38 | } 39 | 40 | public func onPan(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 41 | withLayoutSpec { spec in spec.view?.onPan(handler) } 42 | } 43 | 44 | public func onPinch(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 45 | withLayoutSpec { spec in spec.view?.onPinch(handler) } 46 | } 47 | 48 | public func onRotate(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 49 | withLayoutSpec { spec in spec.view?.onRotate(handler) } 50 | } 51 | 52 | public func onScreenEdgePan(_ handler: @escaping (UIGestureRecognizer) -> Void) -> Self { 53 | withLayoutSpec { spec in spec.view?.onScreenEdgePan(handler) } 54 | } 55 | } 56 | 57 | // MARK: - GestureRecognizer 58 | 59 | extension UIView { 60 | /// All of the gesture recognizers registered through the closure based api. 61 | var gestureRecognizerProxyDictionary: NSMutableDictionary { 62 | get { 63 | if let obj = objc_getAssociatedObject(self, &__handler) as? NSMutableDictionary { 64 | return obj 65 | } 66 | let obj = NSMutableDictionary() 67 | objc_setAssociatedObject(self, &__handler, obj, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 68 | return obj 69 | } 70 | set { 71 | objc_setAssociatedObject(self, &__handler, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 72 | } 73 | } 74 | 75 | /// Flush all of the existing gesture recognizers registered through the closure based api. 76 | public func flushGestureRecognizers() { 77 | guard let array = gestureRecognizerProxyDictionary.allValues as? [WeakGestureRecognizer] else { 78 | return 79 | } 80 | for obj in array { 81 | obj.handler = nil 82 | if let gesture = obj.object { 83 | gesture.removeTarget(nil, action: nil) 84 | gesture.view?.removeGestureRecognizer(gesture) 85 | } 86 | obj.object = nil 87 | } 88 | gestureRecognizerProxyDictionary = NSMutableDictionary() 89 | } 90 | 91 | /// Flush all of the existing gesture recognizers registered through the closure based api. 92 | public func flushGestureRecognizersRecursively() { 93 | flushGestureRecognizers() 94 | for subview in subviews { 95 | subview.flushGestureRecognizersRecursively() 96 | } 97 | } 98 | } 99 | 100 | extension UIView { 101 | func onGestureRecognizer( 102 | type: T.Type, 103 | key: NSString, 104 | numberOfTapsRequired: Int = 1, 105 | numberOfTouchesRequired: Int = 1, 106 | direction: UISwipeGestureRecognizer.Direction = UISwipeGestureRecognizer.Direction.down, 107 | _ handler: @escaping (UIGestureRecognizer) -> Void 108 | ) -> Void { 109 | let wrapper = WeakGestureRecognizer() 110 | wrapper.handler = handler 111 | let selector = #selector(WeakGestureRecognizer.handle(sender:)) 112 | let gesture = T(target: wrapper, action: selector) 113 | wrapper.object = gesture 114 | if let tapGesture = gesture as? UITapGestureRecognizer { 115 | tapGesture.numberOfTapsRequired = numberOfTapsRequired 116 | tapGesture.numberOfTouchesRequired = numberOfTouchesRequired 117 | } 118 | if let swipeGesture = gesture as? UISwipeGestureRecognizer { 119 | swipeGesture.direction = direction 120 | } 121 | // Safely remove the old gesture recognizer. 122 | if let old = gestureRecognizerProxyDictionary.object(forKey: key) as? WeakGestureRecognizer, 123 | let oldGesture = old.object { 124 | old.handler = nil 125 | old.object = nil 126 | oldGesture.removeTarget(nil, action: nil) 127 | oldGesture.view?.removeGestureRecognizer(oldGesture) 128 | } 129 | gestureRecognizerProxyDictionary.setObject(wrapper, forKey: key) 130 | addGestureRecognizer(gesture) 131 | } 132 | 133 | public func onTouchUpInside(_ handler: @escaping (UIGestureRecognizer) -> Void) { 134 | onGestureRecognizer( 135 | type: UITapGestureRecognizer.self, 136 | key: "\(#function)" as NSString, 137 | handler) 138 | } 139 | 140 | public func onTouchDown(_ handler: @escaping (UIGestureRecognizer) -> Void) { 141 | onGestureRecognizer( 142 | type: TouchDownGestureRecognizer.self, 143 | key: "\(#function)" as NSString, 144 | handler) 145 | } 146 | 147 | public func onDoubleTap(_ handler: @escaping (UIGestureRecognizer) -> Void) { 148 | onGestureRecognizer( 149 | type: UITapGestureRecognizer.self, 150 | key: "\(#function)" as NSString, 151 | numberOfTapsRequired: 2, 152 | handler) 153 | } 154 | 155 | public func onLongPress(_ handler: @escaping (UIGestureRecognizer) -> Void) { 156 | onGestureRecognizer( 157 | type: UILongPressGestureRecognizer.self, 158 | key: "\(#function)" as NSString, 159 | handler) 160 | } 161 | 162 | public func onSwipeLeft(_ handler: @escaping (UIGestureRecognizer) -> Void) { 163 | onGestureRecognizer( 164 | type: UISwipeGestureRecognizer.self, 165 | key: "\(#function)" as NSString, 166 | direction: UISwipeGestureRecognizer.Direction.left, 167 | handler) 168 | } 169 | 170 | public func onSwipeRight(_ handler: @escaping (UIGestureRecognizer) -> Void) { 171 | onGestureRecognizer( 172 | type: UISwipeGestureRecognizer.self, 173 | key: "\(#function)" as NSString, 174 | direction: UISwipeGestureRecognizer.Direction.right, 175 | handler) 176 | } 177 | 178 | public func onSwipeUp(_ handler: @escaping (UIGestureRecognizer) -> Void) { 179 | onGestureRecognizer( 180 | type: UISwipeGestureRecognizer.self, 181 | key: "\(#function)" as NSString, 182 | direction: UISwipeGestureRecognizer.Direction.up, 183 | handler) 184 | } 185 | 186 | public func onSwipeDown(_ handler: @escaping (UIGestureRecognizer) -> Void) { 187 | onGestureRecognizer( 188 | type: UISwipeGestureRecognizer.self, 189 | key: "\(#function)" as NSString, 190 | direction: UISwipeGestureRecognizer.Direction.down, 191 | handler) 192 | } 193 | 194 | public func onPan(_ handler: @escaping (UIGestureRecognizer) -> Void) { 195 | onGestureRecognizer( 196 | type: UIPanGestureRecognizer.self, 197 | key: "\(#function)" as NSString, 198 | handler) 199 | } 200 | 201 | public func onPinch(_ handler: @escaping (UIGestureRecognizer) -> Void) { 202 | onGestureRecognizer( 203 | type: UIPinchGestureRecognizer.self, 204 | key: "\(#function)" as NSString, 205 | handler) 206 | } 207 | 208 | public func onRotate(_ handler: @escaping (UIGestureRecognizer) -> Void) { 209 | onGestureRecognizer( 210 | type: UIRotationGestureRecognizer.self, 211 | key: "\(#function)" as NSString, 212 | handler) 213 | } 214 | 215 | public func onScreenEdgePan(_ handler: @escaping (UIGestureRecognizer) -> Void) { 216 | onGestureRecognizer( 217 | type: UIScreenEdgePanGestureRecognizer.self, 218 | key: "\(#function)" as NSString, 219 | handler) 220 | } 221 | } 222 | 223 | // MARK: - Private 224 | 225 | fileprivate class WeakGestureRecognizer: NSObject { 226 | weak var object: UIGestureRecognizer? 227 | var handler: ((UIGestureRecognizer) -> Void)? = nil 228 | 229 | @objc func handle(sender: UIGestureRecognizer) { 230 | handler?(sender) 231 | } 232 | } 233 | 234 | fileprivate var __handler: UInt8 = 0 235 | 236 | class TouchDownGestureRecognizer: UIGestureRecognizer { 237 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 238 | if self.state == .possible { 239 | self.state = .recognized 240 | } 241 | } 242 | override func touchesMoved(_ touches: Set, with event: UIEvent) { 243 | self.state = .failed 244 | } 245 | override func touchesEnded(_ touches: Set, with event: UIEvent) { 246 | self.state = .failed 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRContext.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | NS_ASSUME_NONNULL_BEGIN 5 | 6 | @class CRCoordinator; 7 | @class CRNode; 8 | @class CRContext; 9 | ; 10 | @class CRContextReconciliationInfo; 11 | 12 | NS_SWIFT_NAME(CoordinatorDescriptor) 13 | @interface CRCoordinatorDescriptor : NSObject 14 | /// The coordinator type. 15 | @property(nonatomic, readonly) Class type; 16 | /// The coordinator unique key. 17 | @property(nonatomic, readonly) NSString *key; 18 | 19 | - (instancetype)init NS_UNAVAILABLE; 20 | /// Constructs a new coordinator descriptor. 21 | - (instancetype)initWithType:(Class)type key:(NSString *)key; 22 | @end 23 | 24 | NS_SWIFT_NAME(ContextDelegate) 25 | @protocol CRContextDelegate 26 | /// One of the coordinator is about to invoke @c setNeedReconciliate on the root node. 27 | - (void)context:(CRContext *)context willReconciliateHieararchy:(CRContextReconciliationInfo *)info; 28 | /// Node/View hierarchy reconciliation has just occurred. 29 | - (void)context:(CRContext *)context didReconciliateHieararchy:(CRContextReconciliationInfo *)info; 30 | @end 31 | 32 | NS_SWIFT_NAME(Context) 33 | @interface CRContext : NSObject 34 | /// Layout animator for the nodes registered to this context. 35 | @property(nonatomic, nullable) UIViewPropertyAnimator *layoutAnimator; 36 | 37 | /// Returns the coordinator (or instantiate a new one) of type @c type for the unique identifier 38 | /// passed as argument. 39 | /// @note: Returns @c nil if @c type is not a subclass of @c CRCoordinator (or if it's a statelss 40 | /// coordinator). 41 | - (__kindof CRCoordinator *)coordinator:(CRCoordinatorDescriptor *)descriptor; 42 | 43 | /// Add the object as delegate for this context. 44 | - (void)addDelegate:(id)delegate; 45 | 46 | /// Remove the object as delegate (if necessary). 47 | - (void)removeDelegate:(id)delegate; 48 | 49 | @end 50 | 51 | NS_SWIFT_NAME(ContextReconciliationInfo) 52 | @interface CRContextReconciliationInfo : NSObject 53 | /// Explictly inform the delegate that if the nodes are wrapped inside a @c UITableView or 54 | /// a @c UICollectionView, this must be invalidated and its data reloaded. 55 | @property(nonatomic, readonly) BOOL mustInvalidateLayout; 56 | /// The keys of all of the nodes that have had a rect change during the last reconciliation. 57 | @property(nonatomic, readonly) NSArray *keysForNodesWithMutatedSize; 58 | /// Layout animator that is going to be used for the upcoming reconciliation. 59 | @property(nonatomic, readonly, nullable) UIViewPropertyAnimator *layoutAnimator; 60 | 61 | @end 62 | 63 | NS_ASSUME_NONNULL_END 64 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRContext.mm: -------------------------------------------------------------------------------- 1 | #import "CRContext.h" 2 | #import "CRCoordinator+Private.h" 3 | #import "CRMacros.h" 4 | #import "CRNode.h" 5 | 6 | #pragma mark - CRCoordinatorDescriptor 7 | 8 | @implementation CRCoordinatorDescriptor 9 | 10 | - (instancetype)initWithType:(Class)type key:(NSString *)key { 11 | if (self = [super init]) { 12 | _type = type; 13 | _key = key; 14 | } 15 | return self; 16 | } 17 | 18 | - (BOOL)isEqual:(id)object { 19 | if (object == nil) return; 20 | if (![object isKindOfClass:CRCoordinatorDescriptor.class]) return; 21 | const auto rhs = CR_DYNAMIC_CAST(CRCoordinatorDescriptor, object); 22 | return [rhs.type isEqual:self.type] && [rhs.key isEqualToString:self.key]; 23 | } 24 | 25 | @end 26 | 27 | #pragma mark - CRContext 28 | 29 | @implementation CRContext { 30 | NSMutableDictionary *> 31 | *_coordinators; 32 | NSPointerArray *_delegates; 33 | } 34 | 35 | - (instancetype)init { 36 | if (self = [super init]) { 37 | _coordinators = @{}.mutableCopy; 38 | _delegates = [NSPointerArray weakObjectsPointerArray]; 39 | } 40 | return self; 41 | } 42 | 43 | - (__kindof CRCoordinator *)coordinator:(CRCoordinatorDescriptor *)desc { 44 | CR_ASSERT_ON_MAIN_THREAD(); 45 | if (![desc.type isSubclassOfClass:CRCoordinator.self]) return nil; 46 | const auto container = [self _containerForType:desc.type]; 47 | if (const auto coordinator = container[desc.key]) { 48 | return coordinator; 49 | } 50 | const auto coordinator = CR_DYNAMIC_CAST(CRCoordinator, [[desc.type alloc] initWithKey:desc.key]); 51 | coordinator.key = desc.key; 52 | coordinator.context = self; 53 | container[desc.key] = coordinator; 54 | return coordinator; 55 | } 56 | 57 | - (NSMutableDictionary *)_containerForType:(Class)type { 58 | const auto str = NSStringFromClass(type); 59 | if (const auto container = _coordinators[str]) return container; 60 | const auto container = [[NSMutableDictionary alloc] init]; 61 | _coordinators[str] = container; 62 | return container; 63 | } 64 | 65 | - (void)addDelegate:(id)delegate { 66 | CR_ASSERT_ON_MAIN_THREAD(); 67 | [_delegates compact]; 68 | for (NSUInteger i = 0; i < _delegates.count; i++) 69 | if ([_delegates pointerAtIndex:i] == (__bridge void *)(delegate)) return; 70 | [_delegates addPointer:(__bridge void *)delegate]; 71 | } 72 | 73 | - (void)removeDelegate:(id)delegate { 74 | CR_ASSERT_ON_MAIN_THREAD(); 75 | [_delegates compact]; 76 | NSUInteger removeIdx = NSNotFound; 77 | for (NSUInteger i = 0; i < _delegates.count; i++) 78 | if ([_delegates pointerAtIndex:i] == (__bridge void *)(delegate)) { 79 | removeIdx = i; 80 | break; 81 | } 82 | if (removeIdx != NSNotFound) [_delegates removePointerAtIndex:removeIdx]; 83 | } 84 | 85 | @end 86 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRCoordinator+Private.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "CRCoordinator.h" 5 | 6 | NS_ASSUME_NONNULL_BEGIN 7 | 8 | @class CRNode; 9 | @class CRContext; 10 | 11 | @interface CRCoordinator () 12 | // Private setter modifiers 13 | @property(nonatomic, readwrite) NSString *key; 14 | @property(nonatomic, readwrite, nullable, weak) CRContext *context; 15 | @property(nonatomic, readwrite, nullable, weak) CRNode *node; 16 | 17 | /// @note: Never call the init method manually - coordinators are dynamically constructed, 18 | /// disposed and reused by @c CRContext. 19 | - (instancetype)initWithKey:(NSString *)key; 20 | 21 | @end 22 | 23 | NS_ASSUME_NONNULL_END 24 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRCoordinator.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | NS_ASSUME_NONNULL_BEGIN 5 | 6 | @class CRContext; 7 | @class CRNode; 8 | @class CRNodeHierarchy; 9 | @class CRCoordinatorDescriptor; 10 | @protocol CRNodeDelegate; 11 | 12 | NS_SWIFT_NAME(Coordinator) 13 | @interface CRCoordinator : NSObject 14 | /// The context associated with this coordinator. 15 | @property(nonatomic, readonly, nullable, weak) CRContext *context; 16 | /// The key for this coordinator. 17 | /// If this coordinator is @c transient the value of this property is @c CRCoordinatorStatelessKey. 18 | @property(nonatomic, readonly) NSString *key; 19 | /// The UI node assigned to this coordinator. 20 | @property(nonatomic, readonly, nullable, weak) CRNodeHierarchy *body; 21 | /// The UI node assigned to this coordinator. 22 | @property(nonatomic, readonly, nullable, weak) CRNode *node; 23 | /// Returns the coordinator descriptor. 24 | @property(nonatomic, readonly) CRCoordinatorDescriptor *prototype; 25 | 26 | /// Coordinators are instantiated from @c CRContext. 27 | - (instancetype)init; 28 | 29 | /// Constructs a new node hierarchy and reconciles it against the currently mounted view hierarchy. 30 | - (void)setNeedsReconcile; 31 | 32 | /// Tells the already mounted hierarchy must be re-layout. 33 | /// @note This is preferable to @c setNeedsReconcile whenever there's going to be no changes in 34 | /// the view hierarchy, 35 | - (void)setNeedsLayout; 36 | 37 | /// Overrides this method to manually configure the view hierarchy after it has been layed out. 38 | - (void)onLayout; 39 | 40 | /// Returns the view in the subtree of this node with the given @c key. 41 | - (nullable UIView *)viewWithKey:(NSString *)key; 42 | 43 | /// Returns all the views that have been registered with the given @c reuseIdentifier. 44 | - (NSArray *)viewsWithReuseIdentifier:(NSString *)reuseIdentifier; 45 | 46 | @end 47 | 48 | NS_ASSUME_NONNULL_END 49 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRCoordinator.mm: -------------------------------------------------------------------------------- 1 | #import "CRContext.h" 2 | 3 | #import "CRCoordinator+Private.h" 4 | #import "CRMacros.h" 5 | #import "CRNode.h" 6 | #import "CRNodeHierarchy.h" 7 | 8 | NSString *CRIllegalCoordinatorTypeExceptionName = @"IllegalCoordinatorType"; 9 | 10 | #pragma mark - Coordinator 11 | 12 | @implementation CRCoordinator 13 | 14 | - (instancetype)init { 15 | if (self = [super init]) { 16 | } 17 | return self; 18 | } 19 | 20 | - (CRNodeHierarchy *)body { 21 | return _node.nodeHierarchy; 22 | } 23 | 24 | - (CRCoordinatorDescriptor *)prototype { 25 | return [[CRCoordinatorDescriptor alloc] initWithType:self.class key:self.key]; 26 | } 27 | 28 | // Private constructor. 29 | - (instancetype)initWithKey:(NSString *)key { 30 | if (self = [super init]) { 31 | _key = key; 32 | } 33 | return self; 34 | } 35 | 36 | - (void)setNeedsReconcile { 37 | CR_ASSERT_ON_MAIN_THREAD(); 38 | [self.body setNeedsReconcile]; 39 | } 40 | 41 | - (void)setNeedsLayout { 42 | CR_ASSERT_ON_MAIN_THREAD(); 43 | [self.body setNeedsLayout]; 44 | } 45 | 46 | - (void)onLayout { 47 | CR_ASSERT_ON_MAIN_THREAD(); 48 | } 49 | 50 | - (UIView *)viewWithKey:(NSString *)key { 51 | return [self.body.root viewWithKey:key]; 52 | } 53 | 54 | - (NSArray *)viewsWithReuseIdentifier:(NSString *)reuseIdentifier { 55 | return [self.body.root viewsWithReuseIdentifier:reuseIdentifier]; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRHostingView.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "CRNode.h" 5 | 6 | NS_ASSUME_NONNULL_BEGIN 7 | 8 | @class CRContext; 9 | @class CRNodeHierarchy; 10 | @class CROpaqueNodeBuilder; 11 | 12 | NS_SWIFT_NAME(HostingView) 13 | @interface CRHostingView : UIView 14 | /// The exposed node hierarchy. 15 | @property(nonatomic, readonly) CRNodeHierarchy *body; 16 | 17 | - (instancetype)init NS_UNAVAILABLE; 18 | - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; 19 | - (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; 20 | 21 | /// Construct a new hosting view with the given reference context. 22 | - (instancetype)initWithContext:(CRContext *)context 23 | withOptions:(CRNodeLayoutOptions)options 24 | body:(CROpaqueNodeBuilder * (^)(CRContext *))buildBody 25 | NS_DESIGNATED_INITIALIZER; 26 | 27 | /// Tells the node that the node/view hierarchy must be reconciled. 28 | - (void)setNeedsReconcile; 29 | 30 | /// Tells the node that the node/view hierarchy must be re-layout. 31 | /// @note This is preferable to @c setNeedsReconcile whenever there's going to be no changes in 32 | /// the view hierarchy, 33 | - (void)setNeedsLayout; 34 | 35 | @end 36 | 37 | NS_ASSUME_NONNULL_END 38 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRHostingView.mm: -------------------------------------------------------------------------------- 1 | #include "CRHostingView.h" 2 | 3 | #include "CRContext.h" 4 | #include "CRMacros.h" 5 | #include "CRNode.h" 6 | #include "CRNodeBuilder.h" 7 | #include "CRNodeHierarchy.h" 8 | 9 | @implementation CRHostingView { 10 | __weak CRContext *_context; 11 | CRNodeLayoutOptions _options; 12 | CRNodeHierarchy *_body; 13 | } 14 | 15 | - (instancetype)initWithContext:(CRContext *)context 16 | withOptions:(CRNodeLayoutOptions)options 17 | body:(CROpaqueNodeBuilder * (^)(CRContext *))buildBody { 18 | if (self = [super initWithFrame:CGRectZero]) { 19 | if (@available(iOS 13, *)) { 20 | self.backgroundColor = [UIColor systemBackgroundColor]; 21 | } else { 22 | self.backgroundColor = [UIColor whiteColor]; 23 | } 24 | _context = context; 25 | _options = options; 26 | _body = [[CRNodeHierarchy alloc] initWithContext:context nodeHierarchyBuilder:buildBody]; 27 | [_body buildHierarchyInView:self constrainedToSize:self.bounds.size withOptions:options]; 28 | } 29 | return self; 30 | } 31 | 32 | - (void)setNeedsReconcile { 33 | CR_ASSERT_ON_MAIN_THREAD(); 34 | [_body reconcileInView:self constrainedToSize:self.bounds.size withOptions:_options]; 35 | } 36 | 37 | - (void)setNeedsLayout { 38 | CR_ASSERT_ON_MAIN_THREAD(); 39 | [_body layoutConstrainedToSize:self.bounds.size withOptions:_options]; 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRMacros.h: -------------------------------------------------------------------------------- 1 | #ifndef CRInternalMacros_h 2 | #define CRInternalMacros_h 3 | 4 | #import 5 | #import 6 | 7 | #pragma mark - Type inference and dynamic casts 8 | 9 | // Type inference for local variables. 10 | #if defined(__cplusplus) 11 | #else 12 | #define auto __auto_type 13 | #endif 14 | 15 | // Equivalent to swift nil coalescing operator '??'. 16 | #if defined(__cplusplus) 17 | template 18 | static inline T *_Nonnull CRNilCoalescing(T *_Nullable value, T *_Nonnull defaultValue) { 19 | return value != nil ? value : defaultValue; 20 | } 21 | #define CR_NIL_COALESCING(VALUE, DEFAULT) CRNilCoalescing(VALUE, DEFAULT) 22 | #else 23 | #define CR_NIL_COALESCING(VALUE, DEFAULT) (VALUE != nil ? VALUE : DEFAULT) 24 | #endif 25 | 26 | /// Mirrors Swift's 'as?' operator. 27 | #if defined(__cplusplus) 28 | template 29 | static inline T *_Nullable CRDynamicCast(__unsafe_unretained id _Nullable obj, 30 | bool assert = false) { 31 | if ([(id)obj isKindOfClass:[T class]]) { 32 | return obj; 33 | } 34 | return nil; 35 | } 36 | template 37 | static inline T *_Nonnull CRDynamicCastOrAssert(__unsafe_unretained id _Nullable obj) { 38 | return (T * _Nonnull) CRDynamicCast(obj, true); 39 | } 40 | #define CR_DYNAMIC_CAST(TYPE, VALUE) CRDynamicCast(VALUE) 41 | #define CR_DYNAMIC_CAST_OR_ASSERT(TYPE, VALUE) CRDynamicCastOrAssert(VALUE) 42 | #else 43 | static inline id CRDynamicCast(__unsafe_unretained id obj, Class type, BOOL assert) { 44 | if ([(id)obj isKindOfClass:type]) { 45 | return obj; 46 | } 47 | if (assert) { 48 | NSCAssert(NO, @"failed to cast %@ to %@", obj, type); 49 | } 50 | return nil; 51 | } 52 | #define CR_DYNAMIC_CAST(TYPE, VALUE) ((TYPE * _Nullable) CRDynamicCast(VALUE, TYPE.class, NO)) 53 | #define CR_DYNAMIC_CAST_OR_ASSERT(TYPE, VALUE) \ 54 | ((TYPE * _Nonnull) CRDynamicCast(VALUE, TYPE.class, true)) 55 | #endif 56 | 57 | #pragma mark - Weakify 58 | 59 | #define CR_WEAKNAME_(VAR) VAR##_weak_ 60 | 61 | #define CR_WEAKIFY(VAR) __weak __typeof__(VAR) CR_WEAKNAME_(VAR) = (VAR) 62 | 63 | #define CR_STRONGIFY(VAR) \ 64 | _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wshadow\"") \ 65 | __strong __typeof__(VAR) VAR = CR_WEAKNAME_(VAR); \ 66 | _Pragma("clang diagnostic pop") 67 | 68 | #define CR_STRONGIFY_AND_RETURN_IF_NIL(VAR) \ 69 | CR_STRONGIFY(VAR); \ 70 | if (!(VAR)) { \ 71 | return; \ 72 | } 73 | 74 | // Safe keypath litterals. 75 | #define CR_UNSAFE_KEYPATH(p) @ #p 76 | 77 | #if DEBUG 78 | #define CR_KEYPATH(o, p) ((void)(NO && ((void)o.p, NO)), @ #p) 79 | #else 80 | #define CR_KEYPATH(o, p) @ #p 81 | #endif 82 | 83 | #pragma mark - Misc 84 | 85 | // Equivalent to Swift's @noescape. 86 | #define CR_NOESCAPE __attribute__((noescape)) 87 | 88 | // Ensure the caller method is being invoked on the main thread. 89 | #define CR_ASSERT_ON_MAIN_THREAD() NSAssert(NSThread.isMainThread, @"called off the main thread.") 90 | 91 | #pragma mark - Geometry 92 | 93 | #define CR_CLAMP(x, low, high) \ 94 | ({ \ 95 | __typeof__(x) __x = (x); \ 96 | __typeof__(low) __low = (low); \ 97 | __typeof__(high) __high = (high); \ 98 | __x > __high ? __high : (__x < __low ? __low : __x); \ 99 | }) 100 | 101 | #define CR_CGFLOAT_MAX 32768 102 | #define CR_CGFLOAT_UNDEFINED YGUndefined 103 | #define CR_CGFLOAT_FLEXIBLE CR_CGFLOAT_MAX 104 | #define CR_NORMALIZE(value) (value >= 0.0 && value <= CR_CGFLOAT_MAX ? value : 0.0) 105 | 106 | #pragma mark - Boxable structs 107 | 108 | // Ensure the struct can be boxed in a NSValue by using the @ symbol. 109 | #define CR_OBJC_BOXABLE __attribute__((objc_boxable)) 110 | 111 | typedef struct CR_OBJC_BOXABLE CGPoint CGPoint; 112 | typedef struct CR_OBJC_BOXABLE CGSize CGSize; 113 | typedef struct CR_OBJC_BOXABLE CGRect CGRect; 114 | typedef struct CR_OBJC_BOXABLE CGVector CGVector; 115 | typedef struct CR_OBJC_BOXABLE UIEdgeInsets UIEdgeInsets; 116 | typedef struct CR_OBJC_BOXABLE _NSRange NSRange; 117 | typedef struct CR_OBJC_BOXABLE CGAffineTransform CGAffineTransform; 118 | 119 | #pragma mark - Generics 120 | 121 | NS_ASSUME_NONNULL_BEGIN 122 | 123 | @protocol CRFastEnumeration 124 | - (id)CR_enumeratedType; 125 | @end 126 | 127 | // Usage: CR_FOREACH (s, strings) { ... } 128 | // For each loops using type inference. 129 | #define CR_FOREACH(element, collection) \ 130 | for (typeof((collection).CR_enumeratedType) element in (collection)) 131 | 132 | @interface NSArray (CRFastEnumeration) 133 | - (ElementType)CR_enumeratedType; 134 | @end 135 | 136 | @interface NSSet (CRFastEnumeration) 137 | - (ElementType)CR_enumeratedType; 138 | @end 139 | 140 | @interface NSDictionary (CRFastEnumeration) 141 | - (KeyType)CR_enumeratedType; 142 | @end 143 | 144 | /// This overrides the NSObject declaration of copy with specialized ones that retain 145 | // the generic type. 146 | // This is pure compiler sugar and will create additional warnings for type mismatches. 147 | // @note id-casted objects will create a warning when copy is called on them as there are multiple 148 | // declarations available. Either cast to specific type or to NSObject to work around this. 149 | @interface NSArray (CRSafeCopy) 150 | // Same as `copy` but retains the generic type. 151 | - (NSArray *)copy; 152 | // Same as `mutableCopy` but retains the generic type. 153 | - (NSMutableArray *)mutableCopy; 154 | @end 155 | 156 | @interface NSSet (CRSafeCopy) 157 | // Same as `copy` but retains the generic type. 158 | - (NSSet *)copy; 159 | // Same as `mutableCopy` but retains the generic type. 160 | - (NSMutableSet *)mutableCopy; 161 | @end 162 | 163 | @interface NSDictionary (CRSafeCopy) 164 | // Same as `copy` but retains the generic type. 165 | - (NSDictionary *)copy; 166 | // Same as `mutableCopy` but retains the generic type. 167 | - (NSMutableDictionary *)mutableCopy; 168 | @end 169 | 170 | @interface NSOrderedSet (CRSafeCopy) 171 | // Same as `copy` but retains the generic type. 172 | - (NSOrderedSet *)copy; 173 | // Same as `mutableCopy` but retains the generic type. 174 | - (NSMutableOrderedSet *)mutableCopy; 175 | @end 176 | 177 | @interface NSHashTable (CRSafeCopy) 178 | // Same as `copy` but retains the generic type. 179 | - (NSHashTable *)copy; 180 | @end 181 | 182 | @interface NSMapTable (CRSafeCopy) 183 | // Same as `copy` but retains the generic type. 184 | - (NSMapTable *)copy; 185 | @end 186 | 187 | NS_ASSUME_NONNULL_END 188 | 189 | #pragma mark - NSArray to std::vector and viceversa 190 | 191 | #if defined(__cplusplus) 192 | #include 193 | 194 | NS_ASSUME_NONNULL_BEGIN 195 | 196 | template 197 | static inline NSArray *CRArrayWithVector(const std::vector &vector, 198 | id (^block)(const T &value)) { 199 | NSMutableArray *result = [NSMutableArray arrayWithCapacity:vector.size()]; 200 | for (const T &value : vector) { 201 | [result addObject:block(value)]; 202 | } 203 | 204 | return result; 205 | } 206 | 207 | template 208 | static inline std::vector CRVectorWithElements(id array, 209 | T (^_Nullable block)(id value)) { 210 | std::vector result; 211 | for (id value in array) { 212 | result.push_back(block(value)); 213 | } 214 | return result; 215 | } 216 | 217 | NS_ASSUME_NONNULL_END 218 | 219 | #endif 220 | 221 | #pragma mark - Logging 222 | 223 | #define CR_NOT_REACHED() CR_LOG(@"Unexpected exec @ %s:%s ", __FILE__, __LINE__) 224 | 225 | #define CR_LOG_ENABLED 1 226 | 227 | #ifdef CR_LOG_ENABLED 228 | #define CR_LOG(fmt, ...) NSLog([NSString stringWithFormat:@"(client) %@", fmt], ##__VA_ARGS__) 229 | #else 230 | #define CR_LOG(...) 231 | #endif 232 | 233 | #endif /* CRMaCRos_h */ 234 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNode.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | NS_ASSUME_NONNULL_BEGIN 5 | 6 | NS_SWIFT_NAME(AnyNode) 7 | @interface CRAnyNode : NSObject 8 | @end 9 | 10 | NS_SWIFT_NAME(NodeLayoutOptions) 11 | typedef NS_OPTIONS(NSUInteger, CRNodeLayoutOptions) { 12 | CRNodeLayoutOptionsNone = 1 << 0, 13 | CRNodeLayoutOptionsSizeContainerViewToFit = 1 << 1, 14 | CRNodeLayoutOptionsUseSafeAreaInsets = 1 << 2 15 | }; 16 | 17 | @class CRNode; 18 | @class CRNodeHierarchy; 19 | @class CRContext; 20 | @class CRCoordinator; 21 | @class CRCoordinatorDescriptor; 22 | @class CRNodeLayoutSpec<__covariant V : UIView *>; 23 | 24 | NS_SWIFT_NAME(NodeDelegate) 25 | @protocol CRNodeDelegate 26 | @optional 27 | /// The root node for this hierarchy is being configured and layed out. 28 | /// Additional custom manual layout can be defined here. 29 | /// @note: Use @viewWithKey or @viewsWithReuseIdentifier to query the desired views in the 30 | /// installed view hierarchy. 31 | - (void)rootNodeDidLayout:(CRNode *)node; 32 | /// The node @renderedView just got inserted in the view hierarchy. 33 | - (void)rootNodeDidMount:(CRNode *)node; 34 | @end 35 | 36 | NS_SWIFT_NAME(ConcreteNode) 37 | @interface CRNode<__covariant V : UIView *> : CRAnyNode 38 | /// The context associated with this node hierarchy. 39 | @property(nonatomic, readonly, nullable, weak) CRContext *context; 40 | /// The node hierarchy this node belongs to (if applicable). 41 | @property(nonatomic, nullable, weak) CRNodeHierarchy *nodeHierarchy; 42 | /// The reuse identifier for this node is its hierarchy. 43 | /// Identifiers help Render understand which items have changed. 44 | /// A custom *reuseIdentifier* is mandatory if the node has a custom creation closure. 45 | @property(nonatomic, readonly) NSString *reuseIdentifier; 46 | /// A unique key for the component/node (necessary if the associated coordinator is stateful). 47 | @property(nonatomic, readonly, nullable) NSString *key; 48 | /// This component is the n-th children. 49 | @property(nonatomic, readonly) NSUInteger index; 50 | /// The subnodes of this node. 51 | @property(nonatomic, readonly) NSArray *children; 52 | /// The parent node (if this is not the root node in the hierarchy). 53 | @property(nonatomic, readonly, nullable, weak) CRNode *parent; 54 | /// Returns the root node for this node hierarchy. 55 | @property(nonatomic, readonly) CRNode *root; 56 | /// The type of the associated backing view. 57 | @property(nonatomic, readonly) Class viewType; 58 | /// Backing view for this node. 59 | @property(nonatomic, readonly, nullable) V renderedView; 60 | /// The layout delegate for this node. 61 | @property(nonatomic, nullable, weak) id delegate; 62 | /// Whether this node is a @c CRNullNode or not. 63 | @property(nonatomic, readonly) BOOL isNullNode; 64 | /// Returns the associated coordinator. 65 | /// @note: @c nil if this node hierarchy is not registered to any @c CRContext, or if 66 | /// @c coordinatorType is @c nil. 67 | @property(nonatomic, nullable, readonly) __kindof CRCoordinator *coordinator; 68 | /// The type of the associated coordinator. 69 | @property(nonatomic, nullable, readonly) CRCoordinatorDescriptor *coordinatorDescriptor; 70 | 71 | #pragma mark Constructors 72 | 73 | - (instancetype)initWithType:(Class)type 74 | reuseIdentifier:(nullable NSString *)reuseIdentifier 75 | key:(nullable NSString *)key 76 | viewInit:(UIView * (^_Nullable)(void))viewInit 77 | layoutSpec:(void (^)(CRNodeLayoutSpec *))layoutSpec; 78 | 79 | + (instancetype)nodeWithType:(Class)type 80 | reuseIdentifier:(nullable NSString *)reuseIdentifier 81 | key:(nullable NSString *)key 82 | viewInit:(UIView * (^_Nullable)(void))viewInit 83 | layoutSpec:(void (^)(CRNodeLayoutSpec *))layoutSpec; 84 | 85 | + (instancetype)nodeWithType:(Class)type layoutSpec:(void (^)(CRNodeLayoutSpec *))layoutSpec; 86 | 87 | #pragma mark Setup 88 | 89 | /// Adds the nodes as children of this node. 90 | - (instancetype)appendChildren:(NSArray *)children; 91 | 92 | /// Bind this node to the @c CRCoordinator class passed as argument. 93 | - (instancetype)bindCoordinator:(CRCoordinatorDescriptor *)descriptor; 94 | 95 | /// Register the context for the root node of this node hierarchy. 96 | - (void)registerNodeHierarchyInContext:(CRContext *)context; 97 | 98 | #pragma mark Render 99 | 100 | /// Reconcile the view hierarchy with the one in the container view passed as argument. 101 | /// @note: This method also performs layout and configuration. 102 | - (void)reconcileInView:(nullable UIView *)view 103 | constrainedToSize:(CGSize)size 104 | withOptions:(CRNodeLayoutOptions)options; 105 | 106 | /// Layout and configure the views. 107 | - (void)layoutConstrainedToSize:(CGSize)size withOptions:(CRNodeLayoutOptions)options; 108 | 109 | /// Re-configure the node's backed view. 110 | /// @note This won't invalidate the layout. 111 | - (void)setNeedsConfigure; 112 | 113 | #pragma mark Querying 114 | 115 | /// Returns the view in the subtree of this node with the given @c key. 116 | - (nullable UIView *)viewWithKey:(NSString *)key; 117 | 118 | /// Returns all the views that have been registered with the given @c reuseIdentifier. 119 | - (NSArray *)viewsWithReuseIdentifier:(NSString *)reuseIdentifier; 120 | 121 | @end 122 | 123 | NS_SWIFT_NAME(NullNode) 124 | @interface CRNullNode : CRNode 125 | 126 | /// The default nil node instance. 127 | @property(class, readonly) CRNullNode *nullNode; 128 | 129 | @end 130 | 131 | NS_ASSUME_NONNULL_END 132 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNode.mm: -------------------------------------------------------------------------------- 1 | #import "CRNode.h" 2 | #import "CRContext.h" 3 | #import "CRCoordinator+Private.h" 4 | #import "CRMacros.h" 5 | #import "CRNodeBridge.h" 6 | #import "CRNodeHierarchy.h" 7 | #import "CRNodeLayoutSpec.h" 8 | #import "UIView+CRNode.h" 9 | #import "YGLayout.h" 10 | 11 | @implementation CRAnyNode 12 | @end 13 | 14 | @interface CRNode () 15 | @property(nonatomic, readwrite) __kindof CRCoordinator *coordinator; 16 | @property(nonatomic, readwrite) NSUInteger index; 17 | @property(nonatomic, readwrite, nullable, weak) CRNode *parent; 18 | @property(nonatomic, readwrite, nullable) __kindof UIView *renderedView; 19 | /// The view initialization block. 20 | @property(nonatomic, copy) UIView * (^viewInit)(void); 21 | /// View configuration block. 22 | @property(nonatomic, copy, nonnull) void (^layoutSpec)(CRNodeLayoutSpec *); 23 | @end 24 | 25 | void CRIllegalCoordinatorTypeException(NSString *reason) { 26 | @throw [NSException exceptionWithName:@"IllegalCoordinatorTypeException" 27 | reason:reason 28 | userInfo:nil]; 29 | } 30 | 31 | @implementation CRNode { 32 | NSMutableArray *_mutableChildren; 33 | __weak CRNodeHierarchy *_nodeHierarchy; 34 | __weak CRContext *_context; 35 | CGSize _size; 36 | struct { 37 | unsigned int shouldInvokeDidMount : 1; 38 | } __attribute__((packed, aligned(1))) _flags; 39 | } 40 | 41 | #pragma mark - Initializer 42 | 43 | - (instancetype)initWithType:(Class)type 44 | reuseIdentifier:(NSString *)reuseIdentifier 45 | key:(NSString *)key 46 | viewInit:(UIView * (^_Nullable)(void))viewInit 47 | layoutSpec:(void (^)(CRNodeLayoutSpec *))layoutSpec { 48 | if (self = [super init]) { 49 | _reuseIdentifier = CR_NIL_COALESCING(reuseIdentifier, NSStringFromClass(type)); 50 | _key = key; 51 | _viewType = type; 52 | _mutableChildren = [[NSMutableArray alloc] init]; 53 | self.viewInit = viewInit; 54 | self.layoutSpec = layoutSpec; 55 | } 56 | return self; 57 | } 58 | 59 | #pragma mark - Convenience Initializer 60 | 61 | + (instancetype)nodeWithType:(Class)type 62 | reuseIdentifier:(NSString *)reuseIdentifier 63 | key:(nullable NSString *)key 64 | viewInit:(UIView * (^_Nullable)(void))viewInit 65 | layoutSpec:(void (^)(CRNodeLayoutSpec *))layoutSpec { 66 | return [[CRNode alloc] initWithType:type 67 | reuseIdentifier:reuseIdentifier 68 | key:key 69 | viewInit:viewInit 70 | layoutSpec:layoutSpec]; 71 | } 72 | 73 | + (instancetype)nodeWithType:(Class)type 74 | layoutSpec:(void (^)(CRNodeLayoutSpec *))layoutSpec { 75 | return [[CRNode alloc] initWithType:type 76 | reuseIdentifier:nil 77 | key:nil 78 | viewInit:nil 79 | layoutSpec:layoutSpec]; 80 | } 81 | 82 | #pragma mark - Context 83 | 84 | - (void)registerNodeHierarchyInContext:(CRContext *)context { 85 | CR_ASSERT_ON_MAIN_THREAD(); 86 | if (!_parent) { 87 | _context = context; 88 | [self _recursivelyConfigureCoordinatorsInNodeHierarchy]; 89 | } else 90 | [_parent registerNodeHierarchyInContext:context]; 91 | } 92 | 93 | - (void)_recursivelyConfigureCoordinatorsInNodeHierarchy { 94 | self.coordinator.node = self; 95 | CR_FOREACH(child, _mutableChildren) { [child _recursivelyConfigureCoordinatorsInNodeHierarchy]; } 96 | } 97 | 98 | - (CRContext *)context { 99 | if (_context) return _context; 100 | return _parent.context; 101 | } 102 | 103 | - (CRNode *)root { 104 | if (!_parent) return self; 105 | return _parent.root; 106 | } 107 | 108 | - (__kindof CRCoordinator *)coordinator { 109 | const auto context = self.context; 110 | if (!context) return nil; 111 | if (!_coordinatorDescriptor) return _parent.coordinator; 112 | return [context coordinator:_coordinatorDescriptor]; 113 | } 114 | 115 | - (CRNodeHierarchy *)nodeHierarchy { 116 | if (!_parent) return _nodeHierarchy; 117 | return _parent.nodeHierarchy; 118 | } 119 | 120 | - (void)setNodeHierarchy:(CRNodeHierarchy *)nodeHierarchy { 121 | if (!_parent) { 122 | _nodeHierarchy = nodeHierarchy; 123 | return; 124 | } 125 | [_parent setNodeHierarchy:nodeHierarchy]; 126 | } 127 | 128 | #pragma mark - Children 129 | 130 | - (BOOL)isNullNode { 131 | return false; 132 | } 133 | 134 | - (NSArray *)children { 135 | return _mutableChildren; 136 | } 137 | 138 | - (instancetype)appendChildren:(NSArray *)children { 139 | CR_ASSERT_ON_MAIN_THREAD(); 140 | auto lastIndex = _mutableChildren.lastObject.index; 141 | CR_FOREACH(child, children) { 142 | if (child.isNullNode) continue; 143 | child.index = lastIndex++; 144 | child.parent = self; 145 | [_mutableChildren addObject:child]; 146 | } 147 | return self; 148 | } 149 | 150 | - (instancetype)bindCoordinator:(CRCoordinatorDescriptor *)descriptor { 151 | CR_ASSERT_ON_MAIN_THREAD(); 152 | _coordinatorDescriptor = descriptor; 153 | return self; 154 | } 155 | 156 | #pragma mark - Querying 157 | 158 | - (UIView *)viewWithKey:(NSString *)key { 159 | if ([_key isEqualToString:key]) return _renderedView; 160 | CR_FOREACH(child, _mutableChildren) { 161 | if (const auto view = [child viewWithKey:key]) return view; 162 | } 163 | return nil; 164 | } 165 | 166 | - (NSArray *)viewsWithReuseIdentifier:(NSString *)reuseIdentifier { 167 | auto result = [[NSMutableArray alloc] init]; 168 | [self _viewsWithReuseIdentifier:reuseIdentifier withArray:result]; 169 | return result; 170 | } 171 | 172 | - (void)_viewsWithReuseIdentifier:(NSString *)reuseIdentifier 173 | withArray:(NSMutableArray *)array { 174 | if ([_key isEqualToString:reuseIdentifier] && _renderedView) { 175 | [array addObject:_renderedView]; 176 | } 177 | CR_FOREACH(child, _mutableChildren) { 178 | [child _viewsWithReuseIdentifier:reuseIdentifier withArray:array]; 179 | } 180 | } 181 | 182 | #pragma mark - Layout 183 | 184 | - (void)_constructViewWithReusableView:(nullable UIView *)reusableView { 185 | CR_ASSERT_ON_MAIN_THREAD(); 186 | if (_renderedView != nil) return; 187 | 188 | if ([reusableView isKindOfClass:self.viewType]) { 189 | _renderedView = reusableView; 190 | _renderedView.cr_nodeBridge.node = self; 191 | } else { 192 | if (_viewInit) { 193 | _renderedView = _viewInit(); 194 | } else { 195 | _renderedView = [[self.viewType alloc] initWithFrame:CGRectZero]; 196 | } 197 | _renderedView.yoga.isEnabled = true; 198 | _renderedView.tag = _reuseIdentifier.hash; 199 | _renderedView.cr_nodeBridge.node = self; 200 | _flags.shouldInvokeDidMount = true; 201 | } 202 | } 203 | 204 | - (void)_configureConstrainedToSize:(CGSize)size withOptions:(CRNodeLayoutOptions)options { 205 | [self _constructViewWithReusableView:nil]; 206 | [_renderedView.cr_nodeBridge storeViewSubTreeOldGeometry]; 207 | const auto spec = [[CRNodeLayoutSpec alloc] initWithNode:self constrainedToSize:size]; 208 | _layoutSpec(spec); 209 | 210 | CR_FOREACH(child, _mutableChildren) { 211 | [child _configureConstrainedToSize:size withOptions:options]; 212 | } 213 | 214 | if (_renderedView.yoga.isEnabled && _renderedView.yoga.isLeaf && 215 | _renderedView.yoga.isIncludedInLayout) { 216 | _renderedView.frame.size = CGSizeZero; 217 | [_renderedView.yoga markDirty]; 218 | } 219 | 220 | if (spec.onLayoutSubviews) { 221 | spec.onLayoutSubviews(self, _renderedView, size); 222 | } 223 | } 224 | 225 | - (void)_computeFlexboxLayoutConstrainedToSize:(CGSize)size { 226 | auto rect = CGRectZero; 227 | rect.size = size; 228 | _renderedView.frame = rect; 229 | [_renderedView.yoga applyLayoutPreservingOrigin:NO]; 230 | rect = _renderedView.frame; 231 | rect.size = _renderedView.yoga.intrinsicSize; 232 | _renderedView.frame = rect; 233 | [_renderedView.yoga applyLayoutPreservingOrigin:NO]; 234 | rect = _renderedView.frame; 235 | [_renderedView cr_normalizeFrame]; 236 | } 237 | 238 | - (void)_animateLayoutChangesIfNecessary { 239 | const auto animator = self.context.layoutAnimator; 240 | const auto view = _renderedView; 241 | if (!animator) return; 242 | [view.cr_nodeBridge storeViewSubTreeNewGeometry]; 243 | [view.cr_nodeBridge applyViewSubTreeOldGeometry]; 244 | [animator stopAnimation:YES]; 245 | [animator addAnimations:^{ 246 | [view.cr_nodeBridge applyViewSubTreeNewGeometry]; 247 | }]; 248 | [view.cr_nodeBridge fadeInNewlyCreatedViewsInViewSubTreeWithDelay:animator.duration]; 249 | [animator startAnimation]; 250 | } 251 | 252 | - (void)layoutConstrainedToSize:(CGSize)size withOptions:(CRNodeLayoutOptions)options { 253 | CR_ASSERT_ON_MAIN_THREAD(); 254 | if (_parent != nil) return [_parent layoutConstrainedToSize:size withOptions:options]; 255 | 256 | _size = size; 257 | auto safeAreaOffset = CGPointZero; 258 | if (@available(iOS 11, *)) { 259 | if (options & CRNodeLayoutOptionsUseSafeAreaInsets) { 260 | UIEdgeInsets safeArea = _renderedView.superview.safeAreaInsets; 261 | CGFloat heightInsets = safeArea.top + safeArea.bottom; 262 | CGFloat widthInsets = safeArea.left + safeArea.right; 263 | size.height -= heightInsets; 264 | size.width -= widthInsets; 265 | safeAreaOffset.x = safeArea.left; 266 | safeAreaOffset.y = safeArea.top; 267 | } 268 | } 269 | NSUInteger numberOfLayoutPasses = 1; 270 | for (NSUInteger pass = 0; pass < numberOfLayoutPasses; pass++) { 271 | [self _configureConstrainedToSize:size withOptions:options]; 272 | [self _computeFlexboxLayoutConstrainedToSize:size]; 273 | } 274 | auto frame = _renderedView.frame; 275 | frame.origin.x += safeAreaOffset.x; 276 | frame.origin.y += safeAreaOffset.y; 277 | _renderedView.frame = frame; 278 | 279 | if (options & CRNodeLayoutOptionsSizeContainerViewToFit) { 280 | auto superview = _renderedView.superview; 281 | UIEdgeInsets insets; 282 | insets.left = CR_NORMALIZE(_renderedView.yoga.marginLeft); 283 | insets.right = CR_NORMALIZE(_renderedView.yoga.marginRight); 284 | insets.top = CR_NORMALIZE(_renderedView.yoga.marginTop); 285 | insets.bottom = CR_NORMALIZE(_renderedView.yoga.marginBottom); 286 | auto rect = CGRectInset(_renderedView.bounds, -(insets.left + insets.right), 287 | -(insets.top + insets.bottom)); 288 | rect.origin = superview.frame.origin; 289 | superview.frame = rect; 290 | } 291 | [_renderedView cr_adjustContentSizePostLayoutRecursivelyIfNeeded]; 292 | 293 | [self.coordinator onLayout]; 294 | [self _animateLayoutChangesIfNecessary]; 295 | } 296 | 297 | - (void)_reconcileNode:(CRNode *)node 298 | inView:(UIView *)candidateView 299 | constrainedToSize:(CGSize)size 300 | withParentView:(UIView *)parentView { 301 | // The candidate view is a good match for reuse. 302 | if ([candidateView isKindOfClass:node.viewType] && candidateView.cr_hasNode && 303 | candidateView.tag == node.reuseIdentifier.hash) { 304 | [node _constructViewWithReusableView:candidateView]; 305 | candidateView.cr_nodeBridge.isNewlyCreated = false; 306 | // The view for this node needs to be created. 307 | } else { 308 | [candidateView removeFromSuperview]; 309 | [node _constructViewWithReusableView:nil]; 310 | node.renderedView.cr_nodeBridge.isNewlyCreated = true; 311 | [parentView insertSubview:node.renderedView atIndex:node.index]; 312 | } 313 | const auto view = node.renderedView; 314 | // Get all of the subviews. 315 | const auto subviews = [[NSMutableArray alloc] initWithCapacity:view.subviews.count]; 316 | CR_FOREACH(subview, view.subviews) { 317 | if (!subview.cr_hasNode) continue; 318 | [subviews addObject:subview]; 319 | } 320 | // Iterate children. 321 | CR_FOREACH(child, node.children) { 322 | UIView *candidateView = nil; 323 | auto index = 0; 324 | CR_FOREACH(subview, subviews) { 325 | if ([subview isKindOfClass:child.viewType] && subview.tag == child.reuseIdentifier.hash) { 326 | candidateView = subview; 327 | break; 328 | } 329 | index++; 330 | } 331 | // Pops the candidate view from the collection. 332 | if (candidateView != nil) [subviews removeObjectAtIndex:index]; 333 | // Recursively reconcile the subnode. 334 | [node _reconcileNode:child 335 | inView:candidateView 336 | constrainedToSize:size 337 | withParentView:node.renderedView]; 338 | } 339 | // Remove all of the obsolete old views that couldn't be recycled. 340 | CR_FOREACH(subview, subviews) { [subview removeFromSuperview]; } 341 | } 342 | 343 | - (void)reconcileInView:(UIView *)view 344 | constrainedToSize:(CGSize)size 345 | withOptions:(CRNodeLayoutOptions)options { 346 | CR_ASSERT_ON_MAIN_THREAD(); 347 | if (_parent != nil) 348 | return [_parent reconcileInView:view constrainedToSize:size withOptions:options]; 349 | 350 | _size = size; 351 | const auto containerView = CR_NIL_COALESCING(view, _renderedView.superview); 352 | const auto bounds = CGSizeEqualToSize(size, CGSizeZero) ? containerView.bounds.size : size; 353 | [self _reconcileNode:self 354 | inView:containerView.subviews.firstObject 355 | constrainedToSize:bounds 356 | withParentView:containerView]; 357 | 358 | [self layoutConstrainedToSize:size withOptions:options]; 359 | 360 | if (_flags.shouldInvokeDidMount && 361 | [self.delegate respondsToSelector:@selector(rootNodeDidMount:)]) { 362 | _flags.shouldInvokeDidMount = false; 363 | [self.delegate rootNodeDidMount:self]; 364 | } 365 | } 366 | 367 | - (void)setNeedsConfigure { 368 | const auto spec = [[CRNodeLayoutSpec alloc] initWithNode:self constrainedToSize:_size]; 369 | _layoutSpec(spec); 370 | } 371 | 372 | @end 373 | 374 | #pragma mark - nullNode 375 | 376 | @implementation CRNullNode 377 | 378 | + (instancetype)nullNode { 379 | static CRNullNode *sharedInstance = nil; 380 | static dispatch_once_t onceToken; 381 | dispatch_once(&onceToken, ^{ 382 | sharedInstance = [[self alloc] init]; 383 | }); 384 | return sharedInstance; 385 | } 386 | 387 | - (BOOL)isNullNode { 388 | return true; 389 | } 390 | 391 | @end 392 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNodeBridge.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | NS_ASSUME_NONNULL_BEGIN 5 | 6 | @class CRNode; 7 | @class CRCoordinatorDescriptor; 8 | 9 | NS_SWIFT_NAME(NodeBridge) 10 | @interface CRNodeBridge : NSObject 11 | /// Whether the view has been created at the last render pass. 12 | @property(nonatomic) BOOL isNewlyCreated; 13 | /// The node associated to this view. 14 | @property(nonatomic, nullable) CRNode *node; 15 | /// The bridged view. 16 | @property(nonatomic, nullable, weak) UIView *view; 17 | /// Layout animator for this subtree. 18 | @property(nonatomic, nullable) UIViewPropertyAnimator *layoutAnimator; 19 | 20 | - (instancetype)initWithView:(UIView *)view; 21 | 22 | /// Stores the current (now considered old in the current run-loop) geometry for the associated 23 | /// view and all of its subviews recursively. 24 | - (void)storeViewSubTreeOldGeometry; 25 | 26 | /// Applies the stored old geometry to this view subtree. 27 | - (void)applyViewSubTreeOldGeometry; 28 | 29 | /// Stores the geometry for the associated view after the node has rendered at the end of the 30 | /// current run-loop. 31 | - (void)storeViewSubTreeNewGeometry; 32 | 33 | /// Applies the stored new geometry to this view subtree. 34 | - (void)applyViewSubTreeNewGeometry; 35 | 36 | /// Transition in all of the newly created view in the view hierarchy. 37 | - (void)fadeInNewlyCreatedViewsInViewSubTreeWithDelay:(NSTimeInterval)delay; 38 | 39 | /// Set the property at the given keyPath.nil 40 | - (void)setPropertyWithKeyPath:(NSString *)keyPath 41 | value:(id)value 42 | animator:(nullable UIViewPropertyAnimator *)animator; 43 | 44 | /// Restore the view to its initial state. 45 | - (void)restore; 46 | 47 | @end 48 | 49 | NS_ASSUME_NONNULL_END 50 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNodeBridge.mm: -------------------------------------------------------------------------------- 1 | #import "CRNodeBridge.h" 2 | #import "CRCoordinator.h" 3 | #import "CRMacros.h" 4 | #import "CRNode.h" 5 | #import "CRNodeBridge.h" 6 | #import "UIView+CRNode.h" 7 | 8 | @implementation CRNodeBridge { 9 | /// The previous rect for the associated view. 10 | CGRect _oldGeometry; 11 | /// The new rect for the associated view. 12 | CGRect _newGeometry; 13 | /// The alpha computed after the last render pass. 14 | CGFloat _targetAlpha; 15 | /// The initial property values for the associated view. 16 | NSMutableDictionary *_initialPropertyValues; 17 | } 18 | 19 | - (instancetype)initWithView:(UIView *)view { 20 | if (self = [super init]) { 21 | _view = view; 22 | _initialPropertyValues = [[NSMutableDictionary alloc] init]; 23 | } 24 | return self; 25 | } 26 | 27 | - (void)storeViewSubTreeOldGeometry { 28 | if (!_view.cr_hasNode) return; 29 | _oldGeometry = _view.frame; 30 | 31 | CR_FOREACH(subview, _view.subviews) { 32 | if (!subview.cr_hasNode) continue; 33 | [subview.cr_nodeBridge storeViewSubTreeOldGeometry]; 34 | } 35 | } 36 | 37 | - (void)applyViewSubTreeOldGeometry { 38 | CR_ASSERT_ON_MAIN_THREAD(); 39 | if (!_view.cr_hasNode) return; 40 | if (!(_isNewlyCreated && CGRectEqualToRect(_oldGeometry, CGRectZero))) { 41 | _view.alpha = 0; 42 | } else { 43 | _view.frame = _oldGeometry; 44 | CR_FOREACH(subview, _view.subviews) { 45 | if (!subview.cr_hasNode) continue; 46 | [subview.cr_nodeBridge applyViewSubTreeOldGeometry]; 47 | } 48 | } 49 | } 50 | 51 | - (void)storeViewSubTreeNewGeometry { 52 | if (!_view.cr_hasNode) return; 53 | _newGeometry = _view.frame; 54 | _targetAlpha = _view.alpha; 55 | 56 | CR_FOREACH(subview, _view.subviews) { 57 | if (!subview.cr_hasNode) continue; 58 | [subview.cr_nodeBridge storeViewSubTreeNewGeometry]; 59 | } 60 | } 61 | 62 | - (void)applyViewSubTreeNewGeometry { 63 | CR_ASSERT_ON_MAIN_THREAD(); 64 | if (!_view.cr_hasNode) return; 65 | _view.frame = _newGeometry; 66 | _view.alpha = _targetAlpha; 67 | CR_FOREACH(subview, _view.subviews) { 68 | if (!subview.cr_hasNode) continue; 69 | [subview.cr_nodeBridge applyViewSubTreeNewGeometry]; 70 | } 71 | } 72 | 73 | - (void)fadeInNewlyCreatedViewsInViewSubTreeWithDelay:(NSTimeInterval)delay { 74 | CR_ASSERT_ON_MAIN_THREAD(); 75 | static const auto duration = 0.16; 76 | const auto options = 77 | UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut; 78 | 79 | CR_WEAKIFY(self); 80 | [UIView animateWithDuration:duration 81 | delay:delay 82 | options:options 83 | animations:^{ 84 | CR_STRONGIFY_AND_RETURN_IF_NIL(self); 85 | [self _restoreAlphaRecursively]; 86 | } 87 | completion:nil]; 88 | } 89 | 90 | - (void)_restoreAlphaRecursively { 91 | if (!_view.cr_hasNode) return; 92 | if (fabs(_view.alpha - _targetAlpha) > FLT_EPSILON) _view.alpha = _targetAlpha; 93 | CR_FOREACH(subview, _view.subviews) { [subview.cr_nodeBridge _restoreAlphaRecursively]; } 94 | } 95 | 96 | - (void)setPropertyWithKeyPath:(NSString *)keyPath 97 | value:(id)value 98 | animator:(UIViewPropertyAnimator *)animator { 99 | CR_ASSERT_ON_MAIN_THREAD(); 100 | if (!_view.cr_hasNode) return; 101 | 102 | const id currentValue = [_view valueForKeyPath:keyPath]; 103 | if (!_initialPropertyValues[keyPath]) { 104 | _initialPropertyValues[keyPath] = currentValue; 105 | } 106 | if (![currentValue isEqual:value]) { 107 | CR_WEAKIFY(self); 108 | if (!animator) { 109 | [self.view setValue:value forKeyPath:keyPath]; 110 | } else { 111 | [animator addAnimations:^{ 112 | CR_STRONGIFY_AND_RETURN_IF_NIL(self); 113 | [self.view setValue:value forKeyPath:keyPath]; 114 | }]; 115 | } 116 | } 117 | } 118 | 119 | - (void)restore { 120 | CR_FOREACH(keyPath, _initialPropertyValues) { 121 | const id value = _initialPropertyValues[keyPath]; 122 | [self setPropertyWithKeyPath:keyPath value:value animator:nil]; 123 | } 124 | } 125 | 126 | @end 127 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNodeBuilder+Modifiers.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "CRNodeBuilder.h" 5 | #import "YGLayout.h" 6 | 7 | NS_ASSUME_NONNULL_BEGIN 8 | 9 | @interface CRNodeBuilder (Modifiers) 10 | 11 | /// Padding for the view. 12 | - (instancetype)padding:(CGFloat)padding; 13 | 14 | /// Pads the view using the specified edge insets. 15 | - (instancetype)paddingInsets:(UIEdgeInsets)padding; 16 | 17 | /// Margins for the view. 18 | - (instancetype)margin:(CGFloat)padding; 19 | 20 | /// Margins for the view using the specified edge insets. 21 | - (instancetype)marginInsets:(UIEdgeInsets)padding; 22 | 23 | /// Borders for the view using the specified edge insets. 24 | - (instancetype)border:(UIEdgeInsets)border; 25 | 26 | /// The view background color. 27 | - (instancetype)background:(UIColor *)color; 28 | 29 | /// Clips the view to its bounding frame, with the specified corner radius. 30 | - (instancetype)cornerRadius:(CGFloat)value; 31 | 32 | /// Clips the view to its bounding rectangular frame. 33 | - (instancetype)clipped:(BOOL)value; 34 | 35 | /// Whether the view is hidden or not. 36 | - (instancetype)hidden:(BOOL)value; 37 | 38 | /// Sets the transparency of the view. 39 | - (instancetype)opacity:(CGFloat)value; 40 | 41 | /// Controls the direction in which the children of a node are laid out. 42 | /// This is also referred to as the main axis. The cross axis is the axis perpendicular to 43 | /// the main axis, or the axis which the wrapping lines are laid out in. 44 | - (instancetype)flexDirection:(YGFlexDirection)value; 45 | 46 | /// Defines how to distribute space between and around content 47 | /// items along the main-axis of a flex container, and the inline axis of a grid container. 48 | - (instancetype)justifyContent:(YGJustify)value; 49 | 50 | /// Controls how rows align in the cross direction, overriding the @c alignContent 51 | /// of the parent. 52 | - (instancetype)alignContent:(YGAlign)value; 53 | 54 | /// Aligns children in the cross direction. For example, if children are flowing 55 | /// vertically, @c alignItems controls how they align horizontally. 56 | - (instancetype)alignItems:(YGAlign)value; 57 | 58 | /// Controls how a child aligns in the cross direction, overriding the 59 | /// @c alignItems of the parent. 60 | - (instancetype)alignSelf:(YGAlign)value; 61 | 62 | /// Absolute positioning is always relative to the parent. 63 | /// If you want to position a child using specific numbers of logical pixels relative to its 64 | /// parent, set the child to have absolute position. 65 | /// If you want to position a child relative to something that is not its parent, don't use 66 | /// styles for that. Use the component tree. 67 | - (instancetype)position:(YGPositionType)value; 68 | 69 | /// Controls whether children can wrap around after they hit the end of a flex container. 70 | - (instancetype)flexWrap:(YGWrap)value; 71 | 72 | /// Controls how children are measured and displayed 73 | - (instancetype)overflow:(YGOverflow)value; 74 | 75 | /// Default flex appearance. 76 | - (instancetype)flex; 77 | 78 | /// Sets the flex grow factor of a flex item main size. It specifies how much of the remaining 79 | /// space in the flex container should be assigned to the item (the flex grow factor). 80 | /// The main size is either width or height of the item which is dependent on the @c 81 | /// flexDirection value. 82 | /// The remaining space is the size of the flex container minus the size of all flex items' sizes 83 | /// together. If all sibling items have the same flex grow factor, then all items will receive 84 | /// the same share of remaining space, otherwise it is distributed according to the ratio 85 | /// defined by the different flex grow factors. 86 | - (instancetype)flexGrow:(CGFloat)value; 87 | 88 | /// Sets the flex shrink factor of a flex item. 89 | - (instancetype)flexShrink:(CGFloat)value; 90 | 91 | /// Sets the initial main size of a flex item. 92 | - (instancetype)flexBasis:(CGFloat)value; 93 | 94 | /// The view fixed width. 95 | - (instancetype)width:(CGFloat)value; 96 | 97 | /// The view fixed height. 98 | - (instancetype)height:(CGFloat)value; 99 | 100 | /// The view minimum width. 101 | - (instancetype)minWidth:(CGFloat)value; 102 | 103 | /// The view minimum height. 104 | - (instancetype)minHeight:(CGFloat)value; 105 | 106 | /// The view maximum width. 107 | - (instancetype)maxWidth:(CGFloat)value; 108 | 109 | /// The view maximum height. 110 | - (instancetype)maxHeight:(CGFloat)value; 111 | 112 | /// Matches the parent width. 113 | - (instancetype)matchHostingViewWidthWithMargin:(CGFloat)margin; 114 | 115 | /// Matches the parent height. 116 | - (instancetype)matchHostingViewHeightWithMargin:(CGFloat)margin; 117 | 118 | /// Determines whether user events are ignored and removed from the event queue. 119 | - (instancetype)userInteractionEnabled:(BOOL)userInteractionEnabled; 120 | 121 | /// Applies a transformation. 122 | - (instancetype)transform:(CGAffineTransform)transform animator:(UIViewPropertyAnimator *)animator; 123 | 124 | /// Adds an animator for the whole view layout. 125 | - (instancetype)layoutAnimator:(UIViewPropertyAnimator *)animator; 126 | 127 | @end 128 | 129 | @interface CRNodeBuilder (UIControl) 130 | 131 | /// A Boolean value indicating whether the control is enabled. 132 | - (instancetype)enabled:(BOOL)enabled; 133 | 134 | /// A Boolean value indicating whether the control is in the selected state. 135 | - (instancetype)selected:(BOOL)selected; 136 | 137 | /// A Boolean value indicating whether the control draws a highlight. 138 | - (instancetype)highlighted:(BOOL)selected; 139 | 140 | /// Associates a target object and action method with the control. 141 | - (instancetype)setTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)events; 142 | 143 | @end 144 | 145 | @interface CRNodeBuilder (UILabel) 146 | 147 | /// The current text that is displayed by the label. 148 | - (instancetype)text:(nullable NSString *)text; 149 | 150 | /// The current styled text that is displayed by the label. 151 | - (instancetype)attributedText:(nullable NSAttributedString *)attributedText; 152 | 153 | /// The current styled text that is displayed by the label. 154 | - (instancetype)font:(UIFont *)font; 155 | 156 | /// The color of the text. 157 | - (instancetype)textColor:(UIColor *)textColor; 158 | 159 | /// The technique to use for aligning the text. 160 | - (instancetype)textAlignment:(NSTextAlignment)textAlignment; 161 | 162 | /// The technique to use for wrapping and truncating the label’s text. 163 | - (instancetype)lineBreakMode:(NSLineBreakMode)lineBreakMode; 164 | 165 | /// The maximum number of lines to use for rendering text. 166 | - (instancetype)numberOfLines:(NSUInteger)numberOfLines; 167 | 168 | @end 169 | 170 | NS_ASSUME_NONNULL_END 171 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNodeBuilder+Modifiers.mm: -------------------------------------------------------------------------------- 1 | #import "CRContext.h" 2 | #import "CRCoordinator.h" 3 | #import "CRMacros.h" 4 | #import "CRNodeBuilder+Modifiers.h" 5 | #import "CRNodeLayoutSpec.h" 6 | #import "YGLayout.h" 7 | 8 | void _CRUnsafeSet(CRNodeLayoutSpec *spec, NSString *keyPath, id value) { 9 | const auto selector = NSSelectorFromString(keyPath); 10 | if (![spec.view respondsToSelector:selector]) { 11 | NSLog(@"warning: cannot find keyPath %@ in class %@", keyPath, spec.view.class); 12 | } else { 13 | [spec set:keyPath value:value]; 14 | } 15 | } 16 | 17 | @implementation CRNodeBuilder (Modifiers) 18 | 19 | - (instancetype)padding:(CGFloat)padding { 20 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 21 | [spec set:CR_KEYPATH(spec.view, yoga.padding) value:@(padding)]; 22 | }]; 23 | } 24 | 25 | - (instancetype)paddingInsets:(UIEdgeInsets)padding { 26 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 27 | [spec set:CR_KEYPATH(spec.view, yoga.paddingTop) value:@(padding.top)]; 28 | [spec set:CR_KEYPATH(spec.view, yoga.paddingBottom) value:@(padding.bottom)]; 29 | [spec set:CR_KEYPATH(spec.view, yoga.paddingLeft) value:@(padding.left)]; 30 | [spec set:CR_KEYPATH(spec.view, yoga.paddingRight) value:@(padding.right)]; 31 | }]; 32 | } 33 | 34 | - (instancetype)margin:(CGFloat)margin { 35 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 36 | [spec set:CR_KEYPATH(spec.view, yoga.margin) value:@(margin)]; 37 | }]; 38 | } 39 | 40 | - (instancetype)marginInsets:(UIEdgeInsets)margin { 41 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 42 | [spec set:CR_KEYPATH(spec.view, yoga.marginTop) value:@(margin.top)]; 43 | [spec set:CR_KEYPATH(spec.view, yoga.marginBottom) value:@(margin.bottom)]; 44 | [spec set:CR_KEYPATH(spec.view, yoga.marginLeft) value:@(margin.left)]; 45 | [spec set:CR_KEYPATH(spec.view, yoga.marginRight) value:@(margin.right)]; 46 | }]; 47 | } 48 | 49 | - (instancetype)border:(UIEdgeInsets)border { 50 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 51 | [spec set:CR_KEYPATH(spec.view, yoga.borderTopWidth) value:@(border.top)]; 52 | [spec set:CR_KEYPATH(spec.view, yoga.borderBottomWidth) value:@(border.bottom)]; 53 | [spec set:CR_KEYPATH(spec.view, yoga.borderLeftWidth) value:@(border.left)]; 54 | [spec set:CR_KEYPATH(spec.view, yoga.borderRightWidth) value:@(border.right)]; 55 | }]; 56 | } 57 | 58 | - (instancetype)background:(UIColor *)color { 59 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 60 | [spec set:CR_KEYPATH(spec.view, backgroundColor) value:color]; 61 | }]; 62 | } 63 | 64 | - (instancetype)cornerRadius:(CGFloat)value { 65 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 66 | [spec set:CR_KEYPATH(spec.view, clipsToBounds) value:@(YES)]; 67 | [spec set:CR_KEYPATH(spec.view, layer.cornerRadius) value:@(value)]; 68 | }]; 69 | } 70 | 71 | - (instancetype)clipped:(BOOL)value { 72 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 73 | [spec set:CR_KEYPATH(spec.view, clipsToBounds) value:@(value)]; 74 | }]; 75 | } 76 | 77 | - (instancetype)hidden:(BOOL)value { 78 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 79 | [spec set:CR_KEYPATH(spec.view, hidden) value:@(value)]; 80 | }]; 81 | } 82 | 83 | - (instancetype)opacity:(CGFloat)value { 84 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 85 | [spec set:CR_KEYPATH(spec.view, alpha) value:@(YES)]; 86 | }]; 87 | } 88 | 89 | - (instancetype)flexDirection:(YGFlexDirection)value { 90 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 91 | [spec set:CR_KEYPATH(spec.view, yoga.flexDirection) value:@(value)]; 92 | }]; 93 | } 94 | 95 | - (instancetype)justifyContent:(YGJustify)value { 96 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 97 | [spec set:CR_KEYPATH(spec.view, yoga.justifyContent) value:@(value)]; 98 | }]; 99 | } 100 | 101 | - (instancetype)alignContent:(YGAlign)value { 102 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 103 | [spec set:CR_KEYPATH(spec.view, yoga.alignContent) value:@(value)]; 104 | }]; 105 | } 106 | 107 | - (instancetype)alignItems:(YGAlign)value { 108 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 109 | [spec set:CR_KEYPATH(spec.view, yoga.alignItems) value:@(value)]; 110 | }]; 111 | } 112 | 113 | - (instancetype)alignSelf:(YGAlign)value { 114 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 115 | [spec set:CR_KEYPATH(spec.view, yoga.alignSelf) value:@(value)]; 116 | }]; 117 | } 118 | 119 | - (instancetype)position:(YGPositionType)value { 120 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 121 | [spec set:CR_KEYPATH(spec.view, yoga.position) value:@(value)]; 122 | }]; 123 | } 124 | 125 | - (instancetype)flexWrap:(YGWrap)value { 126 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 127 | [spec set:CR_KEYPATH(spec.view, yoga.flexWrap) value:@(value)]; 128 | }]; 129 | } 130 | 131 | - (instancetype)overflow:(YGOverflow)value { 132 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 133 | [spec set:CR_KEYPATH(spec.view, yoga.overflow) value:@(value)]; 134 | }]; 135 | } 136 | 137 | - (instancetype)flex { 138 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 139 | [spec.view.yoga flex]; 140 | }]; 141 | } 142 | 143 | - (instancetype)flexGrow:(CGFloat)value { 144 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 145 | [spec set:CR_KEYPATH(spec.view, yoga.flexGrow) value:@(value)]; 146 | }]; 147 | } 148 | 149 | - (instancetype)flexShrink:(CGFloat)value { 150 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 151 | [spec set:CR_KEYPATH(spec.view, yoga.flexShrink) value:@(value)]; 152 | }]; 153 | } 154 | 155 | - (instancetype)flexBasis:(CGFloat)value { 156 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 157 | [spec set:CR_KEYPATH(spec.view, yoga.flexBasis) value:@(value)]; 158 | }]; 159 | } 160 | 161 | - (instancetype)width:(CGFloat)value { 162 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 163 | [spec set:CR_KEYPATH(spec.view, yoga.width) value:@(value)]; 164 | }]; 165 | } 166 | 167 | - (instancetype)height:(CGFloat)value { 168 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 169 | [spec set:CR_KEYPATH(spec.view, yoga.height) value:@(value)]; 170 | }]; 171 | } 172 | 173 | - (instancetype)minWidth:(CGFloat)value { 174 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 175 | [spec set:CR_KEYPATH(spec.view, yoga.minWidth) value:@(value)]; 176 | }]; 177 | } 178 | 179 | - (instancetype)minHeight:(CGFloat)value { 180 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 181 | [spec set:CR_KEYPATH(spec.view, yoga.minHeight) value:@(value)]; 182 | }]; 183 | } 184 | 185 | - (instancetype)maxWidth:(CGFloat)value { 186 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 187 | [spec set:CR_KEYPATH(spec.view, yoga.maxWidth) value:@(value)]; 188 | }]; 189 | } 190 | 191 | - (instancetype)maxHeight:(CGFloat)value { 192 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 193 | [spec set:CR_KEYPATH(spec.view, yoga.maxHeight) value:@(value)]; 194 | }]; 195 | } 196 | 197 | - (instancetype)matchHostingViewWidthWithMargin:(CGFloat)margin { 198 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 199 | [spec set:CR_KEYPATH(spec.view, yoga.width) value:@(spec.size.width - 2 * margin)]; 200 | }]; 201 | } 202 | 203 | - (instancetype)matchHostingViewHeightWithMargin:(CGFloat)margin { 204 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 205 | [spec set:CR_KEYPATH(spec.view, yoga.height) value:@(spec.size.height - 2 * margin)]; 206 | }]; 207 | } 208 | 209 | - (instancetype)userInteractionEnabled:(BOOL)userInteractionEnabled { 210 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 211 | [spec set:CR_KEYPATH(spec.view, userInteractionEnabled) value:@(userInteractionEnabled)]; 212 | }]; 213 | } 214 | 215 | - (instancetype)transform:(CGAffineTransform)transform animator:(UIViewPropertyAnimator *)animator { 216 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 217 | [spec set:CR_KEYPATH(spec.view, transform) value:@(transform) animator:animator]; 218 | }]; 219 | } 220 | 221 | /// Adds an animator for the whole view layout. 222 | - (instancetype)layoutAnimator:(UIViewPropertyAnimator *)animator { 223 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 224 | spec.context.layoutAnimator = animator; 225 | }]; 226 | } 227 | 228 | @end 229 | 230 | @implementation CRNodeBuilder (UIControl) 231 | 232 | - (instancetype)enabled:(BOOL)enabled { 233 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 234 | const auto getter = NSSelectorFromString(CR_UNSAFE_KEYPATH(isEnabled)); 235 | if (![spec.view respondsToSelector:getter]) return; 236 | [spec set:CR_UNSAFE_KEYPATH(enabled) value:@(enabled)]; 237 | }]; 238 | } 239 | 240 | - (instancetype)selected:(BOOL)selected { 241 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 242 | const auto getter = NSSelectorFromString(CR_UNSAFE_KEYPATH(isSelected)); 243 | if (![spec.view respondsToSelector:getter]) return; 244 | [spec set:CR_UNSAFE_KEYPATH(selected) value:@(selected)]; 245 | }]; 246 | } 247 | 248 | - (instancetype)highlighted:(BOOL)highlighted { 249 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 250 | const auto getter = NSSelectorFromString(CR_UNSAFE_KEYPATH(isHighlighted)); 251 | if (![spec.view respondsToSelector:getter]) return; 252 | [spec set:CR_UNSAFE_KEYPATH(highlighted) value:@(highlighted)]; 253 | }]; 254 | } 255 | - (instancetype)setTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)events { 256 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 257 | const auto control = CR_DYNAMIC_CAST(UIControl, spec.view); 258 | if (!control) return; 259 | [control removeTarget:nil action:nil forControlEvents:events]; 260 | [control addTarget:target action:action forControlEvents:events]; 261 | }]; 262 | } 263 | 264 | @end 265 | 266 | @implementation CRNodeBuilder (UILabel) 267 | 268 | - (instancetype)text:(nullable NSString *)text { 269 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 270 | const auto button = CR_DYNAMIC_CAST(UIButton, spec.view); 271 | if (button) { 272 | [button setTitle:text forState:UIControlStateNormal]; 273 | } else { 274 | _CRUnsafeSet(spec, CR_UNSAFE_KEYPATH(text), text); 275 | } 276 | }]; 277 | } 278 | 279 | - (instancetype)attributedText:(nullable NSAttributedString *)attributedText { 280 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 281 | const auto button = CR_DYNAMIC_CAST(UIButton, spec.view); 282 | if (button) { 283 | [button setAttributedTitle:attributedText forState:UIControlStateNormal]; 284 | } else { 285 | _CRUnsafeSet(spec, CR_UNSAFE_KEYPATH(attributedText), attributedText); 286 | } 287 | }]; 288 | } 289 | 290 | - (instancetype)font:(UIFont *)font { 291 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 292 | const auto button = CR_DYNAMIC_CAST(UIButton, spec.view); 293 | if (button) { 294 | [spec set:CR_KEYPATH(button, titleLabel.font) value:font]; 295 | } else { 296 | _CRUnsafeSet(spec, CR_UNSAFE_KEYPATH(font), font); 297 | } 298 | }]; 299 | } 300 | 301 | - (instancetype)textColor:(UIColor *)textColor { 302 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 303 | const auto button = CR_DYNAMIC_CAST(UIButton, spec.view); 304 | if (button) { 305 | [button setTitleColor:textColor forState:UIControlStateNormal]; 306 | } else { 307 | _CRUnsafeSet(spec, CR_UNSAFE_KEYPATH(textColor), textColor); 308 | } 309 | }]; 310 | } 311 | 312 | - (instancetype)textAlignment:(NSTextAlignment)textAlignment { 313 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 314 | _CRUnsafeSet(spec, CR_UNSAFE_KEYPATH(textAlignment), @(textAlignment)); 315 | }]; 316 | } 317 | 318 | - (instancetype)lineBreakMode:(NSLineBreakMode)lineBreakMode { 319 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 320 | _CRUnsafeSet(spec, CR_UNSAFE_KEYPATH(lineBreakMode), @(lineBreakMode)); 321 | }]; 322 | } 323 | 324 | - (instancetype)numberOfLines:(NSUInteger)numberOfLines { 325 | return [self withLayoutSpec:^(CRNodeLayoutSpec *spec) { 326 | _CRUnsafeSet(spec, CR_UNSAFE_KEYPATH(numberOfLines), @(numberOfLines)); 327 | }]; 328 | } 329 | 330 | @end 331 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNodeBuilder.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "CRNode.h" 5 | 6 | NS_ASSUME_NONNULL_BEGIN 7 | 8 | NS_SWIFT_NAME(OpaqueNodeBuilder) 9 | @interface CROpaqueNodeBuilder : NSObject 10 | /// Optional reuse identifier. 11 | /// @note: This is required if the node has a custom @c viewInit. 12 | - (instancetype)withReuseIdentifier:(NSString *)reuseIdentifier; 13 | /// Unique node key (required for stateful components). 14 | /// @note: This is required if @c coordinatorType or @c state is set. 15 | - (instancetype)withKey:(NSString *)key; 16 | /// The coordinator assigned to this node. 17 | - (instancetype)withCoordinator:(CRCoordinator *)coordinator; 18 | /// The coordinator type assigned to this node. 19 | - (instancetype)withCoordinatorDescriptor:(CRCoordinatorDescriptor *)descriptor; 20 | /// Defines the node configuration and layout. 21 | - (instancetype)withLayoutSpec:(void (^)(CRNodeLayoutSpec *))layoutSpec; 22 | /// Build the concrete node. 23 | - (CRNode *)build; 24 | @end 25 | 26 | NS_SWIFT_NAME(NullNodeBuilder) 27 | @interface CRNullNodeBuilder : CROpaqueNodeBuilder 28 | /// Build the concrete node. 29 | - (CRNullNode *)build; 30 | @end 31 | 32 | NS_SWIFT_NAME(NodeBuilder) 33 | @interface CRNodeBuilder<__covariant V : UIView *> : CROpaqueNodeBuilder 34 | - (instancetype)init NS_UNAVAILABLE; 35 | /// The view type of the desired @c CRNode. 36 | - (instancetype)initWithType:(Class)type; 37 | /// Custom view initialization code. 38 | - (instancetype)withViewInit:(UIView * (^)(NSString *))viewInit; 39 | /// Defines the node configuration and layout. 40 | - (instancetype)withLayoutSpec:(void (^)(CRNodeLayoutSpec *))layoutSpec; 41 | /// Assign the node children. 42 | - (instancetype)withChildren:(NSArray *)children; 43 | /// Add a child to the node children list. 44 | - (instancetype)addChild:(CRNode *)node; 45 | /// Build the concrete node. 46 | - (CRNode *)build; 47 | @end 48 | 49 | static CRNodeBuilder *CRBuildLeaf(Class type, 50 | void(NS_NOESCAPE ^ configure)(CRNodeBuilder *builder)); 51 | 52 | static CRNodeBuilder *CRBuild(Class type, void(NS_NOESCAPE ^ configure)(CRNodeBuilder *builder), 53 | NSArray *children); 54 | 55 | NS_ASSUME_NONNULL_END 56 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNodeBuilder.mm: -------------------------------------------------------------------------------- 1 | #import "CRNodeBuilder.h" 2 | #import "CRContext.h" 3 | #import "CRCoordinator.h" 4 | #import "CRMacros.h" 5 | 6 | void CRNodeBuilderException(NSString *reason) { 7 | @throw [NSException exceptionWithName:@"NodeBuilderException" reason:reason userInfo:nil]; 8 | } 9 | 10 | static CRNodeBuilder *CRBuildLeaf(Class type, 11 | void(NS_NOESCAPE ^ configure)(CRNodeBuilder *builder)) { 12 | return CRBuild(type, configure, @[]); 13 | } 14 | 15 | static CRNodeBuilder *CRBuild(Class type, void(NS_NOESCAPE ^ configure)(CRNodeBuilder *builder), 16 | NSArray *children) { 17 | const auto builder = [[CRNodeBuilder alloc] initWithType:type]; 18 | CR_FOREACH(node, children) { [builder addChild:[node build]]; } 19 | configure(builder); 20 | return builder; 21 | } 22 | 23 | @implementation CRNullNodeBuilder 24 | 25 | - (CRNullNode *)build { 26 | return CRNullNode.nullNode; 27 | } 28 | 29 | @end 30 | 31 | @implementation CRNodeBuilder { 32 | Class _type; 33 | NSString *_reuseIdentifier; 34 | NSString *_key; 35 | UIView * (^_viewInit)(void); 36 | void (^_layoutSpec)(CRNodeLayoutSpec *); 37 | NSMutableArray *_mutableChildren; 38 | CRCoordinatorDescriptor *_coordinatorDescriptor; 39 | } 40 | 41 | - (instancetype)initWithType:(Class)type { 42 | CR_ASSERT_ON_MAIN_THREAD(); 43 | if (self = [super init]) { 44 | _type = type; 45 | _mutableChildren = @[].mutableCopy; 46 | } 47 | return self; 48 | } 49 | 50 | - (instancetype)withReuseIdentifier:(NSString *)reuseIdentifier { 51 | CR_ASSERT_ON_MAIN_THREAD(); 52 | _reuseIdentifier = reuseIdentifier; 53 | return self; 54 | } 55 | 56 | - (instancetype)withKey:(NSString *)key { 57 | CR_ASSERT_ON_MAIN_THREAD(); 58 | _key = key; 59 | return self; 60 | } 61 | 62 | - (instancetype)withCoordinatorDescriptor:(CRCoordinatorDescriptor *)descriptor { 63 | CR_ASSERT_ON_MAIN_THREAD(); 64 | _coordinatorDescriptor = descriptor; 65 | return self; 66 | } 67 | 68 | - (instancetype)withCoordinator:(CRCoordinator *)coordinator { 69 | CR_ASSERT_ON_MAIN_THREAD(); 70 | const auto descriptor = 71 | [[CRCoordinatorDescriptor alloc] initWithType:coordinator.class key:coordinator.key]; 72 | return [self withCoordinatorDescriptor:descriptor]; 73 | } 74 | 75 | - (instancetype)withViewInit:(UIView * (^)(NSString *))viewInit { 76 | CR_ASSERT_ON_MAIN_THREAD(); 77 | NSString *key = _key; 78 | _viewInit = ^UIView *(void) { return viewInit(key); }; 79 | return self; 80 | } 81 | 82 | - (instancetype)withLayoutSpec:(void (^)(CRNodeLayoutSpec *))layoutSpec { 83 | CR_ASSERT_ON_MAIN_THREAD(); 84 | void (^oldBlock)(CRNodeLayoutSpec *) = [_layoutSpec copy]; 85 | void (^newBlock)(CRNodeLayoutSpec *) = [layoutSpec copy]; 86 | _layoutSpec = [^(CRNodeLayoutSpec *spec) { 87 | if (oldBlock != nil) oldBlock(spec); 88 | if (newBlock != nil) newBlock(spec); 89 | } copy]; 90 | return self; 91 | } 92 | 93 | - (instancetype)withChildren:(NSArray *)children { 94 | CR_ASSERT_ON_MAIN_THREAD(); 95 | _mutableChildren = children.mutableCopy; 96 | CR_FOREACH(child, _mutableChildren) { NSAssert([child isKindOfClass:CRNode.class], @""); } 97 | return self; 98 | } 99 | 100 | - (instancetype)addChild:(CRNode *)node { 101 | CR_ASSERT_ON_MAIN_THREAD(); 102 | [_mutableChildren addObject:node]; 103 | return self; 104 | } 105 | 106 | - (CRNode *)build { 107 | CR_ASSERT_ON_MAIN_THREAD(); 108 | if (_viewInit && !_reuseIdentifier) { 109 | CRNodeBuilderException(@"The node has a custom view initializer but no reuse identifier."); 110 | return CRNullNode.nullNode; 111 | } 112 | const auto node = [[CRNode alloc] initWithType:_type 113 | reuseIdentifier:_reuseIdentifier 114 | key:_key 115 | viewInit:_viewInit 116 | layoutSpec:_layoutSpec]; 117 | if (_coordinatorDescriptor) { 118 | [node bindCoordinator:_coordinatorDescriptor]; 119 | } 120 | [node appendChildren:_mutableChildren]; 121 | return node; 122 | } 123 | 124 | @end 125 | 126 | @implementation CROpaqueNodeBuilder 127 | 128 | - (instancetype)withReuseIdentifier:(NSString *)reuseIdentifier { 129 | NSAssert(NO, @"Called on abstract super class."); 130 | } 131 | 132 | - (instancetype)withKey:(NSString *)key { 133 | NSAssert(NO, @"Called on abstract super class."); 134 | } 135 | 136 | - (instancetype)withLayoutSpec:(void (^)(CRNodeLayoutSpec *))layoutSpec { 137 | NSAssert(NO, @"Called on abstract super class."); 138 | } 139 | 140 | - (instancetype)withCoordinator:(CRCoordinator *)coordinator { 141 | NSAssert(NO, @"Called on abstract super class."); 142 | } 143 | 144 | - (instancetype)withCoordinatorDescriptor:(CRCoordinatorDescriptor *)descriptor { 145 | NSAssert(NO, @"Called on abstract super class."); 146 | } 147 | 148 | - (CRNode *)build { 149 | NSAssert(NO, @"Called on abstract super class."); 150 | } 151 | 152 | @end 153 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNodeHierarchy.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "CRNode.h" 5 | @class CROpaqueNodeBuilder; 6 | 7 | NS_ASSUME_NONNULL_BEGIN 8 | 9 | @class CRContext; 10 | 11 | NS_SWIFT_NAME(NodeHierarchy) 12 | @interface CRNodeHierarchy : NSObject 13 | /// The current root node. 14 | @property(nonatomic, readonly) CRNode *root; 15 | 16 | - (instancetype)init NS_UNAVAILABLE; 17 | 18 | /// Instantiate a new node hierarchy. 19 | - (instancetype)initWithContext:(CRContext *)context 20 | nodeHierarchyBuilder:(CROpaqueNodeBuilder * (^)(CRContext *))buildNodeHierarchy; 21 | 22 | #pragma mark Render 23 | 24 | /// Constructs a new node hierarchy by invoking the @c buildNodeHierarchy block and reconciles it 25 | /// against the view passed as argument. 26 | - (void)buildHierarchyInView:(UIView *)view 27 | constrainedToSize:(CGSize)size 28 | withOptions:(CRNodeLayoutOptions)options; 29 | 30 | /// See @c CRNode.reconcileInView:constrainedToSize:withOptions:. 31 | - (void)reconcileInView:(nullable UIView *)view 32 | constrainedToSize:(CGSize)size 33 | withOptions:(CRNodeLayoutOptions)options; 34 | 35 | /// See @c CRNode.slayoutConstrainedToSize:withOptions:. 36 | - (void)layoutConstrainedToSize:(CGSize)size withOptions:(CRNodeLayoutOptions)options; 37 | 38 | /// Constructs a new node hierarchy by invoking the @c buildNodeHierarchy block and reconciles it 39 | /// against the currently mounted view hierarchy. 40 | - (void)setNeedsReconcile; 41 | 42 | /// Tells the node that the node/view hierarchy must be re-layout. 43 | /// @note This is preferable to @c setNeedsReconcile whenever there's going to be no changes in 44 | /// the view hierarchy, 45 | - (void)setNeedsLayout; 46 | 47 | @end 48 | 49 | NS_ASSUME_NONNULL_END 50 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNodeHierarchy.mm: -------------------------------------------------------------------------------- 1 | #import "CRNodeHierarchy.h" 2 | 3 | #import "CRContext.h" 4 | #import "CRMacros.h" 5 | #include "CRNodeBuilder.h" 6 | 7 | @implementation CRNodeHierarchy { 8 | __weak CRContext *_context; 9 | __weak UIView *_containerView; 10 | CGSize _size; 11 | CRNodeLayoutOptions _options; 12 | CROpaqueNodeBuilder * (^_buildNodeHierarchy)(CRContext *); 13 | } 14 | 15 | - (instancetype)initWithContext:(CRContext *)context 16 | nodeHierarchyBuilder:(CROpaqueNodeBuilder * (^)(CRContext *))buildNodeHierarchy { 17 | if (self = [super init]) { 18 | _context = context; 19 | _buildNodeHierarchy = buildNodeHierarchy; 20 | } 21 | return self; 22 | } 23 | 24 | #pragma mark Render 25 | 26 | - (void)buildHierarchyInView:(UIView *)view 27 | constrainedToSize:(CGSize)size 28 | withOptions:(CRNodeLayoutOptions)options { 29 | CR_ASSERT_ON_MAIN_THREAD(); 30 | _containerView = view; 31 | _size = size; 32 | _options = options; 33 | _root = [_buildNodeHierarchy(_context) build]; 34 | [_root registerNodeHierarchyInContext:_context]; 35 | [_root setNodeHierarchy:self]; 36 | [_root reconcileInView:view constrainedToSize:size withOptions:options]; 37 | } 38 | 39 | - (void)reconcileInView:(nullable UIView *)view 40 | constrainedToSize:(CGSize)size 41 | withOptions:(CRNodeLayoutOptions)options { 42 | CR_ASSERT_ON_MAIN_THREAD(); 43 | _containerView = view; 44 | _size = size; 45 | _options = options; 46 | [_root reconcileInView:view constrainedToSize:size withOptions:options]; 47 | } 48 | 49 | - (void)layoutConstrainedToSize:(CGSize)size withOptions:(CRNodeLayoutOptions)options { 50 | _size = size; 51 | _options = options; 52 | CR_ASSERT_ON_MAIN_THREAD(); 53 | [_root layoutConstrainedToSize:size withOptions:options]; 54 | } 55 | 56 | - (void)setNeedsReconcile { 57 | CR_ASSERT_ON_MAIN_THREAD(); 58 | [self buildHierarchyInView:_containerView constrainedToSize:_size withOptions:_options]; 59 | } 60 | 61 | - (void)setNeedsLayout { 62 | CR_ASSERT_ON_MAIN_THREAD(); 63 | [self layoutConstrainedToSize:_size withOptions:_options]; 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNodeLayoutSpec.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | NS_ASSUME_NONNULL_BEGIN 5 | 6 | @class CRNode; 7 | @class CRContext; 8 | @class CRCoordinator; 9 | 10 | NS_SWIFT_NAME(LayoutSpec) 11 | @interface CRNodeLayoutSpec<__covariant V : UIView *> : NSObject 12 | /// Backing view for this node. 13 | @property(nonatomic, readonly, nullable, weak) V view; 14 | /// The associated node. 15 | @property(nonatomic, readonly, nullable, weak) CRNode *node; 16 | /// The context for this node hierarchy. 17 | @property(nonatomic, readonly, nullable, weak) CRContext *context; 18 | 19 | /// Lays out the view subtree. 20 | /// @note: The layout directives are executed top down and *after* the Yoga layout has been 21 | /// computed (if applicable). 22 | @property(nonatomic, copy, nullable) void (^onLayoutSubviews)(CRNode *, UIView *, CGSize); 23 | /// The boundaries of this node. 24 | @property(nonatomic, readonly) CGSize size; 25 | 26 | - (instancetype)initWithNode:(CRNode *)node constrainedToSize:(CGSize)size; 27 | 28 | - (void)set:(NSString *)keyPath value:(id)value; 29 | - (void)set:(NSString *)keyPath 30 | value:(id)value 31 | animator:(nullable UIViewPropertyAnimator *)animator; 32 | 33 | /// Restore the view to its initial state. 34 | - (void)restore; 35 | 36 | /// Reset all of the view action targets. 37 | /// @note: Applicate to @c UIControl views only. 38 | - (void)resetAllTargets; 39 | 40 | /// Returns the the first coordinator of type @c coordinatorType in the current subtree. 41 | - (nullable __kindof CRCoordinator *)coordinatorOfType:(Class)coordinatorType; 42 | 43 | @end 44 | 45 | NS_SWIFT_NAME(LayoutSpecProperty) 46 | @interface CRNodeLayoutSpecProperty : NSObject 47 | /// The target keyPath in the node view. 48 | @property(nonatomic, readonly) NSString *keyPath; 49 | /// The new value for this property. 50 | @property(nonatomic, readonly) id value; 51 | /// Optional property animator. 52 | @property(nonatomic, readonly, nullable) UIViewPropertyAnimator *animator; 53 | 54 | - (instancetype)initWithKeyPath:(NSString *)keyPath 55 | value:(id)value 56 | animator:(UIViewPropertyAnimator *)animator; 57 | 58 | @end 59 | 60 | NS_ASSUME_NONNULL_END 61 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRNodeLayoutSpec.mm: -------------------------------------------------------------------------------- 1 | #import "CRNodeLayoutSpec.h" 2 | #import "CRContext.h" 3 | #import "CRCoordinator.h" 4 | #import "CRMacros.h" 5 | #import "CRNode.h" 6 | #import "CRNodeBridge.h" 7 | #import "UIView+CRNode.h" 8 | 9 | @implementation CRNodeLayoutSpec { 10 | NSMutableDictionary *_properties; 11 | } 12 | 13 | - (void)set:(NSString *)keyPath value:(id)value { 14 | [self set:keyPath value:value animator:nil]; 15 | } 16 | 17 | - (void)set:(NSString *)keyPath value:(id)value animator:(UIViewPropertyAnimator *)animator { 18 | CR_ASSERT_ON_MAIN_THREAD(); 19 | static Class swiftValueClass; 20 | static dispatch_once_t onceToken; 21 | dispatch_once(&onceToken, ^() { 22 | swiftValueClass = NSClassFromString(@"__SwiftValue"); 23 | }); 24 | if ([value isKindOfClass:swiftValueClass]) { 25 | CR_LOG(@"__SwiftValue passed for key %@. Make sure your enum conforms to " 26 | @"WritableKeyPathBoxableEnum. ", 27 | keyPath); 28 | return; 29 | } 30 | const auto property = [[CRNodeLayoutSpecProperty alloc] initWithKeyPath:keyPath 31 | value:value 32 | animator:animator]; 33 | _properties[keyPath] = property; 34 | [_view.cr_nodeBridge setPropertyWithKeyPath:keyPath value:value animator:animator]; 35 | } 36 | 37 | - (instancetype)initWithNode:(CRNode *)node constrainedToSize:(CGSize)size { 38 | if (self = [super init]) { 39 | _node = node; 40 | _view = node.renderedView; 41 | _context = node.context; 42 | _size = size; 43 | } 44 | return self; 45 | } 46 | 47 | - (__kindof CRCoordinator *)coordinatorOfType:(Class)coordinatorType { 48 | if (![coordinatorType isSubclassOfClass:CRCoordinator.class]) return nil; 49 | 50 | auto coordinator = (CRCoordinator *)nil; 51 | auto node = self.node; 52 | auto context = self.context; 53 | NSAssert(node, @"Called when *node* is nil."); 54 | NSAssert(context, @"Called when *context* is nil."); 55 | while (node) { 56 | if (node.coordinatorDescriptor.type == coordinatorType) { 57 | coordinator = node.coordinator; 58 | break; 59 | } 60 | node = node.parent; 61 | } 62 | return coordinator; 63 | } 64 | 65 | - (void)restore { 66 | [_view.cr_nodeBridge restore]; 67 | } 68 | 69 | - (void)resetAllTargets { 70 | [_view cr_resetAllTargets]; 71 | } 72 | 73 | @end 74 | 75 | @implementation CRNodeLayoutSpecProperty 76 | 77 | - (instancetype)initWithKeyPath:(NSString *)keyPath 78 | value:(id)value 79 | animator:(UIViewPropertyAnimator *)animator { 80 | if (self = [super init]) { 81 | _keyPath = keyPath; 82 | _value = value; 83 | _animator = animator; 84 | } 85 | return self; 86 | } 87 | 88 | @end 89 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/CRUmbrellaHeader.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "CRContext.h" 5 | #import "CRCoordinator.h" 6 | #import "CRMacros.h" 7 | #import "CRNode.h" 8 | #import "CRNodeBridge.h" 9 | #import "CRNodeBuilder.h" 10 | #import "CRNodeHierarchy.h" 11 | #import "CRNodeLayoutSpec.h" 12 | #import "UIView+CRNode.h" 13 | #import "YGLayout.h" 14 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/UIView+CRNode.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "CRNode.h" 5 | 6 | NS_ASSUME_NONNULL_BEGIN 7 | 8 | @class CRNode; 9 | @class CRNodeBridge; 10 | 11 | @interface UIView (CRNode) 12 | /// Whether this view has a node currently associated to it or not. 13 | @property(nonatomic, readonly) BOOL cr_hasNode; 14 | /// Transient node configuration for this view. 15 | @property(nonatomic) CRNodeBridge *cr_nodeBridge; 16 | /// Remove all of the registered targets if this view is a subclass of *UIControl*. 17 | - (void)cr_resetAllTargets; 18 | 19 | - (void)cr_normalizeFrame; 20 | 21 | - (void)cr_adjustContentSizePostLayoutRecursivelyIfNeeded; 22 | 23 | @end 24 | 25 | @interface UIScrollView (CRNode) 26 | /// Set the scroll view content size (if necessary). 27 | - (void)cr_adjustContentSizePostLayout; 28 | 29 | @end 30 | 31 | NS_ASSUME_NONNULL_END 32 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/UIView+CRNode.mm: -------------------------------------------------------------------------------- 1 | #import 2 | #import "CRMacros.h" 3 | #import "CRNode.h" 4 | #import "CRNodeBridge.h" 5 | #import "UIView+CRNode.h" 6 | #import "YGLayout.h" 7 | 8 | @implementation UIView (CRNode) 9 | @dynamic cr_nodeBridge; 10 | 11 | - (BOOL)cr_hasNode { 12 | return self.cr_nodeBridge.node != nil; 13 | } 14 | 15 | - (void)setCr_nodeBridge:(CRNodeBridge *)obj { 16 | objc_setAssociatedObject(self, @selector(cr_nodeBridge), obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 17 | } 18 | 19 | - (CRNodeBridge *)cr_nodeBridge { 20 | auto bridge = 21 | CR_DYNAMIC_CAST(CRNodeBridge, objc_getAssociatedObject(self, @selector(cr_nodeBridge))); 22 | const auto ret = CR_NIL_COALESCING(bridge, [[CRNodeBridge alloc] initWithView:self]); 23 | if (ret != bridge) self.cr_nodeBridge = ret; 24 | return ret; 25 | } 26 | 27 | - (void)cr_resetAllTargets { 28 | CR_ASSERT_ON_MAIN_THREAD(); 29 | const auto control = CR_DYNAMIC_CAST(UIControl, self); 30 | CR_FOREACH(target, control.allTargets) { 31 | [control removeTarget:target action:nil forControlEvents:UIControlEventAllEvents]; 32 | } 33 | } 34 | 35 | - (void)cr_normalizeFrame { 36 | auto rect = self.frame; 37 | rect.origin.x = CR_NORMALIZE(rect.origin.x); 38 | rect.origin.y = CR_NORMALIZE(rect.origin.y); 39 | rect.size.width = CR_NORMALIZE(rect.size.width); 40 | rect.size.height = CR_NORMALIZE(rect.size.height); 41 | self.frame = rect; 42 | } 43 | 44 | - (void)cr_adjustContentSizePostLayoutRecursivelyIfNeeded { 45 | if (!self.cr_hasNode) return; 46 | if ([self isKindOfClass:UIScrollView.class]) { 47 | [(UIScrollView *)self cr_adjustContentSizePostLayout]; 48 | } 49 | CR_FOREACH(subview, self.subviews) { 50 | [subview cr_adjustContentSizePostLayoutRecursivelyIfNeeded]; 51 | } 52 | } 53 | 54 | @end 55 | 56 | @implementation UIScrollView (CRNode) 57 | 58 | - (void)cr_adjustContentSizePostLayout { 59 | if ([self isKindOfClass:UITableView.class]) return; 60 | if ([self isKindOfClass:UICollectionView.class]) return; 61 | dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)); 62 | dispatch_after(time, dispatch_get_main_queue(), ^{ 63 | CGFloat x = 0; 64 | CGFloat y = 0; 65 | CR_FOREACH(subview, self.subviews) { 66 | x = MAX(x, CGRectGetMaxX(subview.frame)); 67 | y = MAX(y, CGRectGetMaxY(subview.frame)); 68 | } 69 | if (self.yoga.flexDirection == YGFlexDirectionColumn || 70 | self.yoga.flexDirection == YGFlexDirectionRowReverse) { 71 | self.contentSize = CGSizeMake(self.contentSize.width, y); 72 | } else { 73 | self.contentSize = CGSizeMake(x, self.contentSize.height); 74 | } 75 | self.scrollEnabled = true; 76 | }); 77 | } 78 | 79 | @end 80 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/YGLayout.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import 12 | #import "Yoga.h" 13 | 14 | static CGSize YGNaNSize = { 15 | .width = YGUndefined, 16 | .height = YGUndefined, 17 | }; 18 | 19 | typedef NS_OPTIONS(NSInteger, YGDimensionFlexibility) { 20 | YGDimensionFlexibilityFlexibleWidth = 1 << 0, 21 | YGDimensionFlexibilityFlexibleHeigth = 1 << 1, 22 | }; 23 | 24 | @interface YGLayout : NSObject 25 | 26 | /** 27 | The property that decides if we should include this view when calculating layout. Defaults totrue. 28 | */ 29 | @property(nonatomic, readwrite, assign, setter=setIncludedInLayout:) BOOL isIncludedInLayout; 30 | 31 | /** 32 | The property that decides during layout/sizing whether or not styling properties should be applied. 33 | Defaults to NO. 34 | */ 35 | @property(nonatomic, readwrite, assign, setter=setEnabled:) BOOL isEnabled; 36 | 37 | @property(nonatomic, readwrite, assign) YGDirection direction; 38 | @property(nonatomic, readwrite, assign) YGFlexDirection flexDirection; 39 | @property(nonatomic, readwrite, assign) YGJustify justifyContent; 40 | @property(nonatomic, readwrite, assign) YGAlign alignContent; 41 | @property(nonatomic, readwrite, assign) YGAlign alignItems; 42 | @property(nonatomic, readwrite, assign) YGAlign alignSelf; 43 | @property(nonatomic, readwrite, assign) YGPositionType position; 44 | @property(nonatomic, readwrite, assign) YGWrap flexWrap; 45 | @property(nonatomic, readwrite, assign) YGOverflow overflow; 46 | @property(nonatomic, readwrite, assign) YGDisplay display; 47 | 48 | @property(nonatomic, readwrite, assign) CGFloat flexGrow; 49 | @property(nonatomic, readwrite, assign) CGFloat flexShrink; 50 | @property(nonatomic, readwrite, assign) CGFloat flexBasis; 51 | 52 | @property(nonatomic, readwrite, assign) CGFloat left; 53 | @property(nonatomic, readwrite, assign) CGFloat top; 54 | @property(nonatomic, readwrite, assign) CGFloat right; 55 | @property(nonatomic, readwrite, assign) CGFloat bottom; 56 | @property(nonatomic, readwrite, assign) CGFloat start; 57 | @property(nonatomic, readwrite, assign) CGFloat end; 58 | 59 | @property(nonatomic, readwrite, assign) CGFloat marginLeft; 60 | @property(nonatomic, readwrite, assign) CGFloat marginTop; 61 | @property(nonatomic, readwrite, assign) CGFloat marginRight; 62 | @property(nonatomic, readwrite, assign) CGFloat marginBottom; 63 | @property(nonatomic, readwrite, assign) CGFloat marginStart; 64 | @property(nonatomic, readwrite, assign) CGFloat marginEnd; 65 | @property(nonatomic, readwrite, assign) CGFloat marginHorizontal; 66 | @property(nonatomic, readwrite, assign) CGFloat marginVertical; 67 | @property(nonatomic, readwrite, assign) CGFloat margin; 68 | 69 | @property(nonatomic, readwrite, assign) CGFloat paddingLeft; 70 | @property(nonatomic, readwrite, assign) CGFloat paddingTop; 71 | @property(nonatomic, readwrite, assign) CGFloat paddingRight; 72 | @property(nonatomic, readwrite, assign) CGFloat paddingBottom; 73 | @property(nonatomic, readwrite, assign) CGFloat paddingStart; 74 | @property(nonatomic, readwrite, assign) CGFloat paddingEnd; 75 | @property(nonatomic, readwrite, assign) CGFloat paddingHorizontal; 76 | @property(nonatomic, readwrite, assign) CGFloat paddingVertical; 77 | @property(nonatomic, readwrite, assign) CGFloat padding; 78 | 79 | @property(nonatomic, readwrite, assign) CGFloat borderLeftWidth; 80 | @property(nonatomic, readwrite, assign) CGFloat borderTopWidth; 81 | @property(nonatomic, readwrite, assign) CGFloat borderRightWidth; 82 | @property(nonatomic, readwrite, assign) CGFloat borderBottomWidth; 83 | @property(nonatomic, readwrite, assign) CGFloat borderStartWidth; 84 | @property(nonatomic, readwrite, assign) CGFloat borderEndWidth; 85 | @property(nonatomic, readwrite, assign) CGFloat borderWidth; 86 | 87 | @property(nonatomic, readwrite, assign) CGFloat width; 88 | @property(nonatomic, readwrite, assign) CGFloat height; 89 | @property(nonatomic, readwrite, assign) CGFloat minWidth; 90 | @property(nonatomic, readwrite, assign) CGFloat minHeight; 91 | @property(nonatomic, readwrite, assign) CGFloat maxWidth; 92 | @property(nonatomic, readwrite, assign) CGFloat maxHeight; 93 | 94 | // Yoga specific properties, not compatible with flexbox specification 95 | @property(nonatomic, readwrite, assign) CGFloat aspectRatio; 96 | 97 | /** 98 | Get the resolved direction of this node. This won't be YGDirectionInherit 99 | */ 100 | @property(nonatomic, readonly, assign) YGDirection resolvedDirection; 101 | 102 | /** 103 | Perform a layout calculation and update the frames of the views in the hierarchy with the results. 104 | If the origin is not preserved, the root view's layout results will applied from {0,0}. 105 | */ 106 | - (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin 107 | NS_SWIFT_NAME(applyLayout(preservingOrigin:)); 108 | 109 | /** 110 | Perform a layout calculation and update the frames of the views in the hierarchy with the results. 111 | If the origin is not preserved, the root view's layout results will applied from {0,0}. 112 | */ 113 | - (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin 114 | dimensionFlexibility:(YGDimensionFlexibility)dimensionFlexibility 115 | NS_SWIFT_NAME(applyLayout(preservingOrigin:dimensionFlexibility:)); 116 | 117 | /** 118 | Returns the size of the view if no constraints were given. This could equivalent to calling [self 119 | sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]; 120 | */ 121 | @property(nonatomic, readonly, assign) CGSize intrinsicSize; 122 | 123 | /** 124 | Returns the number of children that are using Flexbox. 125 | */ 126 | @property(nonatomic, readonly, assign) NSUInteger numberOfChildren; 127 | 128 | /** 129 | Return a BOOL indiciating whether or not we this node contains any subviews that are included in 130 | Yoga's layout. 131 | */ 132 | @property(nonatomic, readonly, assign) BOOL isLeaf; 133 | 134 | /** 135 | Return's a BOOL indicating if a view is dirty. When a node is dirty 136 | it usually indicates that it will be remeasured on the next layout pass. 137 | */ 138 | @property(nonatomic, readonly, assign) BOOL isDirty; 139 | 140 | /** Analogous to flexShrink = 1 and flexGrow = 1 */ 141 | - (void)flex; 142 | 143 | /** 144 | Mark that a view's layout needs to be recalculated. Only works for leaf views. 145 | */ 146 | - (void)markDirty; 147 | 148 | @end 149 | 150 | @interface YGLayout () 151 | /** Reference to the yoga node. */ 152 | @property(nonatomic, assign, nonnull, readonly) YGNodeRef node; 153 | /** Constructs a new layout object associated to the view passed as argument. */ 154 | - (instancetype)initWithView:(UIView *)view; 155 | @end 156 | 157 | // UIView+Yoga 158 | 159 | NS_ASSUME_NONNULL_BEGIN 160 | 161 | typedef void (^YGLayoutConfigurationBlock)(YGLayout *); 162 | 163 | @interface UIView (Yoga) 164 | /** The YGLayout that is attached to this view. It is lazily created. */ 165 | @property(nonatomic, readonly, strong) YGLayout *yoga; 166 | /** Indicates whether or not Yoga is enabled */ 167 | @property(nonatomic, readonly, assign) BOOL isYogaEnabled; 168 | /** 169 | In ObjC land, every time you access `view.yoga.*` you are adding another `objc_msgSend` 170 | to your code. If you plan on making multiple changes to YGLayout, it's more performant 171 | to use this method, which uses a single objc_msgSend call. 172 | */ 173 | - (void)configureLayoutWithBlock:(YGLayoutConfigurationBlock)block 174 | NS_SWIFT_NAME(configureLayout(block:)); 175 | @end 176 | 177 | #pragma mark - Categories 178 | 179 | @interface UIView (YGAdditions) 180 | /// Redirects to 'layer.cornerRadius' 181 | @property(nonatomic, assign) CGFloat cornerRadius; 182 | /// Redirects to 'layer.borderWidth' 183 | @property(nonatomic, assign) CGFloat borderWidth; 184 | /// Redirects to 'layer.borderColor' 185 | @property(nonatomic, strong) UIColor *borderColor; 186 | /// The opacity of the shadow. Defaults to 0. Specifying a value outside the 187 | @property(nonatomic, assign) CGFloat shadowOpacity; 188 | /// The blur radius used to create the shadow. Defaults to 3. 189 | @property(nonatomic, assign) CGFloat shadowRadius; 190 | /// The shadow offset. Defaults to (0, -3) 191 | @property(nonatomic, assign) CGSize shadowOffset; 192 | /// The color of the shadow. Defaults to opaque black. 193 | @property(nonatomic, strong) UIColor *shadowColor; 194 | @end 195 | 196 | @interface UIButton (YGAdditions) 197 | ////Symeetrical to -[UIButton titleForState:] 198 | @property(nonatomic, strong) NSString *text; 199 | @property(nonatomic, strong) NSString *highlightedText; 200 | @property(nonatomic, strong) NSString *selectedText; 201 | @property(nonatomic, strong) NSString *disabledText; 202 | // Symeetrical to -[UIButton titleColorForState:] 203 | @property(nonatomic, strong) UIColor *textColor; 204 | @property(nonatomic, strong) UIColor *highlightedTextColor; 205 | @property(nonatomic, strong) UIColor *selectedTextColor; 206 | @property(nonatomic, strong) UIColor *disabledTextColor; 207 | @property(nonatomic, strong) UIColor *backgroundColorImage; 208 | ////Symmetrical to -[UIButton backgroundImageForState:] 209 | @property(nonatomic, strong) UIImage *backgroundImage; 210 | @property(nonatomic, strong) UIImage *highlightedBackgroundImage; 211 | @property(nonatomic, strong) UIImage *selectedBackgroundImage; 212 | @property(nonatomic, strong) UIImage *disabledBackgroundImage; 213 | // Symmetrical to -[UIButton imageForState:] 214 | @property(nonatomic, strong) UIImage *image; 215 | @property(nonatomic, strong) UIImage *highlightedImage; 216 | @property(nonatomic, strong) UIImage *selectedImage; 217 | @property(nonatomic, strong) UIImage *disabledImage; 218 | @end 219 | 220 | @interface UIImage (YGAdditions) 221 | + (UIImage *)yg_imageWithColor:(UIColor *)color; 222 | + (UIImage *)yg_imageWithColor:(UIColor *)color size:(CGSize)size; 223 | + (UIImage *)yg_imageFromString:(NSString *)string 224 | color:(UIColor *)color 225 | font:(UIFont *)font 226 | size:(CGSize)size; 227 | @end 228 | 229 | @interface UIViewController (YGAdditions) 230 | /** Whether the controller is being modally presented or not. */ 231 | - (BOOL)isModal; 232 | @end 233 | 234 | /** Returns the top-most view controller in the hierarchy. */ 235 | extern UIViewController *UIGetTopmostViewController(void); 236 | 237 | NS_ASSUME_NONNULL_END 238 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/YGLayout.mm: -------------------------------------------------------------------------------- 1 | #import "YGLayout.h" 2 | #import "Yoga.h" 3 | 4 | #define YG_PROPERTY(type, lowercased_name, capitalized_name) \ 5 | -(type)lowercased_name { \ 6 | return YGNodeStyleGet##capitalized_name(self.node); \ 7 | } \ 8 | \ 9 | -(void)set##capitalized_name : (type)lowercased_name { \ 10 | YGNodeStyleSet##capitalized_name(self.node, lowercased_name); \ 11 | } 12 | 13 | #define YG_VALUE_PROPERTY(lowercased_name, capitalized_name) \ 14 | -(CGFloat)lowercased_name { \ 15 | YGValue value = YGNodeStyleGet##capitalized_name(self.node); \ 16 | if (value.unit == YGUnitPoint) { \ 17 | return value.value; \ 18 | } else { \ 19 | return YGUndefined; \ 20 | } \ 21 | } \ 22 | \ 23 | -(void)set##capitalized_name : (CGFloat)lowercased_name { \ 24 | YGNodeStyleSet##capitalized_name(self.node, lowercased_name); \ 25 | } 26 | 27 | #define YG_EDGE_PROPERTY_GETTER(lowercased_name, capitalized_name, property, edge) \ 28 | -(CGFloat)lowercased_name { \ 29 | return YGNodeStyleGet##property(self.node, edge); \ 30 | } 31 | 32 | #define YG_EDGE_PROPERTY_SETTER(lowercased_name, capitalized_name, property, edge) \ 33 | -(void)set##capitalized_name : (CGFloat)lowercased_name { \ 34 | YGNodeStyleSet##property(self.node, edge, lowercased_name); \ 35 | } 36 | 37 | #define YG_EDGE_PROPERTY(lowercased_name, capitalized_name, property, edge) \ 38 | YG_EDGE_PROPERTY_GETTER(lowercased_name, capitalized_name, property, edge) \ 39 | YG_EDGE_PROPERTY_SETTER(lowercased_name, capitalized_name, property, edge) 40 | 41 | #define YG_VALUE_EDGE_PROPERTY_GETTER(objc_lowercased_name, objc_capitalized_name, c_name, edge) \ 42 | -(CGFloat)objc_lowercased_name { \ 43 | YGValue value = YGNodeStyleGet##c_name(self.node, edge); \ 44 | if (value.unit == YGUnitPoint) { \ 45 | return value.value; \ 46 | } else { \ 47 | return YGUndefined; \ 48 | } \ 49 | } 50 | 51 | #define YG_VALUE_EDGE_PROPERTY_SETTER(objc_lowercased_name, objc_capitalized_name, c_name, edge) \ 52 | -(void)set##objc_capitalized_name : (CGFloat)objc_lowercased_name { \ 53 | YGNodeStyleSet##c_name(self.node, edge, objc_lowercased_name); \ 54 | } 55 | 56 | #define YG_VALUE_EDGE_PROPERTY(lowercased_name, capitalized_name, property, edge) \ 57 | YG_VALUE_EDGE_PROPERTY_GETTER(lowercased_name, capitalized_name, property, edge) \ 58 | YG_VALUE_EDGE_PROPERTY_SETTER(lowercased_name, capitalized_name, property, edge) 59 | 60 | #define YG_VALUE_EDGES_PROPERTIES(lowercased_name, capitalized_name) \ 61 | YG_VALUE_EDGE_PROPERTY(lowercased_name##Left, capitalized_name##Left, capitalized_name, \ 62 | YGEdgeLeft) \ 63 | YG_VALUE_EDGE_PROPERTY(lowercased_name##Top, capitalized_name##Top, capitalized_name, YGEdgeTop) \ 64 | YG_VALUE_EDGE_PROPERTY(lowercased_name##Right, capitalized_name##Right, capitalized_name, \ 65 | YGEdgeRight) \ 66 | YG_VALUE_EDGE_PROPERTY(lowercased_name##Bottom, capitalized_name##Bottom, capitalized_name, \ 67 | YGEdgeBottom) \ 68 | YG_VALUE_EDGE_PROPERTY(lowercased_name##Start, capitalized_name##Start, capitalized_name, \ 69 | YGEdgeStart) \ 70 | YG_VALUE_EDGE_PROPERTY(lowercased_name##End, capitalized_name##End, capitalized_name, YGEdgeEnd) \ 71 | YG_VALUE_EDGE_PROPERTY(lowercased_name##Horizontal, capitalized_name##Horizontal, \ 72 | capitalized_name, YGEdgeHorizontal) \ 73 | YG_VALUE_EDGE_PROPERTY(lowercased_name##Vertical, capitalized_name##Vertical, capitalized_name, \ 74 | YGEdgeVertical) \ 75 | YG_VALUE_EDGE_PROPERTY(lowercased_name, capitalized_name, capitalized_name, YGEdgeAll) 76 | 77 | static YGConfigRef globalConfig; 78 | 79 | @interface YGLayout () 80 | 81 | @property(nonatomic, weak, readonly) UIView *view; 82 | 83 | @end 84 | 85 | @implementation YGLayout 86 | 87 | @synthesize isEnabled = _isEnabled; 88 | @synthesize isIncludedInLayout = _isIncludedInLayout; 89 | @synthesize node = _node; 90 | 91 | + (void)initialize { 92 | globalConfig = YGConfigNew(); 93 | YGConfigSetExperimentalFeatureEnabled(globalConfig, YGExperimentalFeatureWebFlexBasis, true); 94 | } 95 | 96 | - (instancetype)initWithView:(UIView *)view { 97 | if (self = [super init]) { 98 | _view = view; 99 | _node = YGNodeNewWithConfig(globalConfig); 100 | YGNodeSetContext(_node, (__bridge void *)view); 101 | _isEnabled = false; 102 | _isIncludedInLayout = true; 103 | } 104 | return self; 105 | } 106 | 107 | - (void)dealloc { 108 | YGNodeFree(self.node); 109 | } 110 | 111 | - (void)flex { 112 | self.flexGrow = 1; 113 | self.flexShrink = 1; 114 | } 115 | 116 | - (BOOL)isDirty { 117 | return YGNodeIsDirty(self.node); 118 | } 119 | 120 | - (void)markDirty { 121 | if (self.isDirty || !self.isLeaf) { 122 | return; 123 | } 124 | // Yoga is not happy if we try to mark a node as "dirty" before we have set 125 | // the measure function. Since we already know that this is a leaf, 126 | // this *should* be fine. Forgive me Hack Gods. 127 | const YGNodeRef node = self.node; 128 | if (YGNodeGetMeasureFunc(node) == nil) { 129 | YGNodeSetMeasureFunc(node, YGMeasureView); 130 | } 131 | YGNodeMarkDirty(node); 132 | } 133 | 134 | - (NSUInteger)numberOfChildren { 135 | return YGNodeGetChildCount(self.node); 136 | } 137 | 138 | - (BOOL)isLeaf { 139 | NSAssert([NSThread isMainThread], @"This method must be called on the main thread."); 140 | if (self.isEnabled) { 141 | for (UIView *subview in self.view.subviews) { 142 | YGLayout *const yoga = subview.yoga; 143 | if (yoga.isEnabled && yoga.isIncludedInLayout) { 144 | return false; 145 | } 146 | } 147 | } 148 | return true; 149 | } 150 | 151 | #pragma mark - Style 152 | 153 | - (YGPositionType)position { 154 | return YGNodeStyleGetPositionType(self.node); 155 | } 156 | 157 | - (void)setPosition:(YGPositionType)position { 158 | YGNodeStyleSetPositionType(self.node, position); 159 | } 160 | 161 | YG_PROPERTY(YGDirection, direction, Direction) 162 | YG_PROPERTY(YGFlexDirection, flexDirection, FlexDirection) 163 | YG_PROPERTY(YGJustify, justifyContent, JustifyContent) 164 | YG_PROPERTY(YGAlign, alignContent, AlignContent) 165 | YG_PROPERTY(YGAlign, alignItems, AlignItems) 166 | YG_PROPERTY(YGAlign, alignSelf, AlignSelf) 167 | YG_PROPERTY(YGWrap, flexWrap, FlexWrap) 168 | YG_PROPERTY(YGOverflow, overflow, Overflow) 169 | YG_PROPERTY(YGDisplay, display, Display) 170 | 171 | YG_PROPERTY(CGFloat, flexGrow, FlexGrow) 172 | YG_PROPERTY(CGFloat, flexShrink, FlexShrink) 173 | YG_VALUE_PROPERTY(flexBasis, FlexBasis) 174 | 175 | YG_VALUE_EDGE_PROPERTY(left, Left, Position, YGEdgeLeft) 176 | YG_VALUE_EDGE_PROPERTY(top, Top, Position, YGEdgeTop) 177 | YG_VALUE_EDGE_PROPERTY(right, Right, Position, YGEdgeRight) 178 | YG_VALUE_EDGE_PROPERTY(bottom, Bottom, Position, YGEdgeBottom) 179 | YG_VALUE_EDGE_PROPERTY(start, Start, Position, YGEdgeStart) 180 | YG_VALUE_EDGE_PROPERTY(end, End, Position, YGEdgeEnd) 181 | YG_VALUE_EDGES_PROPERTIES(margin, Margin) 182 | YG_VALUE_EDGES_PROPERTIES(padding, Padding) 183 | 184 | YG_EDGE_PROPERTY(borderLeftWidth, BorderLeftWidth, Border, YGEdgeLeft) 185 | YG_EDGE_PROPERTY(borderTopWidth, BorderTopWidth, Border, YGEdgeTop) 186 | YG_EDGE_PROPERTY(borderRightWidth, BorderRightWidth, Border, YGEdgeRight) 187 | YG_EDGE_PROPERTY(borderBottomWidth, BorderBottomWidth, Border, YGEdgeBottom) 188 | YG_EDGE_PROPERTY(borderStartWidth, BorderStartWidth, Border, YGEdgeStart) 189 | YG_EDGE_PROPERTY(borderEndWidth, BorderEndWidth, Border, YGEdgeEnd) 190 | YG_EDGE_PROPERTY(borderWidth, BorderWidth, Border, YGEdgeAll) 191 | 192 | YG_VALUE_PROPERTY(width, Width) 193 | YG_VALUE_PROPERTY(height, Height) 194 | YG_VALUE_PROPERTY(minWidth, MinWidth) 195 | YG_VALUE_PROPERTY(minHeight, MinHeight) 196 | YG_VALUE_PROPERTY(maxWidth, MaxWidth) 197 | YG_VALUE_PROPERTY(maxHeight, MaxHeight) 198 | YG_PROPERTY(CGFloat, aspectRatio, AspectRatio) 199 | 200 | #pragma mark - Layout and Sizing 201 | 202 | - (YGDirection)resolvedDirection { 203 | return YGNodeLayoutGetDirection(self.node); 204 | } 205 | 206 | - (void)applyLayout { 207 | [self calculateLayoutWithSize:self.view.bounds.size]; 208 | YGApplyLayoutToViewHierarchy(self.view, NO); 209 | } 210 | 211 | - (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin { 212 | [self calculateLayoutWithSize:self.view.bounds.size]; 213 | YGApplyLayoutToViewHierarchy(self.view, preserveOrigin); 214 | } 215 | 216 | - (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin 217 | dimensionFlexibility:(YGDimensionFlexibility)dimensionFlexibility { 218 | CGSize size = self.view.bounds.size; 219 | if (dimensionFlexibility & YGDimensionFlexibilityFlexibleWidth) { 220 | size.width = YGUndefined; 221 | } 222 | if (dimensionFlexibility & YGDimensionFlexibilityFlexibleHeigth) { 223 | size.height = YGUndefined; 224 | } 225 | [self calculateLayoutWithSize:size]; 226 | YGApplyLayoutToViewHierarchy(self.view, preserveOrigin); 227 | } 228 | 229 | - (CGSize)intrinsicSize { 230 | const CGSize constrainedSize = { 231 | .width = YGUndefined, 232 | .height = YGUndefined, 233 | }; 234 | return [self calculateLayoutWithSize:constrainedSize]; 235 | } 236 | 237 | #pragma mark - Private 238 | 239 | - (CGSize)calculateLayoutWithSize:(CGSize)size { 240 | NSAssert([NSThread isMainThread], @"Yoga calculation must be done on main."); 241 | YGAttachNodesFromViewHierachy(self.view); 242 | const YGNodeRef node = self.node; 243 | YGNodeCalculateLayout(node, size.width, size.height, YGNodeStyleGetDirection(node)); 244 | return (CGSize){ 245 | .width = YGNodeLayoutGetWidth(node), 246 | .height = YGNodeLayoutGetHeight(node), 247 | }; 248 | } 249 | 250 | static YGSize YGMeasureView(YGNodeRef node, float width, YGMeasureMode widthMode, float height, 251 | YGMeasureMode heightMode) { 252 | const CGFloat constrainedWidth = (widthMode == YGMeasureModeUndefined) ? CGFLOAT_MAX : width; 253 | const CGFloat constrainedHeight = (heightMode == YGMeasureModeUndefined) ? CGFLOAT_MAX : height; 254 | UIView *view = (__bridge UIView *)YGNodeGetContext(node); 255 | const CGSize sizeThatFits = [view sizeThatFits:(CGSize){ 256 | .width = constrainedWidth, 257 | .height = constrainedHeight, 258 | }]; 259 | return (YGSize){ 260 | .width = static_cast( 261 | YGSanitizeMeasurement(constrainedWidth, sizeThatFits.width, widthMode)), 262 | .height = static_cast( 263 | YGSanitizeMeasurement(constrainedHeight, sizeThatFits.height, heightMode)), 264 | }; 265 | } 266 | 267 | static CGFloat YGSanitizeMeasurement(CGFloat constrainedSize, CGFloat measuredSize, 268 | YGMeasureMode measureMode) { 269 | CGFloat result; 270 | if (measureMode == YGMeasureModeExactly) { 271 | result = constrainedSize; 272 | } else if (measureMode == YGMeasureModeAtMost) { 273 | result = MIN(constrainedSize, measuredSize); 274 | } else { 275 | result = measuredSize; 276 | } 277 | 278 | return result; 279 | } 280 | 281 | static BOOL YGNodeHasExactSameChildren(const YGNodeRef node, NSArray *subviews) { 282 | if (YGNodeGetChildCount(node) != subviews.count) { 283 | return false; 284 | } 285 | for (int i = 0; i < subviews.count; i++) { 286 | if (YGNodeGetChild(node, i) != subviews[i].yoga.node) { 287 | return false; 288 | } 289 | } 290 | return true; 291 | } 292 | 293 | static void YGAttachNodesFromViewHierachy(UIView *const view) { 294 | YGLayout *const yoga = view.yoga; 295 | const YGNodeRef node = yoga.node; 296 | // Only leaf nodes should have a measure function 297 | if (yoga.isLeaf) { 298 | YGRemoveAllChildren(node); 299 | YGNodeSetMeasureFunc(node, YGMeasureView); 300 | } else { 301 | YGNodeSetMeasureFunc(node, nil); 302 | NSMutableArray *subviewsToInclude = 303 | [[NSMutableArray alloc] initWithCapacity:view.subviews.count]; 304 | for (UIView *subview in view.subviews) { 305 | if (subview.yoga.isIncludedInLayout) { 306 | [subviewsToInclude addObject:subview]; 307 | } 308 | } 309 | if (!YGNodeHasExactSameChildren(node, subviewsToInclude)) { 310 | YGRemoveAllChildren(node); 311 | for (int i = 0; i < subviewsToInclude.count; i++) { 312 | YGNodeInsertChild(node, subviewsToInclude[i].yoga.node, i); 313 | } 314 | } 315 | for (UIView *const subview in subviewsToInclude) { 316 | YGAttachNodesFromViewHierachy(subview); 317 | } 318 | } 319 | } 320 | 321 | static void YGRemoveAllChildren(const YGNodeRef node) { 322 | if (node == nil) { 323 | return; 324 | } 325 | while (YGNodeGetChildCount(node) > 0) { 326 | YGNodeRemoveChild(node, YGNodeGetChild(node, YGNodeGetChildCount(node) - 1)); 327 | } 328 | } 329 | 330 | static CGFloat YGRoundPixelValue(CGFloat value) { 331 | static CGFloat scale; 332 | static dispatch_once_t onceToken; 333 | dispatch_once(&onceToken, ^() { 334 | scale = [UIScreen mainScreen].scale; 335 | }); 336 | return roundf(value * scale) / scale; 337 | } 338 | 339 | static void YGApplyLayoutToViewHierarchy(UIView *view, BOOL preserveOrigin) { 340 | NSCAssert([NSThread isMainThread], @"Framesetting should only be done on the main thread."); 341 | const YGLayout *yoga = view.yoga; 342 | if (!yoga.isIncludedInLayout) { 343 | return; 344 | } 345 | YGNodeRef node = yoga.node; 346 | const CGPoint topLeft = { 347 | YGNodeLayoutGetLeft(node), 348 | YGNodeLayoutGetTop(node), 349 | }; 350 | const CGPoint bottomRight = { 351 | topLeft.x + YGNodeLayoutGetWidth(node), 352 | topLeft.y + YGNodeLayoutGetHeight(node), 353 | }; 354 | const CGPoint origin = preserveOrigin ? view.frame.origin : CGPointZero; 355 | view.frame = (CGRect){ 356 | .origin = 357 | { 358 | .x = YGRoundPixelValue(topLeft.x + origin.x), 359 | .y = YGRoundPixelValue(topLeft.y + origin.y), 360 | }, 361 | .size = 362 | { 363 | .width = YGRoundPixelValue(bottomRight.x) - YGRoundPixelValue(topLeft.x), 364 | .height = YGRoundPixelValue(bottomRight.y) - YGRoundPixelValue(topLeft.y), 365 | }, 366 | }; 367 | if (!yoga.isLeaf) { 368 | for (NSUInteger i = 0; i < view.subviews.count; i++) { 369 | YGApplyLayoutToViewHierarchy(view.subviews[i], NO); 370 | } 371 | } 372 | } 373 | 374 | @end 375 | 376 | // UIView+Yoga 377 | 378 | #import 379 | 380 | static const void *kYGYogaAssociatedKey = &kYGYogaAssociatedKey; 381 | 382 | @implementation UIView (YogaKit) 383 | 384 | - (YGLayout *)yoga { 385 | YGLayout *yoga = objc_getAssociatedObject(self, kYGYogaAssociatedKey); 386 | if (!yoga) { 387 | yoga = [[YGLayout alloc] initWithView:self]; 388 | objc_setAssociatedObject(self, kYGYogaAssociatedKey, yoga, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 389 | } 390 | return yoga; 391 | } 392 | 393 | - (BOOL)isYogaEnabled { 394 | return objc_getAssociatedObject(self, kYGYogaAssociatedKey) != nil; 395 | } 396 | 397 | - (void)configureLayoutWithBlock:(YGLayoutConfigurationBlock)block { 398 | if (block) { 399 | block(self.yoga); 400 | } 401 | } 402 | 403 | @end 404 | 405 | #pragma mark - Categories 406 | 407 | @implementation UIView (YGAdditions) 408 | 409 | - (CGFloat)cornerRadius { 410 | return self.layer.cornerRadius; 411 | } 412 | 413 | - (void)setCornerRadius:(CGFloat)cornerRadius { 414 | self.clipsToBounds = true; 415 | self.layer.cornerRadius = cornerRadius; 416 | } 417 | 418 | - (CGFloat)borderWidth { 419 | return self.layer.borderWidth; 420 | } 421 | 422 | - (void)setBorderWidth:(CGFloat)borderWidth { 423 | self.layer.borderWidth = borderWidth; 424 | } 425 | 426 | - (UIColor *)borderColor { 427 | return [UIColor colorWithCGColor:self.layer.borderColor]; 428 | } 429 | 430 | - (void)setBorderColor:(UIColor *)borderColor { 431 | self.layer.borderColor = borderColor.CGColor; 432 | } 433 | 434 | - (CGFloat)shadowOpacity { 435 | return self.layer.shadowOpacity; 436 | } 437 | 438 | - (void)setShadowOpacity:(CGFloat)shadowOpacity { 439 | self.layer.shadowOpacity = shadowOpacity; 440 | } 441 | 442 | - (CGFloat)shadowRadius { 443 | return self.layer.shadowRadius; 444 | } 445 | 446 | - (void)setShadowRadius:(CGFloat)shadowRadius { 447 | self.layer.shadowRadius = shadowRadius; 448 | } 449 | 450 | - (CGSize)shadowOffset { 451 | return self.layer.shadowOffset; 452 | } 453 | 454 | - (void)setShadowOffset:(CGSize)shadowOffset { 455 | self.layer.shadowOffset = shadowOffset; 456 | } 457 | 458 | - (UIColor *)shadowColor { 459 | return [UIColor colorWithCGColor:self.layer.shadowColor]; 460 | } 461 | 462 | - (void)setShadowColor:(UIColor *)shadowColor { 463 | self.layer.shadowColor = shadowColor.CGColor; 464 | } 465 | 466 | @end 467 | 468 | #pragma mark - UIButton 469 | 470 | @implementation UIButton (YGAdditions) 471 | 472 | - (NSString *)text { 473 | return [self titleForState:UIControlStateNormal]; 474 | } 475 | 476 | - (void)setText:(NSString *)text { 477 | [self setTitle:text forState:UIControlStateNormal]; 478 | } 479 | 480 | - (NSString *)highlightedText { 481 | return [self titleForState:UIControlStateHighlighted]; 482 | } 483 | 484 | - (void)setHighlightedText:(NSString *)highlightedText { 485 | [self setTitle:highlightedText forState:UIControlStateHighlighted]; 486 | } 487 | 488 | - (NSString *)selectedText { 489 | return [self titleForState:UIControlStateSelected]; 490 | } 491 | 492 | - (void)setSelectedText:(NSString *)selectedText { 493 | [self setTitle:selectedText forState:UIControlStateSelected]; 494 | } 495 | 496 | - (NSString *)disabledText { 497 | return [self titleForState:UIControlStateDisabled]; 498 | } 499 | 500 | - (void)setDisabledText:(NSString *)disabledText { 501 | [self setTitle:disabledText forState:UIControlStateDisabled]; 502 | } 503 | 504 | - (UIColor *)textColor { 505 | return [self titleColorForState:UIControlStateNormal]; 506 | } 507 | 508 | - (void)setTextColor:(UIColor *)textColor { 509 | [self setTitleColor:textColor forState:UIControlStateNormal]; 510 | } 511 | 512 | - (UIColor *)highlightedTextColor { 513 | return [self titleColorForState:UIControlStateHighlighted]; 514 | } 515 | 516 | - (void)setHighlightedTextColor:(UIColor *)highlightedTextColor { 517 | [self setTitleColor:highlightedTextColor forState:UIControlStateHighlighted]; 518 | } 519 | 520 | - (UIColor *)selectedTextColor { 521 | return [self titleColorForState:UIControlStateSelected]; 522 | } 523 | 524 | - (void)setSelectedTextColor:(UIColor *)selectedTextColor { 525 | [self setTitleColor:selectedTextColor forState:UIControlStateSelected]; 526 | } 527 | 528 | - (UIColor *)disabledTextColor { 529 | return [self titleColorForState:UIControlStateDisabled]; 530 | } 531 | 532 | - (void)setDisabledTextColor:(UIColor *)disabledTextColor { 533 | [self setTitleColor:disabledTextColor forState:UIControlStateDisabled]; 534 | } 535 | 536 | - (UIColor *)backgroundColorImage { 537 | return nil; 538 | } 539 | 540 | - (void)setBackgroundColorImage:(UIColor *)backgroundColor { 541 | UIImage *image = [UIImage yg_imageWithColor:backgroundColor]; 542 | self.backgroundImage = image; 543 | } 544 | 545 | - (UIImage *)backgroundImage { 546 | return [self backgroundImageForState:UIControlStateNormal]; 547 | } 548 | 549 | - (void)setBackgroundImage:(UIImage *)backgroundImage { 550 | [self setBackgroundImage:backgroundImage forState:UIControlStateNormal]; 551 | } 552 | 553 | - (UIImage *)highlightedBackgroundImage { 554 | return [self backgroundImageForState:UIControlStateHighlighted]; 555 | } 556 | 557 | - (void)setHighlightedBackgroundImage:(UIImage *)highlightedBackgroundImage { 558 | [self setBackgroundImage:highlightedBackgroundImage forState:UIControlStateHighlighted]; 559 | } 560 | 561 | - (UIImage *)selectedBackgroundImage { 562 | return [self backgroundImageForState:UIControlStateSelected]; 563 | } 564 | 565 | - (void)setSelectedBackgroundImage:(UIImage *)selectedBackgroundImage { 566 | [self setBackgroundImage:selectedBackgroundImage forState:UIControlStateSelected]; 567 | } 568 | 569 | - (UIImage *)disabledBackgroundImage { 570 | return [self backgroundImageForState:UIControlStateDisabled]; 571 | } 572 | 573 | - (void)setDisabledBackgroundImage:(UIImage *)disabledBackgroundImage { 574 | [self setBackgroundImage:disabledBackgroundImage forState:UIControlStateDisabled]; 575 | } 576 | 577 | - (UIImage *)image { 578 | return [self imageForState:UIControlStateNormal]; 579 | } 580 | 581 | - (void)setImage:(UIImage *)image { 582 | [self setImage:image forState:UIControlStateNormal]; 583 | } 584 | 585 | - (UIImage *)highlightedImage { 586 | return [self imageForState:UIControlStateHighlighted]; 587 | } 588 | 589 | - (void)setHighlightedImage:(UIImage *)highlightedImage { 590 | [self setImage:highlightedImage forState:UIControlStateHighlighted]; 591 | } 592 | 593 | - (UIImage *)selectedImage { 594 | return [self imageForState:UIControlStateSelected]; 595 | } 596 | 597 | - (void)setSelectedImage:(UIImage *)selectedImage { 598 | [self setImage:selectedImage forState:UIControlStateSelected]; 599 | } 600 | 601 | - (UIImage *)disabledImage { 602 | return [self imageForState:UIControlStateDisabled]; 603 | } 604 | 605 | - (void)setDisabledImage:(UIImage *)disabledImage { 606 | [self setImage:disabledImage forState:UIControlStateDisabled]; 607 | } 608 | 609 | @end 610 | 611 | #pragma mark - UIImage 612 | 613 | @implementation UIImage (YGAdditions) 614 | 615 | + (UIImage *)yg_imageWithColor:(UIColor *)color { 616 | return [self yg_imageWithColor:color size:(CGSize){1, 1}]; 617 | } 618 | 619 | + (UIImage *)yg_imageWithColor:(UIColor *)color size:(CGSize)size { 620 | CGRect rect = (CGRect){CGPointZero, size}; 621 | UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale); 622 | CGContextRef context = UIGraphicsGetCurrentContext(); 623 | CGContextSetFillColorWithColor(context, [color CGColor]); 624 | CGContextFillRect(context, rect); 625 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 626 | UIGraphicsEndImageContext(); 627 | return image; 628 | } 629 | 630 | + (UIImage *)yg_imageFromString:(NSString *)string 631 | color:(UIColor *)color 632 | font:(UIFont *)font 633 | size:(CGSize)size { 634 | UIGraphicsBeginImageContextWithOptions(size, NO, 0); 635 | 636 | NSDictionary *attributes = @{NSFontAttributeName : font, NSForegroundColorAttributeName : color}; 637 | [string drawInRect:CGRectMake(0, 0, size.width, size.height) withAttributes:attributes]; 638 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 639 | UIGraphicsEndImageContext(); 640 | 641 | return image; 642 | } 643 | 644 | @end 645 | 646 | @implementation UIViewController (YGAdditions) 647 | 648 | - (BOOL)isModal { 649 | if ([self presentingViewController]) return true; 650 | if ([[[self navigationController] presentingViewController] presentedViewController] == 651 | [self navigationController]) 652 | return true; 653 | if ([[[self tabBarController] presentingViewController] isKindOfClass:[UITabBarController class]]) 654 | return true; 655 | return false; 656 | } 657 | 658 | @end 659 | 660 | UIViewController *_Nullable UIGetTopmostViewController() { 661 | UIViewController *baseVC = UIApplication.sharedApplication.keyWindow.rootViewController; 662 | if ([baseVC isKindOfClass:[UINavigationController class]]) { 663 | return ((UINavigationController *)baseVC).visibleViewController; 664 | } 665 | if ([baseVC isKindOfClass:[UITabBarController class]]) { 666 | UIViewController *selectedTVC = ((UITabBarController *)baseVC).selectedViewController; 667 | if (selectedTVC) { 668 | return selectedTVC; 669 | } 670 | } 671 | if (baseVC.presentedViewController) { 672 | return baseVC.presentedViewController; 673 | } 674 | return baseVC; 675 | } 676 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/Yoga.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #pragma once 11 | 12 | #ifdef __cplusplus 13 | #define YG_EXTERN_C_BEGIN extern "C" { 14 | #define YG_EXTERN_C_END } 15 | #else 16 | #define YG_EXTERN_C_BEGIN 17 | #define YG_EXTERN_C_END 18 | #endif 19 | 20 | #ifdef _WINDLL 21 | #define WIN_EXPORT __declspec(dllexport) 22 | #else 23 | #define WIN_EXPORT 24 | #endif 25 | 26 | #ifdef WINARMDLL 27 | #define WIN_STRUCT(type) type * 28 | #define WIN_STRUCT_REF(value) &value 29 | #else 30 | #define WIN_STRUCT(type) type 31 | #define WIN_STRUCT_REF(value) value 32 | #endif 33 | 34 | #ifndef FB_ASSERTIONS_ENABLED 35 | #define FB_ASSERTIONS_ENABLED 1 36 | #endif 37 | 38 | #ifdef NS_ENUM 39 | // Cannot use NSInteger as NSInteger has a different size than int (which is the default type of a 40 | // enum). 41 | // Therefor when linking the Yoga C library into obj-c the header is a missmatch for the Yoga ABI. 42 | #define YG_ENUM_BEGIN(name) NS_ENUM(int, name) 43 | #define YG_ENUM_END(name) 44 | #else 45 | #define YG_ENUM_BEGIN(name) enum name 46 | #define YG_ENUM_END(name) name 47 | #endif 48 | 49 | #pragma once 50 | 51 | YG_EXTERN_C_BEGIN 52 | 53 | #define YGAlignCount 8 54 | typedef YG_ENUM_BEGIN(YGAlign){ 55 | YGAlignAuto, YGAlignFlexStart, YGAlignCenter, YGAlignFlexEnd, 56 | YGAlignStretch, YGAlignBaseline, YGAlignSpaceBetween, YGAlignSpaceAround, 57 | } YG_ENUM_END(YGAlign); 58 | WIN_EXPORT const char *YGAlignToString(const YGAlign value); 59 | 60 | #define YGDimensionCount 2 61 | typedef YG_ENUM_BEGIN(YGDimension){ 62 | YGDimensionWidth, 63 | YGDimensionHeight, 64 | } YG_ENUM_END(YGDimension); 65 | WIN_EXPORT const char *YGDimensionToString(const YGDimension value); 66 | 67 | #define YGDirectionCount 3 68 | typedef YG_ENUM_BEGIN(YGDirection){ 69 | YGDirectionInherit, 70 | YGDirectionLTR, 71 | YGDirectionRTL, 72 | } YG_ENUM_END(YGDirection); 73 | WIN_EXPORT const char *YGDirectionToString(const YGDirection value); 74 | 75 | #define YGDisplayCount 2 76 | typedef YG_ENUM_BEGIN(YGDisplay){ 77 | YGDisplayFlex, 78 | YGDisplayNone, 79 | } YG_ENUM_END(YGDisplay); 80 | WIN_EXPORT const char *YGDisplayToString(const YGDisplay value); 81 | 82 | #define YGEdgeCount 9 83 | typedef YG_ENUM_BEGIN(YGEdge){ 84 | YGEdgeLeft, YGEdgeTop, YGEdgeRight, YGEdgeBottom, YGEdgeStart, 85 | YGEdgeEnd, YGEdgeHorizontal, YGEdgeVertical, YGEdgeAll, 86 | } YG_ENUM_END(YGEdge); 87 | WIN_EXPORT const char *YGEdgeToString(const YGEdge value); 88 | 89 | #define YGExperimentalFeatureCount 1 90 | typedef YG_ENUM_BEGIN(YGExperimentalFeature){ 91 | YGExperimentalFeatureWebFlexBasis, 92 | } YG_ENUM_END(YGExperimentalFeature); 93 | WIN_EXPORT const char *YGExperimentalFeatureToString(const YGExperimentalFeature value); 94 | 95 | #define YGFlexDirectionCount 4 96 | typedef YG_ENUM_BEGIN(YGFlexDirection){ 97 | YGFlexDirectionColumn, 98 | YGFlexDirectionColumnReverse, 99 | YGFlexDirectionRow, 100 | YGFlexDirectionRowReverse, 101 | } YG_ENUM_END(YGFlexDirection); 102 | WIN_EXPORT const char *YGFlexDirectionToString(const YGFlexDirection value); 103 | 104 | #define YGJustifyCount 5 105 | typedef YG_ENUM_BEGIN(YGJustify){ 106 | YGJustifyFlexStart, YGJustifyCenter, YGJustifyFlexEnd, 107 | YGJustifySpaceBetween, YGJustifySpaceAround, 108 | } YG_ENUM_END(YGJustify); 109 | WIN_EXPORT const char *YGJustifyToString(const YGJustify value); 110 | 111 | #define YGLogLevelCount 6 112 | typedef YG_ENUM_BEGIN(YGLogLevel){ 113 | YGLogLevelError, YGLogLevelWarn, YGLogLevelInfo, 114 | YGLogLevelDebug, YGLogLevelVerbose, YGLogLevelFatal, 115 | } YG_ENUM_END(YGLogLevel); 116 | WIN_EXPORT const char *YGLogLevelToString(const YGLogLevel value); 117 | 118 | #define YGMeasureModeCount 3 119 | typedef YG_ENUM_BEGIN(YGMeasureMode){ 120 | YGMeasureModeUndefined, 121 | YGMeasureModeExactly, 122 | YGMeasureModeAtMost, 123 | } YG_ENUM_END(YGMeasureMode); 124 | WIN_EXPORT const char *YGMeasureModeToString(const YGMeasureMode value); 125 | 126 | #define YGNodeTypeCount 2 127 | typedef YG_ENUM_BEGIN(YGNodeType){ 128 | YGNodeTypeDefault, 129 | YGNodeTypeText, 130 | } YG_ENUM_END(YGNodeType); 131 | WIN_EXPORT const char *YGNodeTypeToString(const YGNodeType value); 132 | 133 | #define YGOverflowCount 3 134 | typedef YG_ENUM_BEGIN(YGOverflow){ 135 | YGOverflowVisible, 136 | YGOverflowHidden, 137 | YGOverflowScroll, 138 | } YG_ENUM_END(YGOverflow); 139 | WIN_EXPORT const char *YGOverflowToString(const YGOverflow value); 140 | 141 | #define YGPositionTypeCount 2 142 | typedef YG_ENUM_BEGIN(YGPositionType){ 143 | YGPositionTypeRelative, 144 | YGPositionTypeAbsolute, 145 | } YG_ENUM_END(YGPositionType); 146 | WIN_EXPORT const char *YGPositionTypeToString(const YGPositionType value); 147 | 148 | #define YGPrintOptionsCount 3 149 | typedef YG_ENUM_BEGIN(YGPrintOptions){ 150 | YGPrintOptionsLayout = 1, 151 | YGPrintOptionsStyle = 2, 152 | YGPrintOptionsChildren = 4, 153 | } YG_ENUM_END(YGPrintOptions); 154 | WIN_EXPORT const char *YGPrintOptionsToString(const YGPrintOptions value); 155 | 156 | #define YGUnitCount 4 157 | typedef YG_ENUM_BEGIN(YGUnit){ 158 | YGUnitUndefined, 159 | YGUnitPoint, 160 | YGUnitPercent, 161 | YGUnitAuto, 162 | } YG_ENUM_END(YGUnit); 163 | WIN_EXPORT const char *YGUnitToString(const YGUnit value); 164 | 165 | #define YGWrapCount 3 166 | typedef YG_ENUM_BEGIN(YGWrap){ 167 | YGWrapNoWrap, 168 | YGWrapWrap, 169 | YGWrapWrapReverse, 170 | } YG_ENUM_END(YGWrap); 171 | WIN_EXPORT const char *YGWrapToString(const YGWrap value); 172 | 173 | YG_EXTERN_C_END 174 | 175 | #pragma once 176 | 177 | #include 178 | #include 179 | #include 180 | #include 181 | #include 182 | #include 183 | 184 | #ifndef __cplusplus 185 | #include 186 | #endif 187 | 188 | // Not defined in MSVC++ 189 | #ifndef NAN 190 | static const unsigned long __nan[2] = {0xffffffff, 0x7fffffff}; 191 | #define NAN (*(const float *)__nan) 192 | #endif 193 | 194 | #define YGUndefined NAN 195 | 196 | YG_EXTERN_C_BEGIN 197 | 198 | typedef struct YGSize { 199 | float width; 200 | float height; 201 | } YGSize; 202 | 203 | typedef struct YGValue { 204 | float value; 205 | YGUnit unit; 206 | } YGValue; 207 | 208 | typedef struct __attribute__((objc_boxable)) YGValue YGValue; 209 | 210 | static const YGValue YGValueUndefined = {YGUndefined, YGUnitUndefined}; 211 | static const YGValue YGValueAuto = {YGUndefined, YGUnitAuto}; 212 | 213 | typedef struct YGConfig *YGConfigRef; 214 | typedef struct YGNode *YGNodeRef; 215 | typedef YGSize (*YGMeasureFunc)(YGNodeRef node, float width, YGMeasureMode widthMode, float height, 216 | YGMeasureMode heightMode); 217 | typedef float (*YGBaselineFunc)(YGNodeRef node, const float width, const float height); 218 | typedef void (*YGPrintFunc)(YGNodeRef node); 219 | typedef int (*YGLogger)(const YGConfigRef config, const YGNodeRef node, YGLogLevel level, 220 | const char *format, va_list args); 221 | typedef void (*YGNodeClonedFunc)(YGNodeRef oldNode, YGNodeRef newNode, YGNodeRef parent, 222 | int childIndex); 223 | 224 | typedef void *(*YGMalloc)(size_t size); 225 | typedef void *(*YGCalloc)(size_t count, size_t size); 226 | typedef void *(*YGRealloc)(void *ptr, size_t size); 227 | typedef void (*YGFree)(void *ptr); 228 | 229 | // YGNode 230 | WIN_EXPORT YGNodeRef YGNodeNew(void); 231 | WIN_EXPORT YGNodeRef YGNodeNewWithConfig(const YGConfigRef config); 232 | WIN_EXPORT YGNodeRef YGNodeClone(const YGNodeRef node); 233 | WIN_EXPORT void YGNodeFree(const YGNodeRef node); 234 | WIN_EXPORT void YGNodeFreeRecursive(const YGNodeRef node); 235 | WIN_EXPORT void YGNodeReset(const YGNodeRef node); 236 | WIN_EXPORT int32_t YGNodeGetInstanceCount(void); 237 | 238 | WIN_EXPORT void YGNodeInsertChild(const YGNodeRef node, const YGNodeRef child, 239 | const uint32_t index); 240 | WIN_EXPORT void YGNodeRemoveChild(const YGNodeRef node, const YGNodeRef child); 241 | WIN_EXPORT void YGNodeRemoveAllChildren(const YGNodeRef node); 242 | WIN_EXPORT YGNodeRef YGNodeGetChild(const YGNodeRef node, const uint32_t index); 243 | WIN_EXPORT YGNodeRef YGNodeGetParent(const YGNodeRef node); 244 | WIN_EXPORT uint32_t YGNodeGetChildCount(const YGNodeRef node); 245 | 246 | WIN_EXPORT void YGNodeCalculateLayout(const YGNodeRef node, const float availableWidth, 247 | const float availableHeight, 248 | const YGDirection parentDirection); 249 | 250 | // Mark a node as dirty. Only valid for nodes with a custom measure function 251 | // set. 252 | // YG knows when to mark all other nodes as dirty but because nodes with 253 | // measure functions 254 | // depends on information not known to YG they must perform this dirty 255 | // marking manually. 256 | WIN_EXPORT void YGNodeMarkDirty(const YGNodeRef node); 257 | WIN_EXPORT bool YGNodeIsDirty(const YGNodeRef node); 258 | 259 | WIN_EXPORT void YGNodePrint(const YGNodeRef node, const YGPrintOptions options); 260 | 261 | WIN_EXPORT bool YGFloatIsUndefined(const float value); 262 | 263 | WIN_EXPORT bool YGNodeCanUseCachedMeasurement(const YGMeasureMode widthMode, const float width, 264 | const YGMeasureMode heightMode, const float height, 265 | const YGMeasureMode lastWidthMode, 266 | const float lastWidth, 267 | const YGMeasureMode lastHeightMode, 268 | const float lastHeight, const float lastComputedWidth, 269 | const float lastComputedHeight, const float marginRow, 270 | const float marginColumn, const YGConfigRef config); 271 | 272 | WIN_EXPORT void YGNodeCopyStyle(const YGNodeRef dstNode, const YGNodeRef srcNode); 273 | 274 | #define YG_NODE_PROPERTY(type, name, paramName) \ 275 | WIN_EXPORT void YGNodeSet##name(const YGNodeRef node, type paramName); \ 276 | WIN_EXPORT type YGNodeGet##name(const YGNodeRef node); 277 | 278 | #define YG_NODE_STYLE_PROPERTY(type, name, paramName) \ 279 | WIN_EXPORT void YGNodeStyleSet##name(const YGNodeRef node, const type paramName); \ 280 | WIN_EXPORT type YGNodeStyleGet##name(const YGNodeRef node); 281 | 282 | #define YG_NODE_STYLE_PROPERTY_UNIT(type, name, paramName) \ 283 | WIN_EXPORT void YGNodeStyleSet##name(const YGNodeRef node, const float paramName); \ 284 | WIN_EXPORT void YGNodeStyleSet##name##Percent(const YGNodeRef node, const float paramName); \ 285 | WIN_EXPORT type YGNodeStyleGet##name(const YGNodeRef node); 286 | 287 | #define YG_NODE_STYLE_PROPERTY_UNIT_AUTO(type, name, paramName) \ 288 | YG_NODE_STYLE_PROPERTY_UNIT(type, name, paramName) \ 289 | WIN_EXPORT void YGNodeStyleSet##name##Auto(const YGNodeRef node); 290 | 291 | #define YG_NODE_STYLE_EDGE_PROPERTY(type, name, paramName) \ 292 | WIN_EXPORT void YGNodeStyleSet##name(const YGNodeRef node, const YGEdge edge, \ 293 | const type paramName); \ 294 | WIN_EXPORT type YGNodeStyleGet##name(const YGNodeRef node, const YGEdge edge); 295 | 296 | #define YG_NODE_STYLE_EDGE_PROPERTY_UNIT(type, name, paramName) \ 297 | WIN_EXPORT void YGNodeStyleSet##name(const YGNodeRef node, const YGEdge edge, \ 298 | const float paramName); \ 299 | WIN_EXPORT void YGNodeStyleSet##name##Percent(const YGNodeRef node, const YGEdge edge, \ 300 | const float paramName); \ 301 | WIN_EXPORT WIN_STRUCT(type) YGNodeStyleGet##name(const YGNodeRef node, const YGEdge edge); 302 | 303 | #define YG_NODE_STYLE_EDGE_PROPERTY_UNIT_AUTO(type, name) \ 304 | WIN_EXPORT void YGNodeStyleSet##name##Auto(const YGNodeRef node, const YGEdge edge); 305 | 306 | #define YG_NODE_LAYOUT_PROPERTY(type, name) \ 307 | WIN_EXPORT type YGNodeLayoutGet##name(const YGNodeRef node); 308 | 309 | #define YG_NODE_LAYOUT_EDGE_PROPERTY(type, name) \ 310 | WIN_EXPORT type YGNodeLayoutGet##name(const YGNodeRef node, const YGEdge edge); 311 | 312 | YG_NODE_PROPERTY(void *, Context, context); 313 | YG_NODE_PROPERTY(YGMeasureFunc, MeasureFunc, measureFunc); 314 | YG_NODE_PROPERTY(YGBaselineFunc, BaselineFunc, baselineFunc) 315 | YG_NODE_PROPERTY(YGPrintFunc, PrintFunc, printFunc); 316 | YG_NODE_PROPERTY(bool, HasNewLayout, hasNewLayout); 317 | YG_NODE_PROPERTY(YGNodeType, NodeType, nodeType); 318 | 319 | YG_NODE_STYLE_PROPERTY(YGDirection, Direction, direction); 320 | YG_NODE_STYLE_PROPERTY(YGFlexDirection, FlexDirection, flexDirection); 321 | YG_NODE_STYLE_PROPERTY(YGJustify, JustifyContent, justifyContent); 322 | YG_NODE_STYLE_PROPERTY(YGAlign, AlignContent, alignContent); 323 | YG_NODE_STYLE_PROPERTY(YGAlign, AlignItems, alignItems); 324 | YG_NODE_STYLE_PROPERTY(YGAlign, AlignSelf, alignSelf); 325 | YG_NODE_STYLE_PROPERTY(YGPositionType, PositionType, positionType); 326 | YG_NODE_STYLE_PROPERTY(YGWrap, FlexWrap, flexWrap); 327 | YG_NODE_STYLE_PROPERTY(YGOverflow, Overflow, overflow); 328 | YG_NODE_STYLE_PROPERTY(YGDisplay, Display, display); 329 | 330 | YG_NODE_STYLE_PROPERTY(float, Flex, flex); 331 | YG_NODE_STYLE_PROPERTY(float, FlexGrow, flexGrow); 332 | YG_NODE_STYLE_PROPERTY(float, FlexShrink, flexShrink); 333 | YG_NODE_STYLE_PROPERTY_UNIT_AUTO(YGValue, FlexBasis, flexBasis); 334 | 335 | YG_NODE_STYLE_EDGE_PROPERTY_UNIT(YGValue, Position, position); 336 | YG_NODE_STYLE_EDGE_PROPERTY_UNIT(YGValue, Margin, margin); 337 | YG_NODE_STYLE_EDGE_PROPERTY_UNIT_AUTO(YGValue, Margin); 338 | YG_NODE_STYLE_EDGE_PROPERTY_UNIT(YGValue, Padding, padding); 339 | YG_NODE_STYLE_EDGE_PROPERTY(float, Border, border); 340 | 341 | YG_NODE_STYLE_PROPERTY_UNIT_AUTO(YGValue, Width, width); 342 | YG_NODE_STYLE_PROPERTY_UNIT_AUTO(YGValue, Height, height); 343 | YG_NODE_STYLE_PROPERTY_UNIT(YGValue, MinWidth, minWidth); 344 | YG_NODE_STYLE_PROPERTY_UNIT(YGValue, MinHeight, minHeight); 345 | YG_NODE_STYLE_PROPERTY_UNIT(YGValue, MaxWidth, maxWidth); 346 | YG_NODE_STYLE_PROPERTY_UNIT(YGValue, MaxHeight, maxHeight); 347 | 348 | // Yoga specific properties, not compatible with flexbox specification 349 | // Aspect ratio control the size of the undefined dimension of a node. 350 | // Aspect ratio is encoded as a floating point value width/height. e.g. A value of 2 leads to a node 351 | // with a width twice the size of its height while a value of 0.5 gives the opposite effect. 352 | // 353 | // - On a node with a set width/height aspect ratio control the size of the unset dimension 354 | // - On a node with a set flex basis aspect ratio controls the size of the node in the cross axis if 355 | // unset 356 | // - On a node with a measure function aspect ratio works as though the measure function measures 357 | // the flex basis 358 | // - On a node with flex grow/shrink aspect ratio controls the size of the node in the cross axis if 359 | // unset 360 | // - Aspect ratio takes min/max dimensions into account 361 | YG_NODE_STYLE_PROPERTY(float, AspectRatio, aspectRatio); 362 | 363 | YG_NODE_LAYOUT_PROPERTY(float, Left); 364 | YG_NODE_LAYOUT_PROPERTY(float, Top); 365 | YG_NODE_LAYOUT_PROPERTY(float, Right); 366 | YG_NODE_LAYOUT_PROPERTY(float, Bottom); 367 | YG_NODE_LAYOUT_PROPERTY(float, Width); 368 | YG_NODE_LAYOUT_PROPERTY(float, Height); 369 | YG_NODE_LAYOUT_PROPERTY(YGDirection, Direction); 370 | YG_NODE_LAYOUT_PROPERTY(bool, HadOverflow); 371 | 372 | // Get the computed values for these nodes after performing layout. If they were set using 373 | // point values then the returned value will be the same as YGNodeStyleGetXXX. However if 374 | // they were set using a percentage value then the returned value is the computed value used 375 | // during layout. 376 | YG_NODE_LAYOUT_EDGE_PROPERTY(float, Margin); 377 | YG_NODE_LAYOUT_EDGE_PROPERTY(float, Border); 378 | YG_NODE_LAYOUT_EDGE_PROPERTY(float, Padding); 379 | 380 | WIN_EXPORT void YGConfigSetLogger(const YGConfigRef config, YGLogger logger); 381 | WIN_EXPORT void YGLog(const YGNodeRef node, YGLogLevel level, const char *message, ...); 382 | WIN_EXPORT void YGLogWithConfig(const YGConfigRef config, YGLogLevel level, const char *format, 383 | ...); 384 | WIN_EXPORT void YGAssert(const bool condition, const char *message); 385 | WIN_EXPORT void YGAssertWithNode(const YGNodeRef node, const bool condition, const char *message); 386 | WIN_EXPORT void YGAssertWithConfig(const YGConfigRef config, const bool condition, 387 | const char *message); 388 | 389 | // Set this to number of pixels in 1 point to round calculation results 390 | // If you want to avoid rounding - set PointScaleFactor to 0 391 | WIN_EXPORT void YGConfigSetPointScaleFactor(const YGConfigRef config, const float pixelsInPoint); 392 | 393 | // Yoga previously had an error where containers would take the maximum space possible instead of 394 | // the minimum 395 | // like they are supposed to. In practice this resulted in implicit behaviour similar to align-self: 396 | // stretch; 397 | // Because this was such a long-standing bug we must allow legacy users to switch back to this 398 | // behaviour. 399 | WIN_EXPORT void YGConfigSetUseLegacyStretchBehaviour(const YGConfigRef config, 400 | const bool useLegacyStretchBehaviour); 401 | 402 | // YGConfig 403 | WIN_EXPORT YGConfigRef YGConfigNew(void); 404 | WIN_EXPORT void YGConfigFree(const YGConfigRef config); 405 | WIN_EXPORT void YGConfigCopy(const YGConfigRef dest, const YGConfigRef src); 406 | WIN_EXPORT int32_t YGConfigGetInstanceCount(void); 407 | 408 | WIN_EXPORT void YGConfigSetExperimentalFeatureEnabled(const YGConfigRef config, 409 | const YGExperimentalFeature feature, 410 | const bool enabled); 411 | WIN_EXPORT bool YGConfigIsExperimentalFeatureEnabled(const YGConfigRef config, 412 | const YGExperimentalFeature feature); 413 | 414 | // Using the web defaults is the prefered configuration for new projects. 415 | // Usage of non web defaults should be considered as legacy. 416 | WIN_EXPORT void YGConfigSetUseWebDefaults(const YGConfigRef config, const bool enabled); 417 | WIN_EXPORT bool YGConfigGetUseWebDefaults(const YGConfigRef config); 418 | 419 | WIN_EXPORT void YGConfigSetNodeClonedFunc(const YGConfigRef config, 420 | const YGNodeClonedFunc callback); 421 | 422 | // Export only for C# 423 | WIN_EXPORT YGConfigRef YGConfigGetDefault(void); 424 | 425 | WIN_EXPORT void YGConfigSetContext(const YGConfigRef config, void *context); 426 | WIN_EXPORT void *YGConfigGetContext(const YGConfigRef config); 427 | 428 | WIN_EXPORT void YGSetMemoryFuncs(YGMalloc ygmalloc, YGCalloc yccalloc, YGRealloc ygrealloc, 429 | YGFree ygfree); 430 | 431 | WIN_EXPORT float YGRoundValueToPixelGrid(const float value, const float pointScaleFactor, 432 | const bool forceCeil, const bool forceFloor); 433 | 434 | YG_EXTERN_C_END 435 | 436 | #pragma once 437 | 438 | #include 439 | #include 440 | #include 441 | #include 442 | 443 | #include "Yoga.h" 444 | 445 | YG_EXTERN_C_BEGIN 446 | 447 | typedef struct YGNodeList *YGNodeListRef; 448 | 449 | YGNodeListRef YGNodeListNew(const uint32_t initialCapacity); 450 | void YGNodeListFree(const YGNodeListRef list); 451 | uint32_t YGNodeListCount(const YGNodeListRef list); 452 | void YGNodeListAdd(YGNodeListRef *listp, const YGNodeRef node); 453 | void YGNodeListInsert(YGNodeListRef *listp, const YGNodeRef node, const uint32_t index); 454 | void YGNodeListReplace(const YGNodeListRef list, const uint32_t index, const YGNodeRef newNode); 455 | void YGNodeListRemoveAll(const YGNodeListRef list); 456 | YGNodeRef YGNodeListRemove(const YGNodeListRef list, const uint32_t index); 457 | YGNodeRef YGNodeListDelete(const YGNodeListRef list, const YGNodeRef node); 458 | YGNodeRef YGNodeListGet(const YGNodeListRef list, const uint32_t index); 459 | YGNodeListRef YGNodeListClone(YGNodeListRef list); 460 | 461 | YG_EXTERN_C_END 462 | -------------------------------------------------------------------------------- /Sources/CoreRenderObjC/include/CoreRenderObjC.h: -------------------------------------------------------------------------------- 1 | #import "../CRContext.h" 2 | #import "../CRCoordinator.h" 3 | #import "../CRHostingView.h" 4 | #import "../CRMacros.h" 5 | #import "../CRNode.h" 6 | #import "../CRNodeBridge.h" 7 | #import "../CRNodeBuilder.h" 8 | #import "../CRNodeBuilder+Modifiers.h" 9 | #import "../CRNodeHierarchy.h" 10 | #import "../CRNodeLayoutSpec.h" 11 | #import "../UIView+CRNode.h" 12 | #import "../YGLayout.h" 13 | #import "../Yoga.h" 14 | #import 15 | 16 | FOUNDATION_EXPORT double CoreRenderObjCVersionNumber; 17 | FOUNDATION_EXPORT const unsigned char CoreRenderObjCVersionString[]; 18 | -------------------------------------------------------------------------------- /Tests/CoreRenderObjCTests/CRNodeTests.m: -------------------------------------------------------------------------------- 1 | @import CoreRenderObjC; 2 | @import XCTest; 3 | 4 | @interface CRNodeTests : XCTestCase 5 | @property(nonatomic, weak) UILabel *testOutlet; 6 | @end 7 | 8 | @interface TestCoordinator : CRCoordinator 9 | @end 10 | 11 | @interface TestStatelessCoordinator : CRCoordinator 12 | @end 13 | 14 | @implementation CRNodeTests 15 | 16 | - (CRNode *)buildLabelNode { 17 | const auto node = [CRNode nodeWithType:UILabel.class 18 | layoutSpec:^(CRNodeLayoutSpec *spec) { 19 | [spec set:CR_KEYPATH(spec.view, text) value:@"test"]; 20 | [spec set:CR_KEYPATH(spec.view, textColor) value:UIColor.redColor]; 21 | }]; 22 | return node; 23 | } 24 | 25 | - (void)assertLabelDidLayout:(UIView *)view { 26 | const auto label = CR_DYNAMIC_CAST(UILabel, view); 27 | XCTAssertNotNil(label); 28 | XCTAssert([label.text isEqualToString:@"test"]); 29 | XCTAssert([label.textColor isEqual:UIColor.redColor]); 30 | const auto rect = label.frame; 31 | XCTAssert(!CGRectEqualToRect(rect, CGRectZero)); 32 | XCTAssert(!CGRectEqualToRect(rect, CGRectZero)); 33 | } 34 | 35 | - (void)testTrivialLayout { 36 | const auto node = [self buildLabelNode]; 37 | const auto view = [[UIView alloc] init]; 38 | [node reconcileInView:view 39 | constrainedToSize:CGSizeMake(320, CR_CGFLOAT_FLEXIBLE) 40 | withOptions:CRNodeLayoutOptionsSizeContainerViewToFit]; 41 | XCTAssert(view.subviews.count == 1); 42 | [self assertLabelDidLayout:view.subviews.firstObject]; 43 | } 44 | 45 | - (void)testTrivialLayoutAnimated { 46 | const auto node = [self buildLabelNode]; 47 | const auto view = [[UIView alloc] init]; 48 | const auto context = [[CRContext alloc] init]; 49 | context.layoutAnimator = [[UIViewPropertyAnimator alloc] init]; 50 | [node reconcileInView:view 51 | constrainedToSize:CGSizeMake(320, CR_CGFLOAT_FLEXIBLE) 52 | withOptions:CRNodeLayoutOptionsSizeContainerViewToFit]; 53 | [node registerNodeHierarchyInContext:context]; 54 | XCTAssert(view.subviews.count == 1); 55 | [self assertLabelDidLayout:view.subviews.firstObject]; 56 | } 57 | 58 | - (void)testNestedLayout { 59 | const auto node = [CRNode nodeWithType:UIView.self 60 | layoutSpec:^(CRNodeLayoutSpec *spec) { 61 | [spec set:CR_KEYPATH(spec.view, backgroundColor) 62 | value:UIColor.redColor]; 63 | [spec set:CR_KEYPATH(spec.view, yoga.padding) value:@42]; 64 | }]; 65 | [node appendChildren:@[ [self buildLabelNode], [self buildLabelNode], [self buildLabelNode] ]]; 66 | const auto containerView = [[UIView alloc] init]; 67 | const auto context = [[CRContext alloc] init]; 68 | [node registerNodeHierarchyInContext:context]; 69 | 70 | [node reconcileInView:containerView 71 | constrainedToSize:CGSizeMake(320, CR_CGFLOAT_FLEXIBLE) 72 | withOptions:CRNodeLayoutOptionsSizeContainerViewToFit]; 73 | 74 | const auto view = containerView.subviews.firstObject; 75 | const auto sv = view.subviews; 76 | 77 | XCTAssertNotNil(view); 78 | [self assertLabelDidLayout:sv[0]]; 79 | [self assertLabelDidLayout:sv[1]]; 80 | [self assertLabelDidLayout:sv[2]]; 81 | 82 | XCTAssert(fabs(CGRectGetMaxY(sv[0].frame) - sv[1].frame.origin.y) <= 1.0); 83 | XCTAssert(fabs(CGRectGetMaxY(sv[1].frame) - sv[2].frame.origin.y) <= 1.0); 84 | } 85 | 86 | - (void)testThatCoordinatorIsPassedDownToNodeSubtree { 87 | __block auto expectRootNodeHasCoordinator = NO; 88 | __block auto expectLeafNodeHasCoordinator = NO; 89 | const auto root = 90 | [CRNode nodeWithType:UIView.class 91 | reuseIdentifier:nil 92 | key:@"foo" 93 | viewInit:nil 94 | layoutSpec:^(CRNodeLayoutSpec *spec) { 95 | const auto coordinator = [spec coordinatorOfType:TestCoordinator.class]; 96 | expectRootNodeHasCoordinator = CR_DYNAMIC_CAST(TestCoordinator, coordinator); 97 | }]; 98 | [root bindCoordinator:self.testDescriptor]; 99 | 100 | const auto leaf = 101 | [CRNode nodeWithType:UIView.class 102 | layoutSpec:^(CRNodeLayoutSpec *spec) { 103 | const auto coordinator = [spec coordinatorOfType:TestCoordinator.class]; 104 | expectLeafNodeHasCoordinator = CR_DYNAMIC_CAST(TestCoordinator, coordinator); 105 | }]; 106 | [root appendChildren:@[ leaf ]]; 107 | 108 | const auto context = [[CRContext alloc] init]; 109 | [root registerNodeHierarchyInContext:context]; 110 | 111 | const auto view = [[UIView alloc] init]; 112 | [root reconcileInView:view 113 | constrainedToSize:CGSizeMake(320, CR_CGFLOAT_FLEXIBLE) 114 | withOptions:CRNodeLayoutOptionsSizeContainerViewToFit]; 115 | XCTAssertTrue(expectRootNodeHasCoordinator); 116 | XCTAssertTrue(expectLeafNodeHasCoordinator); 117 | } 118 | 119 | - (CRCoordinatorDescriptor *)testDescriptor { 120 | return [[CRCoordinatorDescriptor alloc] initWithType:TestCoordinator.class key:@"test"]; 121 | } 122 | 123 | @end 124 | 125 | @implementation TestCoordinator 126 | @end 127 | 128 | @implementation TestStatelessCoordinator 129 | @end 130 | -------------------------------------------------------------------------------- /Tests/CoreRenderTests/BridgeTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CoreRenderObjC 3 | import CoreRender 4 | 5 | class FooCoordinator: Coordinator { 6 | var count: Int = 0 7 | func increase() { count += 1 } 8 | } 9 | 10 | class BarCoordinator: Coordinator { 11 | } 12 | 13 | class CRSwiftInteropTests: XCTestCase { 14 | 15 | func testGetCoordinator() { 16 | let context = Context() 17 | 18 | Component(context: context) { _, _ in 19 | VStackNode { 20 | LabelNode(text: "Foor") 21 | LabelNode(text: "Bar") 22 | Component(context: context) { _, _ in 23 | ButtonNode(key: "Hi") 24 | } 25 | } 26 | } 27 | } 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /Tests/CoreRenderTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ ] 6 | } 7 | #endif 8 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import CoreRenderTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += CoreRenderTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /docs/assets/carbon_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexdrone/CoreRender/96736a47fbaf771f0d79a24a5e8e45cfac477aff/docs/assets/carbon_4.png -------------------------------------------------------------------------------- /docs/assets/logo_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexdrone/CoreRender/96736a47fbaf771f0d79a24a5e8e45cfac477aff/docs/assets/logo_new.png -------------------------------------------------------------------------------- /docs/assets/screen_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexdrone/CoreRender/96736a47fbaf771f0d79a24a5e8e45cfac477aff/docs/assets/screen_2.png -------------------------------------------------------------------------------- /format_objc.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | format() { 4 | PATH=$(pwd); 5 | echo "✓ $PATH"; 6 | /usr/local/bin/clang-format -i *.h &>/dev/null 7 | /usr/local/bin/clang-format -i *.m &>/dev/null 8 | /usr/local/bin/clang-format -i *.mm &>/dev/null 9 | /usr/local/bin/clang-format -i *.c &>/dev/null 10 | } 11 | 12 | echo "Running clang-format..." 13 | cd Sources/CoreRenderObjC && format; 14 | cd ../../; 15 | cd Tests/CoreRenderObjCTests && format; 16 | --------------------------------------------------------------------------------